Пориньте у Python 3/Версія для друку

Матеріал з Вікіпідручника
Перейти до: навігація, пошук

Зміст

Що нового?[ред.]

❝ Isn’t this where we came in? ❞
— Pink Floyd, The Wall

також відомий під назвою “мінусовий рівень"[ред.]

Ви вже Python програміст? Ви перечитали в оригіналі “Занурення в Python”? Ви купили паперовий примірник? (Якщо так, дякую!) Ви готові з головою пірнути в Python 3? Коли так, читайте далі. Але якщо ні, вам варто почати спочатку.

Python 3 йде в комплекті зі скриптом, що називається 2to3. Вивчіть його. Покохайте його. Використовуйте його. “Перенесення коду в Python 3 за допомогою 2to3” — це довідник усіх змін, які можуть автоматично виправити інструменти 2to3. Оскільки більшість змін є синтаксичними, їх вивчення стане гарним початком. (print відтепер функція, `x` не працює та інше)

“Приклад: перенесення chardet на Python 3” розповідає про мої (врешті-решт, успішні) спроби перенести нетривіальну бібліотеку з Python 2 на Python 3. Можливо, це вам допоможе, а можливо — ні. Навчальна крива виходить доволі крутою, оскільки вам потрібно гарно розібратися в бібліотеці, перш ніж ви зможете зрозуміти, чому вона зламалася і як я все виправив. Найбільш вразливі місця часто розташовані навколо рядків. До речі, щодо рядків...

Рядки... Отакої... Із чого ж розпочати?.. У Python 2 були “strings” та “Unicode strings”. У Python 3 є “bytes” та “strings”. Це означає, що усі рядки відтепер Unicode, і якщо ви хочете працювати із купкою байтів, використовуйте новий байтовий тип. Python 3 ніколи автоматично не здійснюватиме перетворення між strings та bytes, тому, якщо ви не певні будь-якої миті з чим саме працюєте, ваш код майже точно зламається. Для більш детальної інформації дивіться розділ "Текст".

Тема протистояння bytes і strings знову і знову виринатиме на поверхню у цій книзі.

  • У розділі "Файли" ви дізнаєтесь різницю між читанням файлу в режимі “binary” та “text”. Читання (та запис!) в текстовому режимі потребують параметру encoding. Деякі методи текстових файлів рахують символи, а інші рахують байти. Якщо ваш код спирається на те, що один символ == один байт, він зламається на багатобайтових символах.
  • У розділі "Веб сервіси http" модуль httplib2 виокремлює заголовки та дані з http. Заголовки повертаються як рядки, а тіло http повертається у вигляді байтів.
  • У розділі "Серіалізація об'єктів" ви дізнаєтесь, чому модуль pickle у Python 3 визначає новий формат даних, що є несумісним із Python 2. (Натякну: це через відмінності між bytes та strings). До того ж Python 3 підтримує серіалізацію об'єктів json, який взагалі не має bytes. Я покажу вам, як оминути це.
  • У “Приклад: перенесення chardet на Python 3” просто кривава мішанина із bytes та strings.

Навіть якщо ви не турбуєтесь про Unicode (хоча доведеться), все одно мусите прочитати про форматування рядків у Python 3, яке цілковито відрізняється від тієї ж процедури у Python 2.

Ітератори містяться скрізь у Python 3, і зараз я знаю їх набагато краще, ніж п'ять років тому, коли писав “Занурення в Python”. Вам також треба розуміти їх, адже більшість функцій, які у Python 2 повертали списки, тепер повертають ітератори. Як мінімум, ви мусите прочитати другу половину розділу "Класи та ітератори" і другу половину розділу "Детальніше про ітератори".

За численними проханнями я додав розділ "Імена магічних методів", що схожий на розділ документації "Модель даних", тільки більш гумористичний.

Коли я писав “Занурення в Python”, усі доступні XML бібліотеки були справжнім лайном. Пізніше Фредрик Лунд (Fredrik Lundh) написав ElementTree, яка вже не була лайном. Боги Python мудро включили ElementTree до складу стандартної бібліотеки, і зараз вона стала основою для мого нового розділу "XML". Старі шляхи парсингу XML лишаються доступними, але їх слід уникати, адже вони — справжнє лайно!

Також нове в Python — але не у мові, а у спільноті — виникнення репозиторіїв, схожих на Python Package Index (PyPI). Python має утиліти для пакетування вашого коду в стандартних форматах і подальшого його розповсюдження через PyPI. Докладніше про це в розділі "Пакування бібліотек".

Встановлення[ред.]

Tempora mutantur nos et mutamur in illis. (Змінюються часи, і ми змінюємося разом з ними.)
Давньоримська приказка

Занурення[ред.]

Перш ніж ви зможете почати програмувати на Python 3, вам треба його встановити. Чи вже маєте?


Який Python найкращий для вас?[ред.]

Якщо ви користуєтесь віддаленим сервером, ваш хостинг-провайдер, ймовірно, вже має встановлений Python 3. Якщо у вас Linux вдома, то швидше за все ви також маєте Python 3. Більшість популярних дистрибутивів GNU/Linux типово йдуть в комплекті із Python 2, у невеликій, але постійно збільшуваній кількості дистрибутивів також включений і Python 3. Mac OS X містить консольну версію Python 2, проте не містить Python 3. Microsoft Windows не має жодної версії Python. Але не впадайте у відчай! Незалежно від вашої операційної системи, шлях до встановлення Python складатиметься з кількох клацань мишею.

Найпростіше перевірити, чи є Python 3 на вашій операційній системі Linux або Mac OS X - через консоль. Отримавши запрошення командного рядка, наберіть python3 (маленькими літерами, без пробілів), натисніть ENTER і подивіться, що відбуватиметься. На моєму домашньому Linux Python 3.1 вже є, і тому ця команда запускає інтерактивне середовище Python.

mark@atlantis:~$ python3
Python 3.0.1+ (r301:69556, Apr 15 2009, 17:25:52)
[GCC 4.3.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>

(Наберіть exit та натисніть ENTER, аби вийти з інтерактивного середовища)

Мій хостинг-провайдер також має Linux і надає доступ до командного рядка, проте на моєму сервері немає встановленого Python 3. (Ганьба!)

mark@manganese:~$ python3
bash: python3: command not found

Отже, повернімося до запитання, з якого і розпочався цей розділ: який Python найкращий для вас? Будь-який, що працює на вашому комп'ютері.

Встановлення на Microsoft Windows[ред.]

Зараз існують системи Microsoft Windows для двох архітектур: 32-бітної і 64-бітної. Звичайно, є безліч версій Windows - XP, Vista, Windows 7 - Python працюватиме на кожній з них. Набагато важливішою є різниця між 32-бітною і 64-бітною версіями. Якщо ви і гадки не маєте, яка архітектура вашого комп'ютера, ймовірно, вона 32-бітна.

Відвідайте python.org/download і завантажте відповідний встановник Python 3 для вашої архітектури Windows. Ваш вибір буде схожим на щось таке:

  • Python 3.1 Windows installer (Windows binary — does not include source)
  • Python 3.1 Windows AMD64 installer (Windows AMD64 binary — does not include source)

Я не хочу давати тут прямі посилання на файли, оскільки в Python постійно відбуваються якісь незначні оновлення, а мені б не хотілося відповідати за те, що вас омине щось важливе. Ви завжди можете встановити найсвіжішу версію Python 3.x, якщо ваша релігія вам це дозволяє.

WindowsDialogOpenFileSecurityWarning.png

Коли .msi файл завантажиться, клацніть на ньому двічі. На екрані з'явиться віконце-застереження Windows з попередженням, що ви запускаєте виконуваний код. Офіційний встановник Python має цифровий підпис від Python Software Foundation, некомерційної організації, що здійснює нагляд за розробкою Python. Остерігайтеся підробок!

Натисніть кнопку "Run", аби розпочати інсталяцію Python 3.

PythonInstallerSelectUsers.png

Перше питання, яке поставить вам встановник: чи бажаєте ви встановити Python 3 для всіх користувачів цього комп'ютера, чч тільки для вас. Вибір за замовчуванням "встановлювати для усіх користувачів" - найкращий варіант, якщо ви не маєте серйозних причин для іншого. (Одна з можливих причин вибору "тільки для мене" така, що ви встановлюєте Python на свій робочий комп'ютер, не маючи адміністративних повноважень. Але чому ви встановлюєте Python без дозволу адміністратора вашої компанії?.. Не втягуйте мене в неприємності!)

Клацніть "Next", щоб підтвердити обраний тип встановлення.

PythonInstallerSelectDestinationDirectory.png

Далі встановник запропонує вам обрати кінцеву папку. Для усіх версій Python 3.1.x типовим є C:\Python31\, що є прийнятним для більшості користувачів, якщо немає якихось специфічних причин її змінювати. Якщо у вас окремий логічний диск для встановлення застосунків, ви можете обрати його, користуючись вбудованими інструментами, або просто вписати шлях у відповідному полі. Python можна встановлювати не лише на диск C: - у виборі логічного диска і кінцевої папки вас ніхто не обмежує.

Клацніть"Next", щоб підтвердити вибір кінцевої папки.

PythonInstallerCustomizePython.png

Наступне вікно виглядає заскладним, але це лише враження. Так само, як у інших встановниках, ви маєте можливість відмовитися від деяких окремих компонентів Python 3. Якщо вільного місця на диску катма мало, скористайтесь цим діалогом.

  • Register Extensions дозволяє запускати скрипти Python (.py) подвійним клацанням. Рекомендовано, але не обов'язково. (Це налаштування не займає місця на диску, а отже, мало сенсу його видаляти.)
  • Tcl/Tk - це графічна бібліотека, що використовується середовищем Python, до якої ви звертатиметеся під час читання цієї книги. Я наполегливо рекомендую залишити її.
  • Опція документації встановлює файли довідки, що містять переважну більшість інформації з docs.python.org. Рекомендовано, якщо у вас dialup або обмежений доступ до інтернету.
  • Utility Scripts містить скрипт 2to3.py, про який ви прочитаєте у цій книзі трохи пізніше. Необхідно, якщо ви хочете дізнатися, як переносити наявний код із Python 2 на Python 3. Якщо у вас немає коду на Python 2, можете вилучити цей компонент.
  • Test Suite - це набір скриптів для тестування самого інтерпретатора Python. Ми не використовуватимемо їх у цій книзі, та і я за весь час програмування на Python жодного разу не користувався ними. Цілком необов'язково.

PythonInstallerDiskSpaceRequirements.png

Якщо ви не певні, скільки у вас вільного дискового простору, клацніть по кнопці "Disk Usage". Встановник покаже список логічних дисків, порахує, скільки на кожному з них вільного простору зараз і скільки залишиться після встановлення. Клацніть "Ok", щоб повернутися до діалогу вибору компонентів Python.

PythonInstallerRemovingTestSuite.png

Якщо ви вирішили вилучити один з компонентів, натисніть на кнопку перед ним і у спадному меню оберіть "Entire feature will be unavailable". Для прикладу, вилучення test suite збереже вам 7908 кілобайт дискового простору.

Клацніть"Next", щоб підтвердити свій вибір опцій.

PythonInstallerProgressMeter.png

Встановник копіюватиме усі необхідні файли до кінцевої папки. (Це відбувається так швидко, що мені пощастило зробити скріншот лише з третьої спроби!)

PythonInstallerInstallationCompleted.png

[1] Клацніть"Finish", щоб завершити роботу встановника.

WindowsPythonShell.png

У вашому меню "Пуск" має з'явитися новий пункт із назвою Python 3.1. Всередині нього буде програма названа IDLE. Вибір цього елемента запустить інтерактивне середовище Python.

Перейти до "Використання середовища Python".


* * *


Встановлення на Mac OS X[ред.]

Усі сучасні комп'ютери Macintosh використовують процесори Intel (так само, як і більшість Windows PC). Старіші Mac'и використовували процесори PowerPC. Вам не потрібно розуміти відмінність між ними, адже існує лише один встановник Python для Mac'а.

Відвідайте python.org/download і завантажте встановник для Mac'а. Він називатиметься якось схоже на Python 3.1 Mac Installer Disk Image, номер версії може варіюватися. Переконайтеся, що завантажуєте саме версію 3.x, а не 2.x.

Mac-install-0-dmg-contents.png

Ваш браузер має автоматично підмонтувати образ диска і відкрити вікно Finder, щоб показати його вміст. (Якщо це не відбулося, вам потрібно знайти образ диска у вашій папці завантажень і двічі клацнути по ньому мишею для підмонтування. Він називатиметься якось схоже на python-3.1.dmg.) Образ диска міститиме певну кількість текстових файлів (Build.txt, License.txt, ReadMe.txt) і, власне, пакет встановника Python.mpkg.

Двічі клацніть пакет встановника Python.mpkg, щоб розпочалася інсталяція для Mac.

Mac-install-1-welcome.png

Перше вікно встановника покаже вам короткий опис Python і порадить звернутися до файла ReadMe.txt (який ви не читали, чи не так?) за більш детальними відомостями.

Клацніть"Continue", щоб продовжити встановлення.

Mac-install-2-information.png

Наступне вікно містить справді важливу інформацію: для встановлення Python вам необхідно мати Mac OS X 10.3 або пізніші версії. Якщо у вас ще досі Mac OS X 10.2, то її дійсно треба оновити. Apple більше не випускає оновлення безпеки для вашої операційної системи, і ваш комп'ютер наражається на небезпеку, просто під'єднуючись до інтернету. Не кажучи вже про те, що на ньому не працюватиме Python 3.

Клацніть"Continue", щоб перейти до наступного кроку.

Mac-install-3-license.png

Як й інші пристойні встановники, встановник Python демонструє ліцензійну угоду програмного забезпечення. Python - це програмне забезпечення із відкритим вихідним кодом, і його ліцензія схвалена Open Source Initiative. За час свого існування Python змінив певну кількість власників і спонсорів, кожен з яких залишив свій слід у його ліцензії. Але в кінцевому результаті Python має вільний вихідний код і ви можете використовувати його на будь-якій платформі, з будь-якою метою, безкоштовно, без жодних зобов'язань.

Клацніть"Continue" ще раз.

Mac-install-4-license-dialog.png

Через вибрики в стандартах Apple installer framework ви маєте "погодитися" із ліцензійною угодою, щоб продовжити встановлення. Оскільки Python - відкрите програмне забезпечення, "згода" радше додасть вам повноважень, аніж обмежить їх.

Клацніть"Agree", щоб продовжити.

Mac-install-5-standard-install.png

Наступне вікно дозволить вам змінити місце інсталяції. Вам потрібно встановити Python на системний диск, але через обмеження встановника це не перевіряється. Відверто кажучи, мені ніколи не доводилося змінювати місце інсталяції.

Тут ви також можете обрати компоненти, які бажаєте встановити, і вилучити непотрібні. Для цього клацніть "Customize", а в іншому випадку - на "Install".

Mac-install-6-custom-install.png

Якщо ви хочете обрати серед компонентів Python, встановник покаже вам наступний список:

  • Python Framework. Це основна складова Python , вона завжди вибрана і неактивна, оскільки обов'язково має бути встановлена.
  • GUI Applications - це IDLE, графічне середовище Python, яким ви будете користуватися під час читання цієї книги. Я наполегливо рекомендую залишити цей компонент.
  • UNIX command-line tools - це консольні застосунки для Python 3. Я також наполегливо рекомендую залишити цей компонент.
  • Опція документації встановлює файли довідки, що містять переважну більшість інформації з docs.python.org. Рекомендовано, якщо у вас dialup або обмежений доступ до інтернету.
  • Shell profile updater керує оновленнями вашого профілю середовища (використовується у Terminal.app)для того, щоб забезпечити перебування цієї версії Python у шляхах пошуку середовища. Швидше за все, вам не доведеться змінювати цю опцію.
  • Fix system Python краще не обирати. (Після цього ваш Mac використовуватиме python3 для виконання усіх скриптів Python включно із системними скриптами від Apple). Це дуже погано, адже більшість таких скриптів написані для Python 2 і запустити їх під Python 3 без помилок буде неможливо.

Клацніть"Install", щоб продовжити.

Mac-install-7-admin-password.png

Через те, що системний фреймворк і бінарні файли встановлюватимемуться до /usr/local/bin/, встановник запитає у вас пароль адміністратора. Встановити Python на Mac без повноважень адміністратора неможливо.

Клацніть"Ok" для початку інсталяції.

Mac-install-8-progress.png

Поки встановлюються обрані вами елементи, буде показуватися індикатор прогресу.

Mac-install-9-succeeded.png

Якщо усе добре, встановник покаже вам великий зелений прапорець, що означає успішне завершення інсталяції.

Клацніть"Close", аби завершити роботу встановника.

Mac-install-10-application-folder.png

Якщо припустити, що ви не змінювали місце інсталяції, щойно встановлені файли можна буде знайти в папці Python 3.1, яка всередині /Applications. Найважливіший серед них IDLE - графічне середовище Python.

Двічі клацніть IDLE, щоб запустити середовище Python.

Mac-interactive-shell.png

Середовище Python - те місце, де ви перебуватимете майже весь час, вивчаючи Python. Приклади у цій книзі розраховані на те, що ви знаєте, як знайти середовище Python на своїй системі.

Перейти до "Використання середовища Python".

Встановлення на Ubuntu Linux[ред.]

Сучасні дистрибутиви Linux спираються на величезні репозиторії попередньо скомпільованих застосунків, вже готових до встановлення. Точні інструкції варіюються для різних дистрибутивів, але в Ubuntu Linux найпростіший шлях встановити Python 3 - програма "Add/Remove", що є у вашому меню застосунків.

Ubu-install-0-add-remove-programs.png

Коли ви вперше запустите "Add/Remove", вона покаже вам список попередньо обраних застосунків у різних категоріях. Деякі у вас вже встановлені, але більшість - ні. Оскільки репозиторій містить понад 10 000 застосунків, існують різні фільтри, за допомогою яких ви можете переглянути невелику частину вмісту репозиторію. Фільтр - "Canonical-maintained applications" - демонструє частку застосунків, офіційно підтримуваних Canonical - компанії, що колись створила і досі опікується Ubuntu Linux.

Ubu-install-1-all-open-source-applications.png

Python 3 не підтримується Canonical, тож першим кроком для вас стане вибір фільтру "All Open Source applications" у спадному меню.

Ubu-install-2-search-python-3.png

Коли ви оберете фільтр, який включає усі застосунки із відкритим вихідним кодом, скористуйтеся рядком пошуку, аби віднайти у тому переліку Python 3.

Ubu-install-3-select-python-3.png

Тепер список застосунків скоротився до переліку таких, що відповідають запиту "Python 3". Вам треба відмітити два пакети. Перший - Python (v3.0). У ньому міститься власне інтерпретатор Python.

Ubu-install-4-select-idle.png

Другий пакет розташований прямо над першим: IDLE (using Python-3.0). Це - графічне середовище Python, яке ви будете використовувати під час читання цієї книги.

Після того, як ви відмітите ці два пакети, клацніть Apply Changes, щоб продовжити.

Ubu-install-5-apply-changes.png

Пакетний менеджер запитає, чи справді ви хочете додати ці два пакети.

Клацніть Apply, щоб продовжити.

Ubu-install-6-download-progress.png

Поки завантажуються пакети із інтернет-репозиторію Canonical, пакетний менеджер показуватиме вам індикатор прогресу завантаження.

Ubu-install-7-install-progress.png

Одразу після завантаження розпочнеться автоматичне встановлення пакетів.

Ubu-install-8-success.png

Якщо все пройшло добре, пакетний менеджер підтвердить, що обидва пакети були успішно встановлені. Звідси ви зможете запустити середовище Python, двічі клацніть IDLE, і закрити пакетний менеджер, клацніть Close.

Ви завжди можете запустити середовище Python з меню застосунків, клацніть позначку IDLE у підменю Programming.

Ubu-interactive-shell.png

Середовище Python - місце, де ви проведете більшість часу, досліджуючи Python. Приклади у цій книзі розраховані на те, що ви знаєте, як його запустити.

Перейти до "Використання середовища Python".

Встановлення на інші платформи[ред.]

Python 3 доступний для великої кількості різноманітних платформ. Якщо говорити конкретніше, він доступний майже для всіх дистрибутивів Linux, bsd і Solaris. Наприклад, RedHat Linux використовує пакетний менеджер yum. У FreeBSD є свої порти і колекції пакетів, у suse є zypper, а у Solaris - pkgadd. Швидкий веб-пошук зі словами "Python 3 + назва вашої операційної системи" покаже вам, чи є для неї пакет Python 3, і якщо так, то яким чином його встановити.

Використання середовища Python[ред.]

Середовище Python - те місце, де ви зможете досліджувати синтаксис Python, отримувати інтерактивну допомогу по командах і зневаджувати короткі програми. Графічне середовище Python (IDLE) також містить і непоганий текстовий редактор, що підтримує забарвлення синтаксичних елементів Python і безпосередньо поєднано з інтерактивним середовищем. Якщо у вас ще немає улюбленого текстового редактора, спробуйте IDLE.

Перш за все, інтерактивне середовище - чудовий ігровий майданчик для випробування Python. У книзі вам часто траплятимуться приклади, схожі на щось таке:

>>> 1 + 1
2

Три праві кутові дужки, >>>, це запрошення середовища Python. Його не треба набирати. Воно лише означає, що поданий приклад слід вводити у середовищі Python.

1 + 1 - це та частина, яку ви набираєте. Ви можете набрати будь-який коректний вираз або команду Python в інтерактивному середовищі. Не соромтеся - воно не кусається! Найгірше, що може трапитися - ви отримаєте повідомлення про помилку. Команди виконуються одразу ж (як тільки ви натиснете "ENTER"), значення виразів обчислюються теж одразу, а середовище Python показує вам результат.

2 є результатом обчислення цього виразу. Як виявилося, 1 + 1 є коректним виразом на мові Python, результатом якого, ясна річ, є 2. Спробуйте ще одне:

>>> print('Hello world!')
Hello world!

Надзвичайно просто, чи не так? Але є ще безліч речей, які ви можете робити у середовищі Python. Якщо ви колись застрягнете - не зможете згадати команду або потрібні аргументи для якоїсь функції - у середовищі Python завжди можна отримати інтерактивну довідку. Просто наберіть "help" та натисніть "ENTER".

>>> help
Type help() for interactive help, or help(object) for help about object.

[2]


Існує два режими довідки. Скориставшись довідкою про поодинокі об'єкти, ви побачите документацію об'єкта, а потім - знову запрошення середовища Python. Також ви можете увійти в режим інтерактивної довідки, де не обчислюватимуться результати виразів Python. У ньому можна просто набирати ключові слова або назви команд, і на екран виведеться усе, що відомо про команду.

Аби увійти в режим інтерактивної довідки, наберіть help() і натисніть "ENTER".

>>> help()
Welcome to Python 3.0!  This is the online help utility.

If this is your first time using Python, you should definitely check out the tutorial on the Internet at http://docs.python.org/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing Python programs and using Python modules.  To quit this help utility and return to the interpreter, just type "quit".

To get a list of available modules, keywords, or topics, type "modules", "keywords", or "topics".  Each module also comes with a one-line summary of what it does; to list the modules whose summaries contain a given word such as "spam", type "modules spam".

[3]

help>

Зауважте, що запрошення із >>> змінилося на help>. Це нагадує, що ви перебуваєте в режимі інтерактивної довідки. Зараз ви можете ввести будь-яке ключове слово, команду, назву модуля або функції - будь-що зрозуміле для Python - і прочитати документацію по ньому.

help> print

[4]

Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout)
    
    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file: a file-like object (stream); defaults to the current sys.stdout.
    sep:  string inserted between values, default a space.
    end:  string appended after the last value, default a newline.

[5]

help> PapayaWhip
no Python documentation found for 'PapayaWhip'

[6] [7]

help> quit

[8]

You are now leaving help and returning to the Python interpreter.
If you want to ask for help on a particular object directly from the
interpreter, you can type "help(object)".  Executing "help('string')"
has the same effect as typing a particular string at the help> prompt.

[9]

>>>

[10]


IDLE, графічне середовище Python, також містить пристосований для Python текстовий редактор.

Редактори коду та IDE для Python[ред.]

Проте IDLE зовсім не унікальна, коли йдеться про написання програм мовою Python. Починаючи програмування на Python із вивчення мови, багато розробників надають перевагу іншим текстовим редакторам або інтегрованим середовищам розробки (IDE). Я не хочу тут докладно на них зупинятися, але спільнота Python має перелік редакторів із підтримкою Python, що охоплюють широке коло платформ і ліцензій.

Ви також можете переглянути список IDE із підтримкою Python, але поки що небагато з них можуть працювати із Python3. Одне з таких PyDev, плагін для Eclipse, що перетворює його на повноцінне середовище розробки на Python. І PyDev, і Eclipse є кросплатформовими та відкритими.

Серед комерційного програмного забезпечення є Komodo IDE від ActiveState. Воно потребує ліцензії для кожного користувача, але студенти матимуть знижки, і можна скористатися безкоштовною версією, що діє обмежений термін.

Я пишу свої програми на Python вже дев'ять років і роблю це в редакторі GNU Emacs, а зневаджую у середовищі Python в консолі. В розробці на Python немає хибних чи правильних шляхів. Знайдіть саме той, що працюватиме для вас!

Примітки[ред.]

  1. Спеціальна Windows подяка Марку Геммонду (Mark Hammond), що присвятив багато років роботі у напрямку Windows, без нього Python для Windows ще й досі був би Python для DOS.
  2. Наберіть help() для інтерактивної довідки або help(object) для довідки про об'єкт.
  3. Ласкаво просимо до Python 3.0! Вас вітає утиліта онлайн-довідки. Якщо ви вперше використовуєте Python, вам неодмінно варто ознайомитися з інтернет-підручником на http://docs.python.org/tutorial/. Введіть назву будь-якого модуля, ключове слово або тему, щоб дізнатися, як писати програми мовою Python, і використовувати модулі Python. Щоб завершити роботу утиліти довідки і повернутися до інтерпретатора, просто наберіть "quit". Щоб отримати список доступних модулів, ключових слів або тем, наберіть "modules", "keywords" або "topics". Кожний модуль має стислий опис того, що він робить. Щоб отримати список модулів, чий опис містить потрібне вам слово, наприклад, "spam", наберіть "modules spam".
  4. Щоб побачити документацію по функції print(), просто наберіть print і натисніть "ENTER". Інтерактивна довідка покаже вам щось схоже на сторінку man: ім'я функції, короткий опис, аргументи функції, їхні значення за замовчуванням та інше. Якщо документація видається вам незрозумілою, не нервуйте. Ви більше дізнаєтеся про це у кількох наступних розділах. (прим. авт.)
  5. Довідка по вбудованій функції print з модуля builtins. Друкує значення до потоку або до sys.stdout за замовчуванням. Необов'язкові ключові аргументи:
    • file - файлоподібний об'єкт (потік); за замовчуванням sys.stdout.
    • sep - рядок, що додається між двома значеннями; за замовчуванням пробіл.
    • end - рядок, що додається після останього значення; за замовчуванням символ нового рядка.
  6. Немає документації Python для "мусс із папайї"
  7. Звичайно, інтерактивна довідка не знає усе. Якщо ви введете слово, що не є командою Python, назвою модуля або функції чи іншим ключовим словом, їй доведеться лише знизати своїми віртуальними плечима. (прим. авт.)
  8. Щоб вийти з режиму інтерактивної допомоги, наберіть quit та натисніть "ENTER". (прим. авт.)
  9. Ви виходите з режиму інтерактивної довідки і повертаєтесь до інтерпретатора Python. Якщо вам знадобиться довідка з якогось конкретного об'єкту безпосередньо з інтерпретатора, наберіть "help(object)". Виконання "help('string')" матиме той самий ефект, що і введення її після запрошення help>.
  10. Запрошення знову стало >>>, щоб повідомити про вихід з режиму інтерактивної допомоги та повернення до середовища Python. (прим. авт.)


Ваша перша програма[ред.]

Не ховай свої турботи у святій тиші. Маєш проблему? Чудово. Возвеселися, занурся і пізнавай.
Високоповажний Генепола Ґунаратана


За традицією, я повинен занудити вас фундаментальними елементами програмування, аби ми могли повільно перейти до створення чогось корисного. Давайте все це пропустимо. Ось завершена програма, написана на Python. Вона, мабуть, взагалі не містить для вас жодного сенсу. Не хвилюйтесь через це, тому що ми збираємось розібрати кожен її рядок. Але спробуйте все-таки прочитати і перевірити, наскільки ви її зрозумієте.

Пірнаймо![ред.]

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
            1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''
    if size < 0:
        raise ValueError('number must be non-negative')

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)

    raise ValueError('number too large')

if __name__ == '__main__':
    print(approximate_size(1000000000000, False))
    print(approximate_size(1000000000000))

Тепер, давайте запустимо її в командному рядку (перед цим збережіть текст в файл на зразок humansize.py). Під Windows це виглядатиме так:

c:\home\diveintopython3\examples> c:\python31\python.exe humansize.py
1.0 TB
931.3 GiB

На MacOS та Linux так:

you@localhost:~/diveintopython3/examples$ python3 humansize.py
1.0 TB
931.3 GiB

Що щойно відбулось? Ви запустили свою першу Python-програму, викликавши в командному рядку інтерпретатор і передавши йому назву скрипта, який хотіли запустити. Скрипт описує єдину функцію approximate_size(), яка отримує точний розмір файлу в байтах, і повертає "гарний", але приблизний розмір.(Ви могли бачити такий в Windows Explorer, чи Mac OS X Finder, чи Nautilus чи Dolphin чи Thunar на Linux, і в багатьох інших файлових менеджерах. Якщо каталог містить файл TODO розміром 1093 байт, файловий менеджер не виводить TODO 1093 bytes; він показує щось на зразок TODO 1 KB натомість.)

У кінці скрипта ви побачите дві команди - print(approximate_size(arguments)). Це виклики функцій. Один викликає approximate_size з аргументами і передає її результат в функцію print. Функція print - вбудована, ви ніколи не побачите її явного опису. Зате ви можете її використовувати будь-де і будь-коли. (Є ціла купа вбудованих функцій і ще більше функцій, які розділені за модулями. Терпіння...)

То чому запуск скрипта з консолі щоразу дає одні й ті ж результати? Давайте розберемось. І спершу подивимось на функцію approximate_size().


* * *


Опис функцій[ред.]

Python, як і більшість інших мов програмування, має функції, але не має окремих заголовкових файлів як C++ чи секції interface/implementation в Pascal. Коли вам необхідна функція, ви просто оголошуєте її, ось так:

def approximate_size(size, a_kilobyte_is_1024_bytes=True):

Оголошення функції починається з ключового слова def, за яким іде ім’я функції, а після неї - параметри в дужках. Якщо параметрів кілька, вони розділяються комами.

Також варто зауважити, що для функції не задається тип, який вона повертає. Функції в Python не фіксують тип даних, який вони повертають. Вони навіть не визначають наперед чи буде взагалі повертатись якесь значення (хоча насправді кожна функція в Python повертає результат. Якщо в ній виконується оператор return - передається значення виразу після нього. Якщо ж після нього нічого не має, або оператор взагалі не використовується, повертається стандартне значення None, тобто "нічого").

В деяких мовах програмування підпрограми поділяються на функції, які повертають результат, та процедури, які просто виконують певні дії. Python не має такого поділу, тому всі функції повертають хоч щось, навіть якщо це щось - нічого (None).

Функція approximate_size() отримує два аргументи: size та a_kilobyte_is_1024_bytes, проте для жодного з них не задається тип даних. В Python змінні ніколи явно не отримують тип. Python сам з’ясовує якого типу значення містить змінна.

В Java та інших статично типізованих мовах потрібно задавати тип значення, що повертається функцією, та типи її аргументів. В Python тип явно ніколи не описується. Python стежить за дотриманням типів значень, які ви присвоюєте змінним, самостійно.

Необов’язкові та іменовані аргументи[ред.]

Python дозволяє задавати для аргументів типові значення. Якщо значення аргументу не передається при виклику функції, то йому нададуть типового значення. Крім того, аргументи можна передавати в довільному порядку, якщо використовувати іменовані аргументи.

Для прикладу, розглянемо ще раз функцію approximate_size():

def approximate_size(size, a_kilobyte_is_1024_bytes=True):

Другий аргумент a_kilobyte_is_1024_bytes має типове значення True. Це означає, що аргумент необов’язковий: ви можете викликати функцію без нього, а функція буде поводити себе, ніби їй передали True другим аргументом.

Зверніть увагу на кінець скрипту:

if __name__ == '__main__':
    print(approximate_size(1000000000000, False))  
    print(approximate_size(1000000000000))
  • У другому рядку ми викликаємо функцію з двома аргументами. Всередині функції змінна a_kilobyte_is_1024_bytes матиме значення False, тому що ми задали його свідомо.
  • В останньому рядку approximate_size викликається з одним аргументом. Це нормально, бо другий є необов’язковим і всередині функції матиме значення True.

Також можна передавати значення в функцію за іменем аргументу:

>>> from humansize import approximate_size 
>>> approximate_size(4000, a_kilobyte_is_1024_bytes=False)
'4.0 KB'

Це викликає функцію approximate_size(), передавши 4000 в якості першого аргумента (size) та False для аргумента a_kilobyte_is_1024_bytes. (Який є другим аргументом випадково, але незабаром ви побачите, що не обов'язково дотримуватись порядку аргументів).

>>> approximate_size(size=4000, a_kilobyte_is_1024_bytes=False) 
'4.0 KB'

Це викликає функцію approximate_size(), передавши 4000 аргументу з іменем size та False для аргументу, названого a_kilobyte_is_1024_bytes.

>>> approximate_size(a_kilobyte_is_1024_bytes=False, size=4000) 
'4.0 KB'

Це викликає функцію approximate_size(), передавши False аргументу з іменем a_kilobyte_is_1024_bytes та 4000 для аргумента size. (Бачите? Я ж казав, що порядок не має значення.)

>>> approximate_size(a_kilobyte_is_1024_bytes=False, 4000) 
  File "<stdin>", line 1 
SyntaxError: non-keyword arg after keyword arg

Цей виклик не працює, бо ви передали неіменований (позиційний) аргумент після іменованого, а таке ніколи не спрацьовує. Якщо читати список аргументів зліва направо, то після того як ви натрапили на іменований аргумент, всі наступні також мусять бути іменованими.

>>> approximate_size(size=4000, False) 
  File "<stdin>", line 1 
SyntaxError: non-keyword arg after keyword arg

Цей виклик теж не працює, з тієї ж причини, що й попередній. Це було несподіванкою, правда? Зрештою, ви передали 4000 аргументу з іменем size, тоді "очевидно", що значення False призначалось аргументу a_kilobyte_is_1024_bytes. Але Python так не працює. Як тільки він зустрічає іменований аргумент, він очікує що всі інші після нього теж будуть іменованими.


* * *


Написання читабельного коду[ред.]

Не буду знуджувати вас довгою повчальною промовою про важливість документування свого коду. Просто пам’ятайте, що код пишеться раз, а читається багато разів, і найбільш важливою аудиторією читачів є ви самі шість місяців по тому (після того, як ви вже все забули, але мусите щось виправити). Python спрощує написання читабельного коду, і цим варто скористатись. Ви подякуєте мені через шість місяців.

Рядки документації[ред.]

Функції можна документувати, задаючи їм рядки документації (в народі докстрінги). У цій програмі функція approximate_size() має докстрінг:

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''
Кожна функція варта гідного докстрінгу

Потрійні лапки задають багаторядковий літерал (текстову константу). Усе між лапками, що відкривають, і лапками, які закривають, входить до літерала, включно з переходами на новий рядок, відступами на початку та іншими лапками. Такі "багаторядкові рядки" (а це саме Python-рядки, а не окремий тип!) можна зустріти всюди, та найчастіше саме в докстрінгах.

Потрійні лапки також є простим способом задати рядок, що містить як одинарні, так і подвійні рядки.

Рядок документації, якщо він є, мусить бути першим з того, що описується в функції (тобто йти одразу за оголошенням функції). Формально вам не потрібно задавати докстрінг для функції, проте це завжди бажано. Я знаю, про це казали на всіх уроках програмування, але Python дає додатковий стимул писати документацію: вона доступна під час виконання як атрибут функції.

Багато середовищ розробки використовують рядки документації для надання контекстної підказки, виводячи їх як тільки ви напишете ім’я функції чи затримаєте над ним курсор. Це може бути дуже корисним, але залежить від того, як добре ви напишете документацію.


* * *


Шляхи пошуку для import[ред.]

Доки я не зайшов занадто далеко, розповім ще про шляхи пошуку бібліотек. Коли ви робите спробу імпортувати модуль, Python шукає його в кількох місцях. Якщо конкретно - він дивиться у всіх каталогах, перелічених у списку sys.path. Це стандартний список, і ви можете легко його переглядати та модифікувати, використовуючи звичайні методи для роботи зі списками. (Ви дізнаєтесь про це більше у розділі Стандартні типи даних).

>>> import sys

Імпортування модуля sys відкриває нам доступ до його функцій та інших атрибутів.

>>> sys.path
['',
 '/usr/lib/python31.zip',
 '/usr/lib/python3.1',
 '/usr/lib/python3.1/plat-linux2@EXTRAMACHDEPPATH@',
 '/usr/lib/python3.1/lib-dynload',
 '/usr/lib/python3.1/dist-packages',
 '/usr/local/lib/python3.1/dist-packages']

sys.path - це список шляхів до каталогів, у яких Python шукає файли для імпорту. (Він матиме різний вигляд, залежно від операційної системи, версії інтерпретатора й каталогу, до якого його встановили. Python перевірить ці каталоги (в такому ж порядку) на наявність файла з розширенням *.py, ім’я якого збігається з назвою модуля, який ви хочете імпортувати.

>>> sys
<module 'sys' (built-in)>

Чесно кажучи, я збрехав. Правда набагато складніша, тому що не всі модулі зберігаються як файли *.py. Деякі є вбудованими прямо в Python. Вбудовані модулі поводяться так само, як і звичайні, тільки їх вихідний Python-код недоступний, бо вони не написані мовою Python!

>>> sys.path.insert(0, '/home/mark/diveintopython3/examples')

Ви можете додати новий каталог до шляхів імпорту під час виконання скрипта, і після цього Python при кожній спробі імпорту буде шукати модулі і в цьому каталозі. Ефект буде зберігатись, доки працює інтерпретатор або доки хтось не видалить каталог зі списку.

>>> sys.path 
['/home/mark/diveintopython3/examples',
 '',
 '/usr/lib/python31.zip',
 '/usr/lib/python3.1',
 '/usr/lib/python3.1/plat-linux2@EXTRAMACHDEPPATH@',
 '/usr/lib/python3.1/lib-dynload',
 '/usr/lib/python3.1/dist-packages',
 '/usr/local/lib/python3.1/dist-packages']

Користуючись sys.path.insert(0, new_path), ви додали новий каталог першим до списку sys.path list, і тому Python буде спершу шукати в ньому. Це майже завжди те, що нам потрібно. У випадку конфліктів імен (наприклад, якщо Python поставляється з однією версією якоїсь бібліотеки, а вам потрібна інша), це гарантує, що Python буде використовувати ваші модулі, а не ті, що постачались з системою.

Все — об’єкт[ред.]

Якщо ви раптом пропустили: я нещодавно сказав, що функції в Python мають атрибути і що ці атрибути доступні під час виконання програми. Функція, як і все інше в Python, є об’єктом.

Запустіть інтерактивну оболонку Python і прямуйте за мною:

>>> import humansize

Цей рядок імпортує нашу програму humansize як модуль - шматок коду, який ви можете використовувати або інтерактивно, або з іншої програми Python. Тільки-но ви імпортуєте модуль, у вас з’явиться можливість звертатись до його публічних функцій, класів та атрибутів. Саме таким способом скрипти отримують доступ до функціональності інших модулів. Ви в інтерактивній оболонці також можете цим користуватись. Це справді важлива властивість Пайтона, і ми розглянемо її детальніше впродовж цієї книжки.

>>> print(humansize.approximate_size(4096, True))
4.0 KiB

Якщо ви хочете використовувати функції, описані в імпортованих модулях, ви повинні додавати також ім’я модуля. Не достатньо написати просто approximate_size, потрібно humansize.approximate_size. Якщо ви використовували класи в Java, вам це повинно здатись знайомим.

>>> print(humansize.approximate_size.__doc__)
Convert a file size to human-readable form.

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

Замість того, щоб викликати функцію, як це зазвичай робиться, ми просимо значення одного з її атрибутів, __doc__.

import в Python схожий на require в Perl. Тільки-но ви імпортуєте модуль module в Pyhton, можна звертатись до його функцій за допомогою ідентифікатора module.function, тільки-но ви завантажите модуль в Perl, можна буде використовувати його функції через ідентифікатор module::function.

Що таке об’єкт?[ред.]

Усе в Python - об’єкт, і все може мати атрибути та методи. Всі функції мають вбудованй атрибут __doc__, який повертає докстрінг, описаний в коді функції. Модуль sys є об’єктом, який має (серед багатьох інших) атрибут path. І так далі.

Поки що це не відповідає на більш фундаментальне запитання: що таке об’єкт? Різні мови програмування описують "об’єкт" різними способами. У деяких це означає, що всі об’єкти повинні мати атрибути та методи, в інших - що від всіх об’єктів можна успадковувати функціонал. У Python означення вільніше. Деякі об’єкти не мають ані атрибутів, ані методів, але можуть їх мати. Не від всіх об’єктів можна наслідуватись. Але все є об’єктом в тому сенсі, що все може бути присвоєно змінній чи передано як аргумент у функцію.

Ви могли чути термін "об’єкт першого класу" в інших контекстах. У мові Python функції є об’єктами першого класу. Ви можете передати функцію як аргумент іншій функції. Модулі є об’єктами першого класу. Ви можете передати цілий модуль як аргумент функції. Класи є об’єктами першого класу, і кожен екземпляр класу також є об’єктом першого класу.

Я збираюсь повторити це ще раз, у випадку якщо ви пропустили попередні рази: все в Python є об’єктом. Рядки - об’єкти. Списки - об’єкти. Функції - об’єкти. Класи - об’єкти. Екземпляри класів - об’єкти. Навіть модулі - об’єкти.


* * *


Відступи в коді[ред.]

Функції в Pyton не мають явних операторів begin/end або фігурних дужок для позначення початку й кінця коду функції. Єдиним розділювачем є двокрапка (:) та відступи в коді.

def approximate_size(size, a_kilobyte_is_1024_bytes=True):     # 1
    if size < 0:                                               # 2
        raise ValueError('number must be non-negative')        # 3
                                                               # 4
    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:                          # 5
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)

    raise ValueError('number too large')
  1. Блоки коду описуються їх відступами. Під терміном "блок коду" я маю на увазі функції, умовні оператори, тіла циклів і так далі. Відступ позначає початок блоку, а прибирання відступу - його закінчення. Явні дужки чи ключові слова не потрібні. Це означає, що невидимі символи є значимими і повинні бути однорідними. У прикладі вище код функції розділено на блоки чотирма пробілами. Це не обов’язково мусять бути чотири пробіли, головне, щоб кількість їх постійно була однакова. Перший рядок без відступу позначає кінець функції.
  2. У Python після оператора if іде блок коду. Якщо умова в ньому набуває істинного значення, виконується блок з відступом, інакше виконується блок, що йде після else (якщо такий присутній). Дужки навколо умови в if необов’язкові.
  3. Цей рядок знаходиться всередині блоку if. Оператор raise запустить виключення (типу ValueError), але тільки при умові size < 0.
  4. Це ще не кінець функції. Порожні рядки не враховуються при розбитті на блоки. Вони можуть зробити функцію більш читабельною, але не відіграють роль розділювачів.
  5. Цикл for також позначає початок блоку. Блоки коду можуть містити як завгодно багато рядків, аби тільки вони мали однаковий відступ. Цей цикл for містить три рядки коду. Іншого спеціального синтаксису для багаторядкових блоків немає. Тільки робіть відступи - і живіть далі.

Після початкових протестів і кількох жалюгідних аналогій з Фортраном, ви примиритесь і побачите переваги такого синтаксису. Однією з основних переваг є те, що всі програми мовою Python виглядають подібно, тому що відступи - це вимога мови, а не рекомендація стилю. Це спрощує читання і розуміння чужого коду.

Python використовує перехід на новий рядок для розділення операторів і двокрапку з відступами для відділення блоків коду. C++ та Java використовують крапку з комою та фігурні дужки для відділення блоків коду


* * *


Виняткові ситуації[ред.]

Виняткові ситуації в Python використовуються повсюди. Їх використовує буквально кожен модуль, і сам Python створює їх за найрізноманітніших обставин. Впродовж даної книжки ви їх чимало зустрінете.

Що таке виняткова ситуація, або виняток? Зазвичай це помилка, сигнал про те, що щось пішло не так, як планувалось. (Чесно кажучи, не всі винятки є помилками, але поки що не забивайте собі цим голови.) Деякі мови програмування заохочують повертати коди помилок, які потім потрібно перевіряти. Python заохочує створення виключень, які ви можете врахувати і обробити.

Коли помилка відбувається в командній оболонці Python, інтепретатор друкує певні подробиці про отриману виняткову ситуацію і як вона сталась. Таку ситуацію називають необробленою (unhandled). Коли створюється виключення і немає коду, який би явно зауважив його та розібрався, то воно спливає нагору, аж до рівня оболонки Python, яка випльовує деяку зневаджувальну інформацію і припиняє виконувати дану йому команду. В командній оболонці Python це не страшно, але коли таке відбудеться у вашому скрипті, програма припинить свою роботу, якщо виняткова ситуація не буде оброблена. Можливо це саме те, що вам потрібно, але можливо й ні.

На відміну від Java, функції в Python не декларують, які виняткові ситуації вони можуть створити. Це повністю ваша турбота - визначити, які ситуації вам доведеться відловлювати.

Виняткова ситуація не обов’язково мусить спричиняти зупинку програми. Вона може бути обробленою. Іноді така ситуація виникає через написання свого коду з помилками (наприклад, намагаєтесь отримати значення змінної, що не існує), але трапляються також ситуації, котрі можна передбачити. Якщо ви відкриваєте файл, його може не існувати. Якщо ви імпортуєте модуль, він може бути не встановленим. Якщо ви під’єднуєтесь до бази даних, вона може бути недоступною або ваші дані прав доступу можуть бути неправильними. Якщо ви знаєте рядок коду, що може створити виняткову ситуацію, то можете обробити останню використовуючи блок try ... except.

Python використовує блоки try...except для обробки виняткових ситуацій і оператор raise для їх генерації. Java та C++ використовують блоки try...catch для обробки та оператор throw для генерації.

Функція approximate_size() генерує виняток в двох різних випадках: якщо переданий розмір більший, ніж може бути оброблений фунцією, або якщо він від’ємний.

if size < 0:
    raise ValueError('number must be non-negative')

Синтаксис створення винятку досить простий. Використайте команду raise, за якою напишіть назву виключної ситуації і необов’язковий рядок з поясненням того, що сталось. Цей синтаксис нагадує виклик функції. (Насправді, механізм виключних ситуацій реалізовано як набір класів, і цей оператор raise є фактично створенням екземпляра класу ValueError і передачі рядка 'number must be non-negative' його конструктору. Але це ми забігаємо наперед!)

Вам не потрібно обробляти винятки функції, що їх створила. Якщо функція їх не обробляє, винятки передаються функції, яка її викликала, потім функції, що викликала ту функцію, і так далі вгору по стеку. Якщо виняток ніде не обробляється, то програма аварійно завершиться, а Python надрукує "слід" до стандартного потоку виводу помилок і на цьому кінець. Знову ж таки, можливо - це саме те, що вам потрібно. Все залежить від того, що робить ваша програма.

Обробка помилок імпорту[ред.]

Один з вбудованих в Python типів виключень - ImportError, яке генерується, коли ви намагаєтесь імпортувати модуль, і ця спроба виявляється невдалою. Це може трапитись через низку причин, але найпоширеніша - модуль не знайдений в шляхах для пошуку модуля. Можна використати цей виняток для додавання необов’язкових функцій до своєї програми. Наприклад, бібліотека chardet дозволяє автоматично визначати кодування символів. Можливо, ваша програма захоче використовувати цю бібліотеку якщо вона існує, але виховано не перериватиме роботи, навіть якщо користувач її не встановив. Це можна зробити за допомогою блока try .. except.

try:
    import chardet
except ImportError:
    chardet = None

Пізніше можна перевірити наявність модуля chardet простим умовним оператором:

if chardet:
    # do something
else:
    # continue anyway

Іншим типовим використанням виняткових ситуацій ImportError є випадок, коли дві бібліотеки реалізують однакове API, але використання однієї з них більш бажане (наприклад, вона швидша, або використовує менше пам’яті). Ви можете спробувати імпортувати перший модуль, і якщо не вийде, завантажити інший. Наприклад, розділ про XML розповідає про два модулі, що реалізують спільне API, наване ElementTree. Перший lxml - сторонній модуль, який потрібно скачувати і встановлювати самостійно. Інший xml.etree.ElementTree - повільніший, але є частиною стандартної бібліотеки Python 3.

try:
    from lxml import etree
except ImportError:
    import xml.etree.ElementTree as etree

Після виконання цього блоку try..except ви імпортували якийсь модуль і назвали його etree. Оскільки обидва модулі реалізують однакове API, решта вашого коду не повинна перевіряти, який модуль був імпортованим. І оскільки модуль, що імпортується, завжди називається etree, решта коду не буде перемежовуватись умовними операторами для виклику по-різному названих модулів.


* * *


Незв’язані змінні[ред.]

Погляньмо ще раз на рядок коду з функції approximate_size():

multiple = 1024 if a_kilobyte_is_1024_bytes else 1000

Ви ніде не оголошували змінну multiple, ви просто присвоюєте їй значення. Це нормально, бо Python дозволяє вам так роботи. Що Python вам ніколи не дозволить - це звертатись до змінної, якій ще не було присвоєно значення. Така спроба спровокує виняткову ситуацію NameError.

>>> x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>> x = 1
>>> x
1

Якось ви подякуєте Python-у за це.


* * *


Усе чутливе до регістру[ред.]

Усі імена в Python чутливі до регістру: імена змінних, імена функцій, імена класів, імена модулів, імена виняткових ситуацій. Якщо ви можете отримати певне значення, встановити його, викликати або імпортувати - отже, воно чутливе до регістру.

>>> an_integer = 1
>>> an_integer
1
>>> AN_INTEGER
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'AN_INTEGER' is not defined
>>> An_Integer
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'An_Integer' is not defined
>>> an_inteGer
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'an_inteGer' is not defined

І так далі...


* * *


Запуск скриптів[ред.]

Усе в Python - об’єкти

Модулі в Python є об’єктами і мають кілька корисних атрибутів. Один з таких корисних атрибутів можна використати для тестування модуля. За його допомогою до модуля можна додати спеціальний блок, що буде виконуватись лише при запуску інтерпретатора для самого модуля, а не для скрипта, що його імпортує. Подивіться на кілька останніх рядків humansize.py:

if __name__ == '__main__':
    print(approximate_size(1000000000000, False))
    print(approximate_size(1000000000000))

Як і C, Python використовує == для порівняння та = для присвоєння. На відміну від C, Python не дозволяє присвоєння в виразах, тому не має можливості випадково надати змінній значення при порівнянні.

То що ж робить цей if особливим? Ну, модулі - це об’єкти, і всі модулі мають вбудований атрибут __name__. Він залежить від того, як ви використовуєте модуль. Якщо ви імпортуєте його, тоді __name__ - це назва файлу модуля без розширення файлу та шляху до нього.

>>> import humansize
>>> humansize.__name__
'humansize'

Але ви також можете запускати модуль напряму як окрему програму, і в такому випадку __name__ прийме спеціальне стандартне значення: "__main__". Python обчислить умову в if, вона виявиться істинною, і вкладений блок виконається. У нашому випадку він надрукує два значення.

c:\home\diveintopython3> c:\python31\python.exe humansize.py
1.0 TB
931.3 GiB

І це й є наша перша програма!


* * *


Для подальшого читання[ред.]

Стандартні типи даних[ред.]

В основі будь-якої філософії лежить здивування, її розвитком є дослідження, її кінцем – незнання.
Мішель де Монтень


Типи даних. Давайте на хвилинку відкладемо набік вашу першу програму, і поговоримо про типи даних. В Python кожне значення має тип, але ви не повинні оголошувати тип змінної. Як це працює? Покладаючись на початково присвоєне змінній значення, Python визначає якому типу воно належить і запам’ятовує його самостійно.

Python має багато вбудованих типів даних. Ось деякі найважливіші:

  1. Логічні (Булеві) змінні приймають значеня True або False.
  2. Числа можуть бути цілими (1 і 2), з десятковими дробами (1.1 і 1.2), звичайними дробами (1/2 and 2/3), чи навіть комплексними.
  3. Рядки є послідовностями символів Юнікоду (наприклад HTML документ)
  4. Байти та масиви байтів (наприклад зображення в форматі JPEG)
  5. Списки є впорядкованими послідовностями значень.
  6. Кортежі є впорядкованими незмінними послідовностями значень.
  7. Множини є невпорядкованими наборами значень.
  8. Словники є невпорядкованими наборами пар ключ-значення.

Звичайно існує ще багато типів крім вищеперелічених. В Python все - об’єкт, тому є такі типи як модуль, функція, клас, метод, файл і навіть відкомпільований код. Ви вже зустрічались з деякими з них: модулі мали імена, функції мали докстрінґи. Ви дізнаєтесь про класи в розділі Класи та ітератори, і про файли в розділі Файли.

Рядки і байти достатньо важливі і достатньо складні щоб присвятити їм окремий розділ. Давайте спершу розглянемо інших.


* * *


Булевий тип[ред.]

У булевому контексті можна використовувати фактично будь-які вирази

Булеві змінні приймають лише істинне чи хибне значення. Python має для цих значень дві відповідні константи: True та False, які можна використовувати для присвоєння значень змінним. Окрім констант, булеві значення можуть приймати вирази. А в деяких місцях (наприклад, в операторі if) Python навіть очікує того, що значення виразу можна буде привести до булевого типу. Такі місця називаються булевими контекстами. Ви можете використати майже будь-який вираз в булевому контексті, і Python намагатиметься визначити його істинність. Різні типи даних мають різні правила щодо того, які значення є істинними, а які - хибними в булевому контексті. (Сенс останніх речень стане зрозумілішим, коли ви побачите деякі приклади далі в цьому розділі.)

Візьмімо для прикладу цей шматочок коду з humansize.py:

if size < 0:
    raise ValueError('number must be non-negative')

size - ціле число, 0 теж ціле, а < - оператор над числами. Результат виразу size < 0 завжди булевий. Ви можете переконатись в цьому самостійно в інтерактивній оболонці Python:

>>> size = 1
>>> size < 0
False
>>> size = 0
>>> size < 0
False
>>> size = -1
>>> size < 0
True

Від Python 2 була успадкована цікава особливість: за потреби вважати булеві значення числами. True це 1, а False - 0:

>>> True + True
2
>>> True - False
1
>>> True * False
0
>>> True / False
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: int division or modulo by zero

Ей, ей, ей! Ніколи не робіть так. Забудьте навіть, що я вам про таке розповідав. Серйозно.

Числа[ред.]

Числа класні. Їх так багато, що кожен зможе вибрати собі до вподоби скільки схоче. Python підтримує як цілі, так і дробові числа. Вказувати тип числа явно не потрібно, Python сам визначить його за наявністю чи відсутністю десяткової крапки.

>>> type(1) 
<class 'int'>

Функцію type() можна використовувати, щоб визначати тип значення змінної. Як можна було очікувати, 1 - ціле (int).

>>> isinstance(1, int)
True

Для перевірки приналежності типу можна використовувати функцію isinstance().

>>> 1 + 1
2

Додавання двох цілих дає нам цілий результат.

>>> 1 + 1.0
2.0
>>> type(2.0)
<class 'float'>

Результатом додавання цілого та дробового числа є дробове. Для виконання додавання Python приводить ціле число до типу float і повертає результат цього ж типу.

Перетворення цілих в дробові та навпаки[ред.]

Щойно ви бачили, що деякі оператори (такі, як додавання) за потреби перетворюють цілі числа в дробові. Ви можете зробити це саме самостійно.

>>> float(2)
2.0

Можна явно перетворювати int у float, використовуючи функцію float()

>>> int(2.0) 
2

Нічого дивного і в тому, що ви можете зробити навпаки за допомогою функції int

>>> int(2.5) 
2

Функція int просто відкидає дробову частину, а не округлює.

>>> int(-2.5) 
-2

Функція int для від’ємних повертає найменше ціле число, більше або рівне даному (тобто теж просто відкидає дробову частину). Тому не плутайте її з функцією math.floor.

>>> 1.12345678901234567890
1.1234567890123457

Десяткові дроби мають точність до 15 знаків після коми.

>>> type(1000000000000000)
<class 'int'>

Цілі можуть бути як завгодно великими.

Python 2 має окремі типи int і long для цілих і довгої арифметики. Тип int був обмежений значенням sys.maxint, яке залежало від платформи (зазвичай ). Python 3 має лише один цілий тип, який загалом поводиться так, як тип long в Python 2. Див. PEP 237 для деталей.

Основні числові операції[ред.]

З числами можна робити все:

>>> 11 / 2      
5.5

Оператор / виконує ділення чисел з плаваючою крапкою. Він повертає float, навіть якщо чисельник та знаменник цілі.

>>> 11 // 2     
5

Оператор // виконує хитрий вид цілочисельного ділення. Коли результат додатній, ви можете вважати його відкиданням (не округленням) дробової частини звичайного ділення, але будьте обережними з цим.

>>> 11 // 2    
6

При цілочисельному діленні від’ємних чисел оператор // округлює "вгору" до найближчого цілого. Хоча, говорячи формально, він округлує вниз, тому що -6 менше за -5.

>>> 11.0 // 2   
5.0

Оператор // не завжди повертає цілі. Якщо чисельник чи знаменник дробові, // повертає float, яке, щоправда, все одно округлене до найближчого цілого.

>>> 11 ** 2     
121

Оператор ** означає піднесення до степеня.

>>> 11 % 2      
1

Оператор % повертає остачу від цілочисельного ділення.

У Python 2 оператор / зазвичай означав цілочисельне ділення (якщо застосовувався до цілих), але ви могли просто змусити його поводитись як дробове ділення, включаючи спеціальну директиву у свій код:

>>> from __future__ import division
>>> 1/2
У Python 3 оператор / завжди означає ділення чисел з плаваючою комою. Див. PEP 238 для деталей.


Звичайні дроби[ред.]

Python не обмежується тільки цілими числами та десятковими дробами. Він також знає арифметику зі звичайними дробами, яку ви вивчили і одразу забули ще в школі.

>>> import fractions

Щоб почати використовувати звичайні дроби, потрібно імпортувати модуль fractions.

>>> x = fractions.Fraction(1, 3) 
>>> x
Fraction(1, 3)

Щоб створити дріб, використовують об’єкт типу Fraсtion і передають його конструктору чисельник та знаменник.

>>> x * 2
Fraction(2, 3)

З дробами можна виконувати звичайні математичні дії, результатом яких буде новий об’єкт Fraсtion.

>>> fractions.Fraction(6, 4) 
Fraction(3, 2)

Об’єкт Fraсtion автоматично скорочує дроби.

>>> fractions.Fraction(0, 0)
Traceback (most recent call last): 
  File "<stdin>", line 1, in <module> 
  File "fractions.py", line 96, in __new__ 
    raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
ZeroDivisionError: Fraction(0, 0)

Python не дозволить створити дріб з нульовим знаменником.

Тригонометрія[ред.]

В Python також доступні тригонометричні функції.

>>> import math 
>>> math.pi 
3.1415926535897931

Модуль math має константу для - відношення довжини кола до його діаметру.

>>> math.sin(math.pi / 2)
1.0

math містить всі базові тригонометричні функції: sin(), cos(), tan() і навіть такі, як asin().

>>> math.tan(math.pi / 4)
0.99999999999999989

Щоправда, варто зауважити, що Python не дає абсолютної точності. tan(π / 4) мусить повертати 1.0, а не 0.99999999999999989.

Числа в булевому контексті[ред.]

Нульові значення вважаються хибними, ненульові - істинними

Ви можете використовувати числа в булевому контексті, такому, як умова в операторі if. Нульові значення вважаються хибними, ненульові - істинними.

>>> def is_it_true(anything): 
...   if anything: 
...     print("yes, it's true")
...   else: 
...     print("no, it's false")
...

Ви знали, що можна описувати свої функції прямо в інтерактивній сесії Python? Просто натискайте Enter в кінці кожного рядка і додайте один порожній рядок в кінці функції.

>>> is_it_true(1)
 yes, it's true 
>>> is_it_true(-1)
 yes, it's true 
>>> is_it_true(0)
 no, it's false

У булевому контексті всі цілі, окрім нуля, дорівнюють True, 0 - False.

>>> is_it_true(0.1)
 yes, it's true 
>>> is_it_true(0.0)
 no, it's false

Ненульові десяткові дроби - True, 0.0 - False. Будьте обережними з цим! Найменша похибка округлення (цілком ймовірна річ, як ви могли побачити в попередньому розділі) - і Python буде перевіряти 0.0000000000001 замість 0.0 та отримає значення True.

>>> import fractions 
>>> is_it_true(fractions.Fraction(1, 2))
 yes, it's true 
>>> is_it_true(fractions.Fraction(0, 1))
 no, it's false

Звичайні дроби теж можна використати в булевому контексті. Fraction(0, n) - хибне для будь-якого n. Усі інші дроби істинні.


* * *


Списки[ред.]

Списки - це робоча кобилка мови Python. Коли я кажу "список", ви можете подумати "масив, розмір якого я повинен задати наперед і який мусить містити елементи одного типу і т.д.". Не думайте так. Списки набагато крутіші за масиви.

Список у Python - це як масив у Perl 5. У Perl 5 змінні, які містять масиви, завжди починаються з символу @. У Python змінні можна називати як завгодно, а Python сам визначить тип.

Список у Python - це набагато більше, ніж масив в Java (хоча й може використовуватись як такий, якщо це все, що вам потрібно від життя). Кращим аналогом міг би бути клас ArrayList, який може містити довільні об’єкти і динамічно розширюватись при додаванні нових елементів.

Створення списку[ред.]

Створити список дуже просто - помістіть список значень, розділених комою, в квадратні дужки:

>>> a_list = ['a', 'b', 'mpilgrim', 'z', 'example'] 
>>> a_list
['a', 'b', 'mpilgrim', 'z', 'example']

Створюємо список з п’яти елементів. Їхній порядок зберігається, що не є випадковим, бо список - впорядкований набір елементів.

>>> a_list[0]
'a'

Нумерація елементів списку починається з нуля. Першим елементом непорожнього списку a_list є a_list[0].

>>> a_list[4]
'example'

Останнім елементом списку a_list з п’яти елементів є a_list[4]. Тому що нумерація починається з нуля.

>>> a_list[-1]
'example'

Від’ємні індекси в масиві задають відповідні по порядку елементи, якщо рахувати справа наліво. Останнім елементом непорожнього списку a_list завжди є a_list[-1].

>>> a_list[-3]
'mpilgrim'

Якщо від’ємні індекси для вас трохи заплутані, думайте про них так: a_list[-n] == a_list[len(a_list) - n]. Тому в нашому випадку: a_list[-3] == a_list[5 - 3] == a_list[2].

Зрізання списків[ред.]

a_list[0] - перший елемент a_list.

Щойно створивши список, ви можете отримати будь-яку його частину як новий список. Ця частина називатиметься зрізом.

>>> a_list
['a', 'b', 'mpilgrim', 'z', 'example'] 
>>> a_list[1:3]
['b', 'mpilgrim']

Ви можете отримати зріз списку, задавши два індекси. Отримане значення - новий список, який містить всі елементи списку по порядку, починаючи від першого індексу (тут a_list[1]) і аж до другого (тут a_list[3]), але не включаючи його.

>>> a_list[1:-1]
['b', 'mpilgrim', 'z']

Зріз утворюється і тоді, коли один чи обидва з індексів від’ємні. Якщо це допоможе, можете думати так: ми читаємо список зліва направо. Першим індексом задається перший елемент з тих, які нам потрібні. Елемент, що задається другим індексом, - перший з тих, які нам не потрібні. Повертаються лише ті значення, які потрібні.


>>> a_list[0:3]
['a', 'b', 'mpilgrim']

Нумерація елементів в списку починається з нуля, тому a_list[0:3] поверне перші три елементи списку, починаючи з a_list[0] аж до a_list[3] невключно.

>>> a_list[:3]
['a', 'b', 'mpilgrim']

Якщо лівий індекс зрізу дорівнює нулю, його можна не вказувати. Тому a_list[:3] - це те саме, що й a_list[0:3].

>>> a_list[3:]
['z', 'example']

Аналогічно, якщо правий індекс - довжина списку, його теж можна не вказувати. Тому a_list[3:] - такий самий, як a_list[3:5], бо цей список має п’ять елементів. Тут є приємна симетрія. В даному п’ятиелементному списку a_list[:3] повертає перші три елементи, а a_list[3:] - останні два. Взагалі, a_list[:n] поверне перших n елементів, а a_list[n:] - решту, незалежно від довжини списку. (Якщо n - більше чи рівне довжини списку, всі елементи попадуть в перший зріз, а решта відповідно буде порожньою.)

>>> a_list[:]
['a', 'b', 'mpilgrim', 'z', 'example']

Якщо пропустити обидва індекси, всі елементи списку будуть включені до зрізу. Але це те саме, що й оригінальна змінна a_list. Це новий список, який містить ті самі елементи, що й оригінал. a_list[:] - скорочення для створення копії списку.

Додавання елементів до списку[ред.]

Існує чотири способи додавання елементів в список.

>>> a_list = ['a'] 
>>> a_list = a_list + [2.0, 3]

Оператор + конкатенує два списки, щоб утворити новий. Список може містити довільну кількість елементів (обмежень на розмір списку немає, окрім, звісно, об’єму доступної пам’яті). Проте ви мусите пам’ятати, що конкатенація списків створює додатковий список в пам’яті. В даному випадку він негайно присвоюється існуючій змінній a_list. Тому цей рядок коду насправді працює в два етапи: створення нового списку, і потім присвоєння. Якщо список довгий, то в проміжку між етапами програма може потребувати досить великі об’єми пам’яті.

>>> a_list 
['a', 2.0, 3]

Список може містити елементи будь-яких типів, і елементи в списку не мусять бути одного типу. Тут ми маємо список, що складається з рядка, числа з плаваючою крапкою і цілого числа.

>>> a_list.append(True) 
>>> a_list 
['a', 2.0, 3, True]

Метод append() додає один елемент до кінця списку. (Тепер в списку вже чотири різні типи даних!)

>>> a_list.extend(['four', 'Ω']) 
>>> a_list 
['a', 2.0, 3, True, 'four', 'Ω']

Списки реалізовані як класи. Створення списку - це насправді ініціалізація екземпляру класу. І, так само як й інші класи, списки мають методи, щоб оперувати на них. Метод extend() приймає один аргумент - список, і додає всі елементи цього списку до початкового.

>>> a_list.insert(0, 'Ω') 
>>> a_list 
['Ω', 'a', 2.0, 3, True, 'four', 'Ω']

Метод insert() вставляє один елемент в список. Його перший аргумент - індекс першого аргументу в списку, який буде зсунутий зі своєї позиції. Елементи списку не мусять бути унікальними; наприклад, зараз список містить два різні елементи зі значенням 'Ω': перший елемент (a_list[0]) і останній елемент (a_list[6]).

Метод a_list.insert(0, value) схожий на функцію unshift() в Perl. Він додає елемент на початок списку, і всі інші елементи змінюють свій індекс, щоб звільнити місце.

Давайте детальніше розглянемо різницю між append() та extend().

>>> a_list = ['a', 'b', 'c'] 
>>> a_list.extend(['d', 'e', 'f']) 
>>> a_list 
['a', 'b', 'c', 'd', 'e', 'f']

Метод extend() отримує один аргумент, який завжди мусить бути списком, і додає кожен з елементів аргументу в a_list.

>>> len(a_list) 
6 
>>> a_list[-1]
'f'

Якщо для списку з трьох елементів викликати метод extend(), якому теж передати список з трьох елементів, то в результаті отримаємо шестиелементний список.

>>> a_list.append(['g', 'h', 'i']) 
>>> a_list
['a', 'b', 'c', 'd', 'e', 'f', ['g', 'h', 'i']]

З іншого боку, append() теж отримує єдиний аргумент, який щоправда може бути будь-якого типу. Тут ми викликаємо append() теж для списку з трьох елементів.

>>> len(a_list) 
7 
>>> a_list[-1] 
['g', 'h', 'i']

Якщо для списку з шести елементів викликати метод append() і передати йому список, ми отримаємо список з семи елементів. Чому семи? Тому що останній елемент, незважаючи на те, що є списком, все одно буде одним елементом. Списки можуть містити будь-які дані, в тому числі й інші списки.

Пошук значень в списку[ред.]

>>> a_list = ['a', 'b', 'new', 'mpilgrim', 'new'] 
>>> a_list.count('new') 
2

Як і можна було очікувати, метод count() (підрахуй) повертає кількість входжень певного елемента в список.

>>> 'new' in a_list 
True 
>>> 'c' in a_list 
False

А якщо вам лише потрібно знати чи є якийсь елемент в списку, чи його немає, оператор in буде дещо швидшим. Оператор in завжди повертає True або False, він не рахує скільки разів елемент входить в список.

>>> a_list.index('mpilgrim') 
3

Ні оператор in, ні метод count() не говорять вам, де саме в списку перебуває елемент. Якщо вам потрібна така інформація - зверніться до методу index(). За замовчуваням він шукає по всьому списку, хоча ви можете передати необов’язковий другий елемент - індекс, з якого починати пошук, і навіть необов’язковий третій аргумент - індекс, на якому закінчувати пошук.

>>> a_list.index('new') 
2

Метод index() знаходить місце першого входження елемента в список. У даному випадку, 'new' двічі зустрічається в списку, в елементах a_list[2] та a_list[4], але index() повертає лише позицію першого входження.

>>> a_list.index('c') 
Traceback (innermost last):
 File "<interactive input>", line 1, in ? 
ValueError: list.index(x): x not in list

Ви могли не очікувати такого, але якщо значення не знаходиться в списку, метод index() згенерує виняток.

Стривайте, що? Все правильно: якщо index() не знаходить значення в списку, він генерує виняток. Це значно відрізняється від поведінки аналогічних методів ув інших мовах, які б повертали якийсь недійсний індекс, наприклад -1. І хоча це спершу здається незручним, думаю, з часом ви це оціните. Це означає, що у вашій програмі відбудеться збій прямо в місці виникнення проблеми, а не триватиме робота з неправильними даними, що в майбутньому призведе до дивних незрозумілих помилок. Пам’ятаєте --1 - це допустимий індекс. Якщо метод index() поверне -1, це призведе до не надто веселих сеансів зневадження.


Видалення елементів зі списку[ред.]

Списки ніколи не містять порожнин

Списки можуть розширюватись та скорочуватись автоматично. Ви вже бачити частину з доповненням списку. Так сам є кілька різних способів вилучення елементів зі списку.

>>> a_list = ['a', 'b', 'new', 'mpilgrim', 'new'] 
>>> a_list[1] 
'b' 
>>> del a_list[1] 
>>> a_list ['a', 'new', 'mpilgrim', 'new']

Для того, щоб видалити конкретний елемент зі списку, можна використати інструкцію del.

>>> a_list[1]
'new'

Спроба отримати елемент з індексом 1 після видалення елемента з таким індексом не спричинює помилку. Всі елементи, які перебували в списку після видаленого, зсувають свій індекс, щоб "заповнити порожнину", яка виникла в результаті видалення елемента.

Не знаєте індекс елемента? Не проблема, їх можна видаляти знаючи значення.

>>> a_list.remove('new') 
>>> a_list
['a', 'mpilgrim', 'new']

Елементи можна також видаляти зі списку за допомогою методу remove(). Метод remove() отримує значення і видаляє перше входження елемента з таким значенням зі списку. Знову ж таки, всі елементи після видаленого зсунуться, щоб заповнити порожнину. Списки ніколи не містять порожнин.

>>> a_list.remove('new') 
>>> a_list
['a', 'mpilgrim'] 
>>> a_list.remove('new')
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list

Метод remove() можна викликати так часто, як вам захочеться, проте він буде генерувати винятки при спробах видалити елемент, відсутній у списку.

Видалення елементів зі списку: бонусний раунд[ред.]

Іншим цікавим методом списку є pop(). Метод pop() - це просто ще один спосіб видалити елемент зі списку, але з викрутасом.

>>> a_list = ['a', 'b', 'new', 'mpilgrim'] 
>>> a_list.pop()
'mpilgrim' 
>>> a_list
['a', 'b', 'new']

При виклику без аргументів pop() видаляє останній елемент зі списку і повертає його значення.

>>> a_list.pop(1)
'b' 
>>> a_list
['a', 'new'] 
>>> a_list.pop()
'new' 
>>> a_list.pop()
'a'

Ви можете витягувати зі списку довільні елементи. Просто передавайте в pop() їхній індекс, і він самостійно видалить їх, зсуне індекси всіх інших і поверне нам значення.

>>> a_list.pop()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
IndexError: pop from empty list

Виклик pop() для порожнього списку генерує виняток.

Виклик методу pop() без аргументів подібний функції pop() в Perl. Він видаляє останній елемент зі списку і повертає значення видаленого елементу. Perl містить іншу функцію shift(), яка видаляє перший елемент і повертає його значення. В Python це еківалентно виклику pop(0).

Списки в булевому контексті[ред.]

Порожні списки хибні в булевому контексті; всі інші списки істинні.

Списки також можна використовувати в булевих контекстах таких, як, наприклад умова в if.

>>> def is_it_true(anything):
...   if anything:
...     print("yes, it's true")
...   else:
...     print("no, it's false")
... 
>>> is_it_true([])
no, it's false

У булевому контексті будь-який порожній список хибний.

>>> is_it_true(['a'])
yes, it's true

Будь-який список, який містить хоча б один елемент, істинний у булевому контексті.

>>> is_it_true([False])
yes, it's true

Незалежно від значення елементів, які він містить, непорожній список - істинний.


* * *


Кортежі[ред.]

Кортеж - це незмінюваний список. Він завжди залишається таким, яким його створили.

>>> a_tuple = ("a", "b", "mpilgrim", "z", "example") 
>>> a_tuple
('a', 'b', 'mpilgrim', 'z', 'example')

Кортеж записується майже так само, як список, але замість квадратних дужок використовуються круглі.

>>> a_tuple[0]
'a'

Елементи кортежу мають визначений порядок, так само, як і в списку. Так само індексація починається з нуля.

>>> a_tuple[-1]
'example'

Від’ємні індекси відраховуються від кінця кортежу, так само, як і в списках.

>>> a_tuple[1:3]
('b', 'mpilgrim')

Зрізи теж працюють. Коли ви робите зріз списку - отримуєте новий список, робите зріз кортежу - отримуєте новий кортеж.

Основна відмінність між списками та кортежами: кортежі не можна змінювати. Технічно - вони незмінні. Практично - вони не мають методів, які б дозволили вам змінити їх. Списки містять такі методи: append(), extend(), insert(), remove() і pop(). Кортежі не містять таких методів. Ми можемо зробити зріз кортежу (тому що це створює новий кортеж) і перевірити, чи кортеж містить певне значення (тому що це не змінює кортеж), і ... на цьому все.

>>> a_tuple
('a', 'b', 'mpilgrim', 'z', 'example')
>>> a_tuple.append("new")               
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'append'

Доповнювати: не можна.

>>> a_tuple.remove("z")                 
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
AttributeError: 'tuple' object has no attribute 'remove'

Вилучати: не можна.

>>> a_tuple.index("example")            
4

Можна шукати елементи, бо це не змінює кортеж.

>>> "z" in a_tuple                      
True

Ну і можна перевіряти, чи елемент міститься в кортежі.

Тоді навіщо вони потрібні?

  • Кортежі працюють швидше, ніж списки. Якщо вам потрібен незмінний набір значень і все що ви будете робити - ітерувати по ньому, то використовуйте кортеж замість списку.
  • Ваш код стане безпечнішим, якщо ви захистите від запису дані, які не мусять змінюватись.
  • Деякі кортежі можуть використовуватись як ключі в словнику (кажучи конкретно - кортежі, які містять лише незмінні елементи, тобто числа, рядки чи інші кортежі). Списки ніколи не можна використовувати як ключі словника, тому що списки можуть змінюватись.

Кортежі можна перетворювати в списки, і навпаки. Вбудована функція tuple() отримує список і повертає кортеж з такими ж елементами, а функція list() отримує кортеж і повертає список. tuple() ніби "заморожує" список, а list() "розморожує" кортеж.

Кортежі в булевому контексті[ред.]

>>> def is_it_true(anything):
...   if anything:
...     print("yes, it's true")
...   else:
...     print("no, it's false")
... 
>>> is_it_true(())
no, it's false

У булевому контексті порожній кортеж еквівалентний False.

>>> is_it_true(('a', 'b'))
yes, it's true

Будь-який кортеж, що містить хоча б один елемент, - істинний.

>>> is_it_true((False,))
yes, it's true

Будь-який непорожній кортеж - істинний, незалежно від значень його елементів. Але ви помітили кому?

>>> type((False))
<class 'bool'> 
>>> type((False,))
<class 'tuple'>

Щоб створити кортеж з одного елемента, ви мусите написати після нього кому. Якщо коми немає, Python думає, що ви просто взяли вираз в дужки, що не матиме жодного ефекту.

Присвоєння кількох значень за раз[ред.]

Кортежі дозволяють робити класний трюк: присвоювати по кілька значень за раз:

>>> v = ('a', 2, True) 
>>> (x, y, z) = v 
>>> x
'a' 
>>> y
2 
>>> z
True

v - кортеж з трьох елементів, а (x, y, z) - кортеж з трьох змінних. Присвоєння одного іншому присвоює кожній змінній кортежу значення з v.

Це можна використовувати дуже різноманітно. Припустимо, ви хочете присвоїти імена наборові змінних. Ви можете використати вбудовану функцію range() разом з присвоєнням і кортежем змінних, щоб швидко роздати їм відповідні значення.

>>> (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)

Вбудована функція range() створює послідовність цілих чисел. (Правду кажучи, вона повертає ітератор, а не список чи кортеж, але ми розглянемо це детальніше потім.) MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY - змінні, які ми оголошуємо. (Цей приклад взятий з модуля calendar, маленького цікавого модуля, який друкує календарі, аналогічно програмі cal в Unix. Цей модуль містить константи для всіх днів тижня.)

>>> MONDAY
0 
>>> TUESDAY
1 
>>> SUNDAY
6

Тепер кожна змінна має своє значення: MONDAY = 0, TUESDAY = 1 і так далі.

Також можна використовувати присвоєння багатьох значень для створення функцій, які повертають кілька значень, повертаючи кортеж з ними. Той, хто викликав функцію, може зберігати результат як єдиний кортеж, а може присвоїти його значення окремим змінним. Багато стандартних бібліотек мови Python містять такі функції. Однією з них є модуль os, про який ви дізнаєтесь в наступному розділі.


* * *


Множини[ред.]

Множини - це "сумки" з невпорядкованими унікальними значеннями. Множина може містити значення будь-якого незмінюваного типу. Якщо маєте дві множини, то можете здійснювати над ними звичні операції над множинами: об’єднання, перетин і різницю.

Створення множини[ред.]

Почнемо за порядком. Створити множину просто.

>>> a_set = {1} 
>>> a_set
{1}

Щоб створити множину з одного елемента, помістіть цей елемент у фігурні дужки.

>>> type(a_set)
<class 'set'>

Множини, як і всі інші типи в Python, реалізовані як класи. Але про це пізніше.

>>> a_set = {1, 2} 
>>> a_set
{1, 2}

Щоб створити множину з кількох елементів, перелічіть їх у фігурних дужках через кому.

Множину також можна створити зі списку.

>>> a_list = ['a', 'b', 'mpilgrim', True, False, 42] 
>>> a_set = set(a_list)

Щоб створити множину зі списку, використайте функцію set(). (Педанти, які знають про те, як реалізовані множини, вкажуть, що це насправді не виклик функції, а створення екземпляру класу. Я обіцяю, що далі в цій книжці ви дізнаєтесь про різницю. А поки що просто знайте, що set() поводить себе як функція, що повертає множину.)

>>> a_set
{'a', False, 'b', True, 'mpilgrim', 42}

Як я раніше згадував, множини можуть містити значення будь-якого типу. І як я казав, вони невпорядковані. Ця множина не зберігає початковий порядок елементів списку, з якого ви її створили. І якщо ви додасте до неї ще елементи, вона не буде пам’ятати, в якому порядку ви їх додали.

>>> a_list
['a', 'b', 'mpilgrim', True, False, 42]

Початковий список не змінюється.

Не маєте ніяких значень взагалі? Не проблема! Можна створити порожню множину.

>>> a_set = set()

Щоб створити порожню множину, викличте set() без параметрів.

>>> a_set
set()

Текстове представлення порожньої множини виглядає дещо дивно. Ви ж напевне очікували побачити {}? Це б означало порожній словник, а не порожню множину. Про словники ми дізнаємось трохи далі в цьому ж розділі.

>>> type(a_set)
<class 'set'>

Незважаючи на те, що вона дивно виглядає, порожня множина - це все одно множина.

>>> len(a_set)
0

Яка не містить елементів.

>>> not_sure = {} 
>>> type(not_sure)
<class 'dict'>

З історичних причин дві фігурні дужки позначають не порожню множину, а порожній словник.

Зміна множини[ред.]

Існує два способи додавати елементи до множини: метод add() і метод set().

>>> a_set = {1, 2} 
>>> a_set.add(4) 
>>> a_set
{1, 2, 4}

Метод add() приймає один аргумент будь-якого незмінного типу і додає його в множину.

>>> len(a_set)
3

Тепер множина містить три елементи.

>>> a_set.add(1) 
>>> a_set
{1, 2, 4} 
>>> len(a_set)
3

Множина - це сумка з унікальними елементами. Якщо ви спробуєте додати до множини елемент, який там вже був, нічого не станеться. Не відбудеться ніякої помилки, і нічого не зміниться. Множина і далі міститиме три елементи.

>>> a_set = {1, 2, 3} 
>>> a_set
{1, 2, 3} 
>>> a_set.update({2, 4, 6})

Метод update() приймає множину і додає всі елементи цієї множини до нашої. Це те ж саме, що викликати add() для кожного елемента множини-параметра.

>>> a_set
{1, 2, 3, 4, 6}

Значення, які вже були в множині, ігноруються, бо множини не містять дублікатів.

>>> a_set.update({3, 6, 9}, {1, 2, 3, 5, 8, 13}) 
>>> a_set
{1, 2, 3, 4, 5, 6, 8, 9, 13}

Також update() можна викликати з довільним числом параметрів, це те ж саме, що викликати цей метод послідовно для кожного з них окремо.

>>> a_set.update([10, 20, 30]) 
>>> a_set
{1, 2, 3, 4, 5, 6, 8, 9, 10, 13, 20, 30}

Окрім множин, update() може приймати багато інших типів, наприклад, списки. Для них він поводиться аналогічно - додає кожен їх елемент в множину.

Видалення елементів з множини[ред.]

Є три способи видаляти елементи з множини. Перші два - discard() і remove() - мають одну тонку відмінність.

>>> a_set = {1, 3, 6, 10, 15, 21, 28, 36, 45} 
>>> a_set
{1, 3, 36, 6, 10, 45, 15, 21, 28} 
>>> a_set.discard(10) 
>>> a_set
{1, 3, 36, 6, 45, 15, 21, 28}

Метод discard() приймає єдине значення як параметр, і видаляє це значення з множини.

>>> a_set.discard(10) 
>>> a_set
{1, 3, 36, 6, 45, 15, 21, 28}

Якщо викликати discard() для значення, якого немає в множині, в ній нічого не зміниться. Не згенерується жодного винятку.

>>> a_set.remove(21) 
>>> a_set
{1, 3, 36, 6, 45, 15, 28}

Метод remove() також приймає єдине значення як аргумент, і також видаляє це значення з множини.

>>> a_set.remove(21)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
KeyError: 21

А ось і тонка відмінність. Якщо значення немає в множині, метод remove() кидає виняток KeyError.

Як і списки, множини мають метод pop().

>>> a_set = {1, 3, 6, 10, 15, 21, 28, 36, 45} 
>>> a_set.pop()
1 
>>> a_set.pop()
3 
>>> a_set.pop()
36 
>>> a_set
{6, 10, 45, 15, 21, 28}

Метод pop() видаляє одне значення з множини і повертає його. Щоправда, оскільки множини - це невпорядковані набори, то "останнього" немає елемента - важко передбачити, який елемент буде повернуто.

>>> a_set.clear() 
>>> a_set
set()

Метод clear() видаляє всі значення множини, роблячи її порожньою. Це дорівнює присвоєнню a_set = set(), яке просто створить нову порожню множину і замінить посилання на неї в змінній.

>>> a_set.pop()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
KeyError: 'pop from an empty set'

Спроба виклику pop() для порожньої множини створить виняток KeyError.

Теоретико-множинні операції[ред.]

Розділ названо так тому, що зараз ми розглянемо ті дії з множинами, які вивчає відповідний розділ математики.

>>> a_set = {2, 4, 5, 9, 12, 21, 30, 51, 76, 127, 195} 
>>> 30 in a_set
True 
>>> 31 in a_set
False

Щоб визначити, чи належить множині елемент , використовуйте оператор in. Працює так само, як і зі списками.

>>> b_set = {1, 2, 3, 5, 6, 8, 9, 12, 15, 17, 18, 21} 
>>> a_set.union(b_set)
{1, 2, 195, 4, 5, 6, 8, 12, 76, 15, 17, 18, 3, 21, 30, 51, 9, 127}

Метод union() (об’єднання) повертає множину, що складається з елементів, які належать хоча б одній з двох множин.

>>> a_set.intersection(b_set)
{9, 2, 12, 5, 21}

Метод intersection() (перетин) повертає множину, що складається з елементів, які належать одночасно двом множинам.

>>> a_set.difference(b_set)
{195, 4, 76, 51, 30, 127}

Метод difference() (різниця) повертає множину з тих елементів a_list, які не належать b_list.

>>> a_set.symmetric_difference(b_set)
{1, 3, 4, 6, 8, 76, 15, 17, 18, 195, 127, 30, 51}

Метод symmetric_difference (симетрична різниця) повертає множину з тих елементів, які належать рівно одній з множин.

Троє з цих методів симетричні.

>>> b_set.symmetric_difference(a_set)                                       
{3, 1, 195, 4, 6, 8, 76, 15, 17, 18, 51, 30, 127}

Симетрична різниця між a_set і b_set може відрізнятись від симетричної різниці між ними ж у протилежному порядку, але пам’ятайте, що множини не є впорядкованими. Будь-які дві множини, що містять однаковий набір значень, вважаються рівними.

>>> b_set.symmetric_difference(a_set) == a_set.symmetric_difference(b_set)  
True

Що власне й доводить порівняння вище. Не вводьтесь ув оману друкованим представленням множин оболонкою Python. Вони містять однакові значення, тому є рівними.

>>> b_set.union(a_set) == a_set.union(b_set)                                
True

Об’єднання двох множин теж симетричне.

>>> b_set.intersection(a_set) == a_set.intersection(b_set)                  
True

І перетин множин симетричний.

>>> b_set.difference(a_set) == a_set.difference(b_set)                      
False

А різниця - ні. І це логічно, бо різниця множин аналогічна відніманню чисел, тож порядок операндів має значення.

І нарешті, є кілька запитань про множини, на які можна отримати відповідь:

>>> a_set = {1, 2, 3} 
>>> b_set = {1, 2, 3, 4} 
>>> a_set.issubset(b_set)
True

a_set - підмножина (subset) множини b_set — всі елементи a_set також належать b_set.

>>> b_set.issuperset(a_set)
True

Таке саме запитання, лишень навпаки. b_set - надмножина a_set, тому що всі члени a_set також є членами b_set.

>>> a_set.add(5) 
>>> a_set.issubset(b_set)
False 
>>> b_set.issuperset(a_set)
False

Як тільки ви додасте до a_set значення, що не міститься в b_set, обидві перевірки повернуть False.

Множини в булевому контексті[ред.]

Тут, як завжди, все просто:

>>> def is_it_true(anything):
...   if anything:
...     print("yes, it's true")
...   else:
...     print("no, it's false")
... 
>>> is_it_true(set())
no, it's false 
>>> is_it_true({'a'})
yes, it's true 
>>> is_it_true({False})
yes, it's true

Порожня множина - False, а непорожня, не залежно від вмісту елементів - True.


* * *


Словники[ред.]

Словники - це невпорядковані набори пар "ключ - значення". Коли ви додаєте до словника новий ключ, завжди також повинні додати туди і значення (яке можна буде змінити пізніше). Вони оптимізовані для отримання значення, якщо ви знаєте відповідний ключ, але ніяк не навпаки.

Словник у Python - це як Perl 5. У Perl 5 змінні, що зберігають хеші, завжди починаються з символу %. У мові Python змінні можна називати довільним чином, Python запам’ятає їхній тип самостійно.

Створення словника[ред.]

Створити словник нескладно. Синтаксис подібний до синтаксису множин, але замість значень - пари "ключ-значення". Щойно ви отримали словник, можете перевіряти значення за їхніми ключами.

>>> a_dict = {'server': 'db.diveintopython3.org', 'database': 'mysql'}
>>> a_dict
{'server': 'db.diveintopython3.org', 'database': 'mysql'}

Спершу створюємо новий словник з двома елементами і присвоюємо їхні значення змінній a_dict. Кожен елемент - це пара "ключ-значення", і вся множина з елементів поміщена в фігурні дужки.

>>> a_dict['server']
'db.diveintopython3.org'

'server' - це ключ і пов'язане з ним значення, яке іменується як a_dict['server'] - 'db.diveintopython3.org'.

>>> a_dict['database']
'mysql'

'database' - ключ, який пов'язаний зі значенням 'mysql'.

>>> a_dict['db.diveintopython3.org']
Traceback (most recent call last):
 File "<stdin>", line 1, in <module> 
KeyError: 'db.diveintopython3.org'

Можна отримати значення за ключем, але не можна отримувати ключі за значенням. Тому a_dict['server'] дорівнює 'db.diveintopython3.org', а a_dict['db.diveintopython3.org'] генерує винятки, бо 'db.diveintopython3.org' - не ключ.

Модифікація словника[ред.]

Словники не мають попередньо заданого обмеження в розмірі. Ви в будь-який час можете додавати до словника нові пари "ключ-значення" чи змінювати значення чинної пари. Розвиваючи попередній приклад:

>>> a_dict
{'server': 'db.diveintopython3.org', 'database': 'mysql'} 
>>> a_dict['database'] = 'blog' 
>>> a_dict
{'server': 'db.diveintopython3.org', 'database': 'blog'}

Словник не може містити дублікатів ключів. Присвоєння значення чинному ключу затре старе значення.

>>> a_dict['user'] = 'mark'

Нові пари можна додавати коли завгодно. Синтаксис аналогічний синтаксису модифікації чинної пари.

>>> a_dict
{'server': 'db.diveintopython3.org', 'user': 'mark', 'database': 'blog'}

Новий елемент (ключ 'user' і значення 'mark') з'являється всередині. Насправді те, що елементи були впорядковані в першому прикладі, - лише випадковість, як і те, що зараз вони не впорядковані.

>>> a_dict['user'] = 'dora'
>>> a_dict
{'server': 'db.diveintopython3.org', 'user': 'dora', 'database': 'blog'}

Присвоєння значення чинному ключу просто замінює старе на нове.

>>> a_dict['User'] = 'mark'
>>> a_dict
{'User': 'mark', 'server': 'db.diveintopython3.org', 'user': 'dora', 'database': 'blog'}

Чи замінить це значення ключа 'user' назад на "mark"? Ні! Придивіться до ключа уважніше - він починається з великої літери. Ключі словника чутливі до регістру, тому ця інструкція створить нову пару "ключ-значення", а не перепише чинну. Вони може бути схожими для вас, але для Python вони цілковито різні.

Словники змішаних значень[ред.]

Словники створені не тільки для рядків. Словникові значення можуть мати будь-який тип, включаючи цілі, булеві, довільні об'єкти і навіть інші словники. І всередині одного словника значення не мусять мати один тип. Обмеження на ключі словника більш суворі, але вони теж можуть бути рядками, цілими та кількома іншими типами, і їх теж можна змішувати всередині одного словника.

Насправді ви вже бачили словник з нерядковими ключами і значеннями в вашій першій програмі.

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
            1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

Давайте розберемо це в інтерактивній оболонці.

>>> SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
...             1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
>>> len(SUFFIXES)
2

Як і для списків з множинами, функція len() повертає кількість елементів у словнику.

>>> 1000 in SUFFIXES
True

І як і в списках та множинах, ви можете використовувати оператор in для того, щоб з'ясувати, чи перебуває певний ключ у словнику.

>>> SUFFIXES[1000]
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

1000 - це ключ в словнику SUFFIXES. Його значення - список з восьми елементів.

>>> SUFFIXES[1024]
['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']

Так само з ключем 1024.

>>> SUFFIXES[1000][3]
'TB'

Оскільки SUFFIXES[1000] - список, ми можемо звернутись до окремого елемента списку за його індексом.

Словники в булевому контексті[ред.]

Порожні словники хибні, всі інші - істинні

Словники теж можна використовувати в булевому контексті.

>>> def is_it_true(anything):
...   if anything:
...     print("yes, it's true")
...   else:
...     print("no, it's false")
...
>>> is_it_true({})
no, it's false

Порожній словник завжди дорівнює False в булевому контексті.

>>> is_it_true({'a': 1})
yes, it's true

Будь-який словник, що містить хоча б один елемент, дорівнює True в булевому контексті.


* * *


None[ред.]

None - спеціальна константа мови Python. Це значення, що позначає відсутність значення. None - це не те ж саме що й False. None не 0. При порівнянні з None будь-що, окрім None, завжди поверне False.

None - єдине значення свого власного типу NoneType. Ви можете присвоїти значення None будь-яку змінну, але не можете створити інші об'єкти класу NoneType. Всі змінні зі значенням None дорівнюють одна одній.

>>> type(None)
<class 'NoneType'>
>>> None == False
False
>>> None == 0
False
>>> None == ''
False
>>> None == None
True
>>> x = None
>>> x == None
True
>>> y = None
>>> x == y
True

None в булевому контексті[ред.]

У булевому контексті None еквівалентно False, а not None - True.

>>> def is_it_true(anything):
...   if anything:
...     print("yes, it's true")
...   else:
...     print("no, it's false")
...
>>> is_it_true(None)
no, it's false
>>> is_it_true(not None)
yes, it's true

Для подальшого читання[ред.]


Вирази над структурами[ред.]

Наша уява напружена до країв, не як в художній літературі, щоб уявити собі речі яких немає, а щоб зрозуміти ті речі, які насправді є.
Річард Фейнман


Кожна мова програмування має щось, що могло б бути складним, але в ній воно спеціально зроблено просто. Якщо ви прийшли від іншої мови, ви можете це запросто пропустити, через те, що ваша стара мова не робила це просто (тому що вона натомість робила простим щось інше). Цей розділ розповість вам про спискові вирази, словникові вирази, і множинні вирази: три пов’язані ідеї, які зібрані навколо одної дуже потужної техніки. Але спершу, я хочу здійснити невелику екскурсію, в два модулі, які допоможуть вам переміщуватись по файловій системі.


* * *


Робота з файлами та каталогами[ред.]

Python 3 поширюється з модулем os, що містить купу функцій для отримання інформації і у деяких випадках для маніпуляції, локальними каталогами, файлами, процесами та змінними середовища. Python робить все що може, щоб надати уніфіковане API на всіх підтримуваних операційних системах щоб ваші програми могли працювати на будь-якому комп’ютері, з якомога меншою кількістю платформо-залежного коду.

Поточна робоча директорія[ред.]

Якщо ви тільки-но встановили Python, ви будете проводити багато часу в інтерактивній оболонці. Протягом цієї книжки, ви побачити приклади, які працюють за такою схемою:

  1. Імпортують один з прикладів з каталогу examples.
  2. Викликають функцію з того модуля
  3. Пояснюють результат
Хоч якийсь з каталогів завжди повинен бути поточним робочим.

Якщо ви не знаєте свою поточну робочу директорію, перший крок напевне буде невдалим, і закінчиться з ImportError. Чому? Бо Python буде шукати модуль в шляхах пошуку імпортів, але не знайде, бо там немає каталогу examples. Щоб це виправити можна зробити дві речі:

  1. Додати каталог examples в шляхи імпорту.
  2. Змінити поточну робочу директорію на examples

Поточна робоча директорія - невидима змінна яку Python ввесь час зберігає в пам’яті. Поточна робоча директорія завжди задана, незалежно чи ви в інтерактивній оболонці, запустили скрипт з командного рядка, чи виконуєте CGI скрипт на якомусь веб-сервері.

Модуль os містить дві функції для роботи з поточною робочою директорією.

>>> import os

Він поширюється разом з Python, і його можна імпортувати будь-де в будь-який момент.

>>> print(os.getcwd())
C:\Python31

Щоб визначити поточну робочу директорію використайте функцію os.getcwd(). Коли ви запустаєте графічну оболонку інтерпретатора - поточна директорія зазвичай та, де знаходиться виконуваний файл ітерпретатора. На Windows це зазвиай там, куди ви встановили Python, за замовучуванням C:\Python31. Якщо ви запускаєте інтерпретатор з командної оболонки, то поточна робоча директорія - та сама що й директорія в якій ви були, коли запустили python3.

>>> os.chdir('/Users/pilgrim/diveintopython3/examples')

Використовуйте функцію os.chdir() щоб змінити поточну робочу директорію.

>>> print(os.getcwd())
C:\Users\pilgrim\diveintopython3\examples

Коли я викликав функцію os.chdir(), я використав Лінукс-синтаксис запису шляху (прямі слеші, відсутні буквенні позначення логічних дисків) навіть незважаючи на те, що я в цей момент працюю на Windows. Це одне з місць в яких Python намагається стерти різницю між операційними системами.

Робота з іменами файлів та директорій[ред.]

Поки ми говоримо про директорії, я хочу вказати на те, що модуль os.path містить функції для маніпулювання іменами файлів та директорій.

>>> import os 
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py'))
/Users/pilgrim/diveintopython3/examples/humansize.py

Функція os.path.join() складає шлях з кількох часткових шляхів. В даному випадку - просто послідовно з’єднує рядки.

>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py'))
/Users/pilgrim/diveintopython3/examples\humansize.py

В цьому дещо менш тривіальному випадку, виклик os.path.join() додає додатковий слеш до імені перед його приєднанням до імені файла. Це зворотній, а не прямий слеш, тому що я пишу цей приклад під Windows. Якщо ви повторите такі ж дії в Linux чи Mac OS X, ви побачите замість цього прямий слеш. Не жартуйте зі слешами, завжди використовуйте os.path.join() і дозвольте мові Python зробити все правильно.

>>> print(os.path.expanduser('~'))
c:\Users\pilgrim

Функція os.path.expanduser() розгортає шлях який використовує ~ для позначення домашнього каталога корисувача в повний. Вона працює на будь-якій платформі де користувачі можуть мати домашню категорію, включно з Linux, Mac OS X та Windows. Шлях який повертається не містить останнього слеша, але функція os.path.join() не звертає на таке увагу.

>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py'))
c:\Users\pilgrim\diveintopython3\examples\humansize.py

Комбінуючи ці техніки, можна просто конструювати шляхи для директорій та файлів в домашньому каталозі. Функція os.path.join() приймає будь-яку кількість аргументів. Я дуже радів, коли відкрив це, тому що addSlashIfNecessary() - одна з тих дурних маленьких функцій які я завжди писав, коли створював свій інструментарій в новій мові. Не пишіть такі малі дурні функції в Python - про це вже замість вас подбали розумні люди.

os.path також містить функції для поділу повних імен файлів та каталогів на їх складові частини.

>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py'
>>> os.path.split(pathname)
('/Users/pilgrim/diveintopython3/examples', 'humansize.py')

Функція split() розділяє повне ім'я файла, і повертає кортеж що містить шлях та назву.

>>> (dirname, filename) = os.path.split(pathname)

Пам'ятаєте, коли я казав що можна використовувати множинне присвоювання щоб отримувати кілька значень з функції? os.path.split() - саме така функція. Ми присвоюємо результат її роботи кортежу з двох змінних. Кожна змінна приймає значення відповідного елемента кортежу що повертається.

>>> dirname
'/Users/pilgrim/diveintopython3/examples'

Перша змінна, dirname, приймає значення першого елементу результату os.path.split() - шлях до файла.

>>> filename
'humansize.py'

Друга змінна, filename приймає значення другого елементу кортежу - ім'я файла.

>>> (shortname, extension) = os.path.splitext(filename)
>>> shortname
'humansize'
>>> extension
'.py'

Модуль os.path також містить функцію os.path.splitext(), яка відділяє від імені файла його розширення, і повертає кортеж з двох елементів. Ви можете використати подібну техніку щоб присвоїти ці значення різним змінним.

Лістинги директорій[ред.]

Модуль glob використовує шаблони як в командному рядку

Модуль glob - інший інструмент зі стандартної бібліотеки. Це простий спосіб отримувати вміст директорій в своїх програмах, і він використовує шаблони з якими ви вже могли бути знайомими працюючи з командного рядка.

>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml')
['examples\\feed-broken.xml',
 'examples\\feed-ns0.xml',
 'examples\\feed.xml']

Модуль glob отримує шаблон, і повертає шляхи до всіх файлів і директорій які цьому шаблону відповідають. Наприклад, шаблон "*.xml", відповідає будь-якому файла з розширенням .xml.

>>> os.chdir('examples/')

Тепер перейдемо в підкаталог examples функція os.chdir() може приймати відносні шляхи.

>>> glob.glob('*test*.py')
['alphameticstest.py',
 'pluraltest1.py',
 'pluraltest2.py',
 'pluraltest3.py',
 'pluraltest4.py',
 'pluraltest5.py',
 'pluraltest6.py',
 'romantest1.py',
 'romantest10.py',
 'romantest2.py',
 'romantest3.py',
 'romantest4.py',
 'romantest5.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']

В шаблон можна додавати кілька загальних символів. НАприклад в цьому прикладі шаблон шукає всі файли, назви яких закінчуються розширенням .py і містять слово test де-небудь.

Отримання метаданих файла[ред.]

Кожна сучасна файлова система зберігає певні метадані про кожен файл: час створення, час останньї зміни, розмір і т.п. Python надає єдине API для доступу до цих метаданих. Вам не потрібно відкривати файл, все що потрібно, це назва файла.

>>> import os
>>> print(os.getcwd())                
c:\Users\pilgrim\diveintopython3\examples

Поточна робоча директорія - examples.

>>> metadata = os.stat('feed.xml')

feed.xml - файл в каталозі examples. Виклик функції os.stat() повертає об'єкт, який містить кілька різних видів відомостей про файл.

>>> metadata.st_mtime                  
1247520344.9537716

st_mtime - час модифікації, але записаний в не надто зручному форматі. (Технічно це кількість секунд від початку Епохи, яка почалася першого січня 1970-того року. Серйозно.)

>>> import time

Модуль time - частина стандартної бібліотеки. Він містить функції для перетворень між різними форматами часу, настройки часових зон, та форматування часу в тексті.

>>> time.localtime(metadata.st_mtime)  
time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17,
  tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)

Функція time.localtime() перетворює часове значення часу з секунд-від-початку-епохи (які є властивістю файла st_mtime значення якої повернула нам функція os.stat()) в більш зручну структуру з року, місяця, дня, години, хвилини, секунд, і так далі.

>>> metadata.st_size                              
3070

Функція os.stat() також повертає розмір файла в атрибуті st_size. Файл feed.xml містить 3070 байт.

>>> import humansize
>>> humansize.approximate_size(metadata.st_size)  
'3.0 KiB'

Ми можемо передати атрибут st_size функції approximate_size().

Отримання абсолютних шляхів[ред.]

У попередній секції функція glob.glob() повернула список відносних шляхів. Перший приклад використовував шляхи вигляду 'examples\feed.xml', а другий - навіть коротші відносні шляхи що складались лише з імені файла. Поки ви залишаєтесь в тій самій поточній директорії, ці відносні шляхи будуть працювати для відкриття файлів та отримання їх метаданих. Але якщо вам потрібно отримати абсолютні шляхи (тобто такі що включають всі директорії аж до кореневої, або до літери що позначає диск) - тоді вам потрібна функція os.path.realpath().

>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml


* * *


Спискові вирази[ред.]

Спискові вирази можуть містити довільні вирази мови Python

Спискові вирази (англ. list comprehensions) - це компактний спосіб створення списку з іншої послідовності, застосовуючи певну функцію до кожного елемента послідовності. (Іноді українською їх ще називали генераторні списки, але цей термін невдалий, тому що генератори, і генераторні вирази - зовсім інша історія).

>>> a_list = [1, 9, 8, 4]
>>> [elem * 2 for elem in a_list]
[2, 18, 16, 8]

Щоб зрозуміти що твориться прочитайте вираз справа наліво. a_list - список з якого ми беремо елементи. Інтерпретатор бере кожний елемент по черзі, і тимчасово присвоює його значення змінній elem. Після цього обчислює значення функції elem * 2 і додає результат до списку який повертається.

>>> a_list
[1, 9, 8, 4]

Спискові вирази створюють нові списки, старі залишаються незмінними.

>>> a_list = [elem * 2 for elem in a_list]
>>> a_list
[2, 18, 16, 8]

Крім того, можна цілком безпечно присвоювати результат спискового виразу змінній яка використовується у виразі. Python спочатку створить новий список в пам’яті, і тільки після завершення обчислення його елементів присвоїть отримані значення оригінальній змінній.

Всередині спискових виразів можна використовувати довільні вирази мови Python, в тому числі і функції модуля os.

>>> import os, glob
>>> glob.glob('*.xml')                                 
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']

Це поверне список всіх .xml в поточній робочій директорії.

>>> [os.path.realpath(f) for f in glob.glob('*.xml')]  
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']

А цей списковий вираз перетворить список імен файлів на список повних шляхів до них.

Спискові вирази також можуть фільтрувати елементи, включаючи в кінцевий список не кожні з них.

>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000]  
['pluraltest6.py',
 'romantest10.py',
 'romantest6.py',
 'romantest7.py',
 'romantest8.py',
 'romantest9.py']

Щоб відфільтрувати елементи списку можна додати оператор if в кінець спискового виразу. Вираз після ключового слова if буде обчислений для кожного елемента списку, і якщо він прийме істинне значення, елемент буде доданий до результуючого списку. Цей списковий вираз отримує список всіх файлів .py поточного каталогу, і вираз після if відфільтровує всі елементи розмір яких не більший за 6000. Таких файлів шість, тому кінцевий список містить шість елементів.

Поки що всі приклади спискових виразів містили прості вирази: домножини значення на константу, викликати функцію, чи взагалі просто повернути оригінальне значення списку (після фільтрування). Але обмежень на складність спискового немає.

>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')]
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
 (3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
 (3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]

Цей списковий вираз знаходить всі xml-файли в поточному каталозі, отримує розмір кожного файла (викликаючи функцію os.stat()), і створює кортеж з розміру файла і абсолютного шляху до нього (викликаючи функцію os.path.realpath()).

>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')]
[('3.0 KiB', 'feed-broken.xml'),
 ('3.3 KiB', 'feed-ns0.xml'),
 ('3.0 KiB', 'feed.xml')]

А цей списковий вираз - покращення попереднього за допомогою виклику функції approximate_size(), для розміру кожного файла.


* * *


Словникові вирази[ред.]

Словникові вирази - це так само як спискові вирази, тільки в результаті ми отримуємо словник а не список.

>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')]

Це не словниковий вираз, це все ще списковий вираз. Він знаходить всі файли .py, що містять в імені слово test, і будує кортежі з імені файла і його метаданих (які повертаються функцією os.stat()).

>>> metadata[0]
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
 st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
 st_mtime=1247520344, st_ctime=1247520344))

Кожен елемент результату - кортеж.

>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')}

А оце - словниковий вираз. Синтаксис подібний до спискового виразу, з двома основними відмінностями. По-перше, його поміщено в фігурні дужки замість квадратних. По-друге, замість єдиного виразу для кожного елемента, він містить два вирази розділені двокрапкою. Вираз перед двокрапкою (в цьому прикладі f) є ключем, а вираз після двокрапки (в даному випадку os.stat(f)) - значення.

>>> type(metadata_dict)
<class 'dict'>

Результат обчислення словникового виразу - словник. Який сюрприз!

>>> list(metadata_dict.keys())
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
 'pluraltest4.py']

Ключами даного словника є імена файла отримані з функції glob.glob('*test*.py').

>>> metadata_dict['alphameticstest.py'].st_size
2509

Значення пов’язане з кожним ключем було обчислене функцією os.stat(). Це означає що ми можемо отримувати зі словника метадані файла за його назвою. Одне із полів метаданих - st_size, розмір файла. Файл alphameticstest.py має розмір 2509 байт.

Як і спискові, словникові вирази можуть містити оператор if щоб фільтрувати вхідну послідовність на основі певного булевого виразу.

>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')}

Цей вираз будує словник який містить метаінформацію для кожного файла в поточній директорії.

>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \     
...                   for f, meta in metadata_dict.items() if meta.st_size > 6000}

Цей словниковий вираз побудовано на основі попереднього. Він відбирає тільки файли розмір яких більший за 6000 байт (if meta.st_size > 6000), та використовує їх щоб створити словник, ключами якого є імена файлів без розширення (os.path.splitext(f)[0]) а значення яких - приблизний розмір кожного файла (humansize.approximate_size(meta.st_size)).

>>> list(humansize_dict.keys()) 
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']

Ми вже бачили що таких файлів шість.

>>> humansize_dict['romantest9'] 
'6.5 KiB'

Значення кожного ключа - рядок що обчислений функцією approximate_size().

Інші трюки з словниковими виразами[ред.]

Ось трюк з використанням словникого виразу, який можливо колись стане в нагоді: обмін місцями ключів і значень словника.

>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}

Звичайно, це працює тільки якщо значення словника належать до незмінного типу, як рядки чи кортежі. Якщо ви спробуєте зробити це з словником який містить списки, все завершиться ефектною невдачею.

>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'


* * *


Множинні вирази[ред.]

Ну, і не варто забувати про те, що множини теж мають синтаксис виразів які дозволяють їх утворювати. Він надзвичайно схожий на синтаксис словникових виразів, тільки містить звичайні значення замість пар ключ:значення.

>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set}           
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}

Множинні вирази (як і всі інші) можуть приймати на вхід множину. Тут ми будуємо множину квадратів натуральних чисел від нуля до 9.

>>> {x for x in a_set if x % 2 == 0}  
{0, 8, 2, 4, 6}

І як і всі попередні, множинні вирази теж можуть містити оператор if для фільтрації.

>>> {2**x for x in range(10)}         
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}

Ну, і множинним виразам не обов’язково використовувати як базові дані множину, підійде будь-яка послідовність.


* * *


Для подальшого читання[ред.]

Текст[ред.]

I’m telling you this ’cause you’re one of my friends.
My alphabet starts where your alphabet ends!


Dr. Seuss, On Beyond Zebra!


Деяка нудна теорія яку ви повинні зрозуміти перед тим як зайнятись практикою[ред.]

Мало хто з людей про це думає, але тексти неймовірно складні. Взяти б для початку хоча б алфавіт. Люди з острова Бугенвіль мають найменший алфавіт в світі; письмо на їх мові, що зветься Ротока, складається лише з дванадцяти літер: A, E, G, I, K, O, P, R, S, T, U, та V. З іншого кінця спектру, такі мови як Китайська, Японська та Корейська мають тисячі символів. Англійська має 26 літер, чи 52 якщо рахувати окремо великі і маленькі, і ще купку знаків пунктуації: !@#$%&.

Коли ви кажете "текст", ви напевне маєте на увазі "букви та інші символи на екрані мого комп’ютера". Але комп’ютери не працюють з символами, вони працюють з бітами і байтами. Будь-який текст який ви бачите насправді зберігається в певному кодуванні. Говорячи дуже грубо, кодування символів - це бінарне відношення між зображенням символів які ви бачите на екрані, і даними які комп’ютер насправді зберігає в пам’яті та на диску. Існує багато різноманітних кодувань символів, деякі з них оптимізовані для конкретних мов, наприклад англійської, китайської або української, а інші можуть використовуватись в багатьох мовах.

Насправді все ще трохи складніше. Багато символів є спільними для різних кодувань, але кожне кодування використовує іншу послідовність біт, для збереження тих символів в пам’яті чи на диску. Тому ви можете думати про кодування тексту, як про певний вид криптографічного ключа. Як тільки хтось дає вам послідовність байт - файл чи веб-сторінку, не важливо - і стверджує що це "текст", вам потрібно знати яке кодування вони використали, щоб мати змогу перетворити байти в символи. Якщо вони передали вам неправильний ключ, або взагалі ніякого ключа, задача розшифровки тих даних залишається для вас, і я вам не заздрю. Існує велика ймовірність що ви виберете неправильне кодування, і отримаєте абракадабру.

Все що ви думали що знали про текст - неправда.

Звичайно ви бачили подібні веб-сторінки, з дивними знаками питання в тих місцях де повинні бути апострофи. Це зазвичай означає, що автор сторінки не вказав кодування правильно, ваш браузер спробував вгадати, і вийшла суміш очікуваних і неочікуваних символів. В англійській мові це може трошки дратувати. В інших мовах результат може бути цілком нечитабельним.

Існує кодування символів для кожної основної мови в світі. Так як всі мови різні, і пам’ять та дисковий простір історично були дорогими, кожне кодування символів оптимізується під конкретну мову. Під цим я маю на увазі, що кожне кодування що використовувало одні і ті ж цифри (0 - 255) для позначення символів тієї мови. Наприклад, ви напевне знайомі з кодуванням ASCII яке зберігає символи англійської мови, як числа що знаходяться між 0 та 127. (65 велике “A”, 97 маленьке “a”, і т.п.) Англійська має дуже простий алфавіт, тому він може бути повністю записаний менш ніж 128 числами. Якщо хтось із вас знає двійкову систему числення, для цього потрібно 7 з восьми біт в байті.

Західноєвропейскі мови, такі як Французька, Іспанська та Німецька мають більше символів ніж Англійська. Або, більш точно, вони мають букви, які комбінуються з різноманітними діакритичними знаками, такі як наприклад символ "ñ" в Іспанській. Найтиповішим кодуванням для цих мов є CP-1252, також відоме як “windows-1252” тому що воно широко використовується на платформі Microsoft Windows. Кодування CP-1252 в діапазоні 0-127 має такі ж символи як і ASCII, та потім доповнює їх в діапазоні 128-255 такими символами як n-з-тильдою-зверху (241), u-з-двома-крапками-зверху (252), і т.п. Це все ще однобайтове кодування, символ з найбільшим кодом, 255, все ще можна записати в одному байті.

Крім того, є ще такі мови як Китайська, Японська та Корейська, які мають так багато символів що потребують багатобайтових кодувань. Тобто кожен символ в них закодовано двобайтовим числом від 0 до 65535. Але різні багатобайтові кодування все ще мають таку ж спільну проблему як і однобайтові - вони використовують однакові числа для запису різних символів. Просто діапазон кодів став ширшим, тому що символів які потрібно закодувати набагато більше.

Це було майже нормальним в світі без мережі, де "текст" - це щось що ви самостійно набрали, і вряди-годи друкували. Не було багато "звичайного тексту". Код програм записувався в ASCII, а всі решта використовували текстові процесори, які описували свої власті (нетекстові) форми, які зберігали інформацію про кодування разом з оформленням та іншими даними. Люди відкривали ці документи тими ж текстовими процесорами що й автор, тому все більш-менш працювало.

Тепер подумайте про зростання глобальних мереж, таких як пошта, чи веб. Багато "звичайного тексту" літає навколо світу, створюючись на одному комп’ютері, і пересилаючись за допомогою другого комп’ютера на третій. Комп’ютери можуть бачити тільки числа, але числа можуть означати різні речі. О ні! Що робити? Ну, системи повинні створюватись так, щоб зберігати інформацію про кодування з кожним шматком "звичайного тексту". Пам’ятайте, це ключ до розшифрування чисел які читає комп’ютер, і перетворення їх в букви які можуть читати люди. Загублений ключ розшифрування означає спотворений текст, абракадабру, чи ще щось гірше.

Тепер подумайте про спробу збегірати різноманітні шматки тексту в одному місці, наприклад в одній таблиці бази даних яка зберігає всі листи які ви отримували. Вам потрібно збегігати кодування кожного щоб могти його правильно відобразити. Думаєте це важко? Спробуйте здійснити пошук по вашій базі даних, що означатиме конвертацію тексту між різними кодуваннями на льоту. Хіба це не звучить весело?

Тепер подумайте про можливість існування багатомовних документів, де символи з кількох мов використовуються поряд. (Підказка: програми які намагались працювати з такими документами зазвичай використовували спеціальні символи для перемикання "режимів". Опа, ви в кириличному кодуванні koi8-r, тому 241 означає Я; опа, тепер ви в грецькому кодуванні для Mac тому, 241 означає ώ.) І звичайно ви теж колись захочете робити пошук по таких документах.

Тепер багато плачте, тому що все що ви знали про текст - неправда. Не існує такої штуки як "простий текст".

Юнікод[ред.]

З’являється Юнікод.

Юнікод - система створена для запису кожного символа кожної мови. Юнікод представляє кожну букву, символ чи ідеограму як чотирибайтове число. Кожне число задає унікальний символ якої-небудь мови. (Не всі числа використовуються, але більш ніж 65535 з них так, тому двох байтів не достатньо). Символи що використовуються в багатьох мовах зазвичай мають один номер, якщо не існує достатньої етимологічної причини для протилежного. Кожному символу відповідає одне число, і кожному числу відповідає один символ. Кожне число завжди означає одне і те ж, бо немає "режимів" за якими потрібно слідкувати. U+0041 - завжди 'A', навіть якщо мова вашого тексту не містить цієї літери.

На перший погляд це виглядає як чудова ідея. Одне кодування щоб керувати ними всіма.[1] Багато мов в одному документів. Більш ніяких "змін режиму" щоб змінювати кодування посеред файла. Але в вас має виникнути законне запитання. Чотири байти? На кожен символ‽ Це виглядає страшенно витратно, особливо для таких мов як англійська чи іспанська, які потребують менше одного байта (256 символів) щоб виразити всі можливі символи мови. Насправді це марнотратство і в ієрогліфічних мовах (таких як китайська), яким ніколи не стане потрібно більше ніж два байти на символ.

Існує кодування Юнікоду яке використовує чотири байти на символ. Воно називається UTF-32, тому що 32 біти це і є 4 байти. UTF-32 - пряме кодування. Воно бере кожен номер символу Юнікоду, і записує його в цих чотирьох байтах. Це має певні переваги, найважливішою з яких є те, що ви можете знайти n-тий символ рядка за константний час, тому що n-тий символ починається з (4*n)-того байта. Це також має кілька недоліків, найочевиднішим з яких є те, що щоб зберегти кожен чортів символ потрібно аж чотири чортових байти.

І хоча є страшенно багато символів Юнікоду, виявляється що більшість людей майже ніколи не використовують символів поза першими 65535. Тому, існує інше кодування Юнікоду, назване UTF-16 (тому що 16 бітів = 2 байти). UTF-16 кодує кожен символ від 0 до 65535 двома байтами, після чого використовує деякі брудні хаки в випадках коли вам потрібно записати рідковживані символи юнікоду з "астрального простору" поза 65535-тим символом. Найбільш очевидна перевага: UTF-16 займає вдвічі менше місця ніж UTF-32, тому що кожен символ потребує лише двох байт місця замість чотирьох (звісно окрім тих які таки потребують). І ви все ще можете просто знаходити n-тий символ за константний час, якщо припустите, що рядок не містить жодних символів з "астрального простору", що є гарним припущенням аж поки воно не стає невірним.

Але існує також не такий очевидний недолік як кодування UTF-32 так і UTF-16. Різні комп’ютерні системи зберігають окремі байти різними способами. Це означає що символ U+4E2D може зберігатись в UTF-16 як 4E 2D та як 2D 4E, залежно від порядку байтів в системі. (Для UTF-32 взагалі можливі чотири різні порядки запису). Поки ваші документи ніколи не покидають вашого комп’ютера, ви в безпеці - різні додатки одного і того ж комп’ютера будуть використовувати один і той самий порядок байтів. Але якщо ви захочете передати документи між системами, чи можливо у всесвітню мережу, вам потрібно буде якимось способом вказати порядок байт вашого кодування. Інакше система що отримує дані не буде знати чи двобайтова послідовність 4E 2D означає U+4E2D U+2D4E.

Щоб розвє’язати цю проблему, багатобайтові кодування Юнікоду описують "Мітку порядку байт" (англ. Byte order mark), яка є спеціальним недрукованим символом який ви можете вставити на початку свого документа щоб вказати в якому порядку розміщені його байти. Для UTF-16 мітка порядку байт - U+FEFF. Якщо ви отримуєте документ, що починається з байт FF FE, ви знаєте що байти йдуть в одному порядку, якщо FE FF - в протилежному.

І все ж, UTF-16 не ідеальна, особливо коли вам доводиться мати справу з великою кількістю символів ASCII. Якщо подумати, то навіть китайська веб-сторінка міститиме багато ASCII символів - елементи та атрибути HTML що оточуватимуть друковані китайські ієрогліфи. Здатність знайти n-тий символ за одиничний час це добре, але все ще залишається надокучлива проблема тих символів з астральної площини, яка означає що ви не можете гарантувати те що кожен символ це рівно два байти, тому ви насправді не можете знайти n-тий символ за константний час, якщо звісно не заведете окремий індекс. І звичайно на світі море тексту ASCII...

Інші люди задумувались над цими питаннями і прийшли до рішення:

UTF-8

UTF-8 - система кодування Юнікоду змінної довжини. Тобто, різні символи можуть займати різну кількість байт. Для символів ASCII (A-Z, і т.п.) UTF-8 використовує лише один байт на символ. Насправді вона навіть використовує точно такі ж байти як і в ASCII: перші 128 кодів символів (0-127) в UTF-8 не відрізняються від кодів тих самих символів в ASCII. "Розширені латинські" символи, такі як ñ та ö, і кирилиця займають два байти. (Байти це не простий номер символа в таблиці Юнікоду, а хитрим чином закодований). Китайські символи, такі як 中, займають три байти. Рідко використовувані символи з "астральної площини" займають чотири байти.

Недоліки: через те що кожен символ займає різну кількість байт, знаходження n-того символа це операція складності O(N), тобто чим довший рядок, тим більше часу потрібно щоб знайти певний символ в ньому. Також, потрібні певні маніпуляції над бітами для того щоб отримати код символа з байт які його кодують, та навпаки.

Переваги: дуже ефективне кодування стандартних символів ASCII. Не гірше ніж UTF-16 для кирилиці, і розширеної латиниці. Краще ніж UTF-32 для китайських ієрогліфів. Також (і ви не мусите мені тут вірити, тому що я не збираюсь показувати вам математику), через природу необхідних операцій з бітами пропадає проблема порядку байт. Документ записаний в UTF-8 використовує однакову послідовність байт на всіх комп’ютерах.


* * *


Пірнаймо![ред.]

В Python 3 всі рядки є послідовностями символів Юнікоду. В Python не існує такої штуки як рядок закодований в UTF-8, чи рядок закодований як cp1251. "Це рядок в UTF-8?" - неправильне запитання. UTF-8 - спосіб кодування символів як послідовностей байт. Якщо ви хочете взяти рядок і перетворити його на послідовність байт в певному кодуванні, Python 3 може допомогти в цьому. Якщо ви хочете взяти послідовність байт, і перетворити в послідовність символів Python може допомогти і в цьому. Байти не є символами: байти це байти. Символи - це абстракція. Рядок - послідовність таких абстракцій.

>>> s = '深入 Python'

Щоб створити рядок, помістіть його в лапки. Рядки в Python можуть описуватись як одиничними ('), так і подвійними (") лапками.

>>> len(s)
9

Вбудована функція len() повертає довжину рядка, тобто кількість символів. Це та ж сама функція яку ви використовували щоб знайти довжину рядка, кортежу, множини чи словника. Рядок - це як кортеж з символів.

>>> s[0]
'深'

Так само я ви можете отримати окремі елементи списку, ви можете отримувати окремі символи рядка за індексом.

>>> s + ' 3'
'深入 Python 3'

Так як і в списках, ви можете конкатенувати рядки використовуючи оператор +.


* * *


Форматування рядків[ред.]

Давайте ще раз глянемо на humansize.py.

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
            1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

'KB', 'MB', 'KiB' - це все рядки.

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''

Докстрінґи функції - теж рядки. Їх текст може містити багато рядків, тому він використовує три одинарні лапки для позначення початку і кінця.

    if size < 0:
        raise ValueError('number must be non-negative')

Тут ми бачимо ще один рядок, що передається в функцію як людське пояснення винятку що стався.

    multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
    for suffix in SUFFIXES[multiple]:
        size /= multiple
        if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)

А це ... Почекайте. Це ще що за хрінь?

Python 3 підтримує форматування значень в рядки. І хоча тут можуть використовуватись дуже складні вирази, але найпростішим використанням є вставка значення в рядок.

>>> username = 'mark' 
>>> password = 'PapayaWhip' 
>>> "{0}'s password is {1}".format(username, password)
"mark's password is PapayaWhip"

Тут відбувається багато всього. По-перше, виклик методу над рядковим літералом. Рядки це об’єкти, а об’єкти мають методи. По-друге, значенням виразу є рядок. По-третє, {0} та {1} - поля заміни, які замінюються аргументами що передаються в метод format().

Складені імена полів[ред.]

Попередній приклад ілюструє найпростіший випадок, де поля що підставляються - звичайні цілі. Цілі поля підстановки розглядаються як індекси в списку аргументів методу format(). Це означає що {0} замінюється першим аргументом (в даному випадку username), {1} - другим (password), і т.д. Можна використовувати скільки завгодно аргументів. Але поля підстановки набагато потужніші.

>>> import humansize
>>> si_suffixes = humansize.SUFFIXES[1000]      
>>> si_suffixes
['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']

Замість того щоб викликати якусь функцію модуля humansize ми отримуємо описаний в ньому список суфіксів.

>>> '1000{0[0]} = 1{0[1]}'.format(si_suffixes)  
'1000KB = 1MB'

А це виглядає складно, але насправді таким не є. {0} посилається на перший аргумент переданий в метод format(), в нашому випадку це був би si_suffixes. Але si_suffixes - це список. Тому {0[0]} стосується першого елементу в цьому списку: 'KB'. Так само, {0[1]} стосується другого елементу списку переданого першим аргументом: 'MB'. Все ззовні фігурних дужок, включаючи 1000, знак рівності та прогалики залишається неторканим. Кінцевий результат - рядок '1000KB = 1MB'.

{0} замінюється на перший аргумент функції format(), {1} - на другий.

Цей приклад показує, що специфікатори формату можуть отримувати доступ до елементів структур даних використовуючи синтакс мови Python. Це називається складеними іменами полів. Наступні складені імена працюють:

  • Передача списку і отримання елементів за індексом (як в попередньому прикладі)
  • Передача словника і отримання значень за ключами
  • Передача модуля, і отримання його функцій та констант за іменем
  • Передача екземпляра класу і отримання його атрибутів за іменем
  • Будь-яка комбінація попереднього

І щоб вас дещо вразити, ось приклад що використовує все вище перелічене:

>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'

І ось як воно працює:

  • Модуль sys містить інформацію про запущений в даний момент інтерпретатор. Після того, як його імпортували, його можна передавати як аргумент методу format(). Тому поле підстановки {0} посилається на модуль sys.
  • sys.modules - це словник усіх модулів які були імпортованими в даній сесії інтепретатора. Ключами є імена модулів, значеннями - самі об'єкти модулів. Тому поле підстановки {0.modules} посилається на словник відповідних модулів.
  • sys.modules['humansize'] - це модуль humansize який ви недавно імпортували. Поле підстановки {0.modules[humansize]} відповідає модулю humansize. Зауважте невеличку відмінність у синтаксисі. В коді Python ключами словника sys.modules були б рядки, щоб отримати елемент потрібно було б передавати ключ в лапках (наприклад 'humansize'). Але в полі підстановки ви опускаєте лапки навколо ключа словника. Цитуючи PEP 3101: Advanced String Formatting, "Правила розпізнавання ключів словника дуже прості. Якщо він починається з цифри, то розглядається як число, інакше використовується як рядок".
  • sys.modules['humansize'].SUFFIXES[1000] - список суфіксів SI: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']. Тому поле підстановки {0.modules[humansize].SUFFIXES[1000]} відповідає цьому списку.
  • sys.modules['humansize'].SUFFIXES[1000][0] - перший елемент списку суфіксів SI: 'KB'. Тому повною заміною поля {0.modules[humansize].SUFFIXES[1000][0]} є двосимвольний рядок 'KB'.

Специфікатори формату[ред.]

Але зачекайте. Є ще! Давайте глянемо ще разок на дивний рядок коду з humansize.py:

if size < multiple:
    return '{0:.1f} {1}'.format(size, suffix)

{1} замінюється другим аргументом який передається методу format(), який є суфіксом. Але що таке {0:.1f}? Ну, це двоє речей: вже знайома вам {0}, і незнайома :.1f. Друга половина (включаючи двокрапку і все після неї) описує специфікатор формату, який описує те, як змінна що підставляється повинна форматуватись.

Специфікатори формату дозволяють модифікувати текст що підставляється різними корисними способами, як це робить функція printf мови C. Ви можете доповнювати значення зліва нулями чи пропусками, вирівнювати рядки, керувати точністю десяткових дробів, і навіть відображати числа в шістнадцятковій системі числення

Всередині поля підстановки, двокрапка (:) позначає початок специфікатору формату. Специфікатор формату ".1" означає округлення до однієї цифри після коми. Специфікатор формату "f" означає число з плаваючою крапкою. Тому отримавши значення змінної size = 698.24 та suffix = 'GB', відформатований рядок міститиме '698.2 GB':

>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'

На рахунок всіх кривавих деталей щодо специфікаторів формату, звертайтесь до розділу Format Specification Mini-Language в офіційній документації мови Python.


* * *


Інші типові рядкові методи[ред.]

Окрім форматування, з рядками можна робити кілька інших корисних трюків.

>>> s = '''Finished files are the re- 
... sult of years of scientif- 
... ic study combined with the 
... experience of years.'''

Ви можете вводити багаторядкові рядки прямо в інтерактивній оболонці. Для цього почніть рядок і коли натиснете Enter, оболонка запропонує вам продовжити рядок. Коли ви закриєте потрійні лапки, натиснення Enter виконає команду (в даному випадку присвоєння рядка змінній s).

>>> s.splitlines() 
['Finished files are the re-',
 'sult of years of scientif-',
 'ic study combined with the',
 'experience of years.']

Рядковий метод splitlines() повертає список рядків, в якому кожен рядок відповідає одному рядку даного. Зауважте що символи повернення каретки на кінці рядка не включаються.

>>> print(s.lower()) 
finished files are the re- 
sult of years of scientif- 
ic study combined with the 
experience of years.

Метод lower() переводить весь рядок в нижній регістр. Метод upper() - в верхній.

>>> s.lower().count('f')
6

Метод count() шукає кількість входжень підрядка в рядок. Так, в даному реченні літера 'f' справді зустрічається 6 разів.

Ось інший поширений випадок. Нехай в нас є набір пар ключ-значення в форматі 'key1=value1&key2=value2', і ви хочете розділити їх та створити словник вигляду {key1: value1, key2: value2}.

>>> query = 'user=pilgrim&database=master&password=PapayaWhip' 
>>> a_list = query.split('&') 
>>> a_list
['user=pilgrim', 'database=master', 'password=PapayaWhip']

Метод рядка split(), має один аргумент - розділювач. Метод розбиває рядок на список рядків, якщо вони були розділені розділювачем. В цьому випадку розділювачем є символ амперсанда, але він може бути довільним рядком.

>>> a_list_of_lists = [v.split('=', 1) for v in a_list if '=' in v]
>>> a_list_of_lists 
[['user', 'pilgrim'], ['database', 'master'], ['password', 'PapayaWhip']]

Тепер в нас є список рядків, кожен з яких містить ключ, після якого знак рівності, а потім значення. Ми можемо використати спискові вирази, щоб пройтись по списку і поділити кожен рядок на два, покладаючись на перший знайдений знак рівності. Необов’язковим другим аргументом методу split() є кількість бажаних поділів. 1 означатиме "ділити лише раз", тому метод split() поверне двоелементний список. (В теорії значення теж може містити знаки рівності. Якщо ви просто застосуєте 'key=value=foo'.split('='), ви отримаєте трьохелементрий список ['key', 'value', 'foo'].)

>>> a_dict = dict(a_list_of_lists)
>>> a_dict {'password': 'PapayaWhip', 'user': 'pilgrim', 'database': 'master'}

І нарешті, Python може перетворити список списків у словник, просто за допомогою функції dict().

доробити

Зрізання рядків[ред.]

Якщо ви маєте заданий рядок, ви можете отримати будь-яку його частину як новий рядок. Це називається зрізом рядка. Зрізи рядків працюють так само як зрізи списків, що досить логічно, бо рядки - всього лише послідовності символів.

>>> a_string = 'My alphabet starts where your alphabet ends.'
>>> a_string[3:11]
'alphabet'

Можна отримати частину рядка ("зріз") задавши два індекси. Новий рядок буде містити всі символи по порядку, починаючи від першого індекса, і не включаючи другий індекс.

>>> a_string[3:-3]
'alphabet starts where your alphabet en'

Як і в списках, індекси можуть бути від’ємними.

>>> a_string[0:2]
'My'

Індексація рядків починається з нуля, тому вираз a_string[0:2] поверне перші два символи рядка від a_string[0], аж до a_string[2] невключно.

>>> a_string[:18]
'My alphabet starts'

Якщо лівий індекс зрізу - нуль, його можна опустити, так як він припускається за замовчуванням. Тому a_string[:18] працює аналогічно a_string[0:18].

>>> a_string[18:]
' where your alphabet ends.'

Аналогічно, якщо правий індекс зрізу дорівнює довжині рядка, його можна опустити. Тому a_string[18:] еквівалентий a_string[18:44], через те що в рядку 44 символи. Тут існує приємна симетрія. В цьому 44-символьному рядку a_string[:18] повертає перші 18 символів, а a_string[18:] повертає все крім перших 18 символів. Взагалі, a_string[:n] завжди поверне n перших символів, а a_string[n:] поверне решту, незалежно від довжини рядка.


* * *


Рядки та байти[ред.]

Байти це байти, а символи це абстракція. Незмінна послідовність символів Unicode називається рядком. Незмінна послідовність чисел між 0 та 255 називається байтовим об'єктом.

>>> by = b'abcd\x65'
>>> by
b'abcde'

Для запису байтового об’єкта використовують синтаксис b (байтовий літерал). Кожен байт у літералі може бути символом ASCII чи шістнадцятковим кодом від \x00 до \xff (0–255).

>>> type(by)
<class 'bytes'>

Типом байтового об’єкта є bytes

>>> len(by)
5

Як і у випадку списків та рядків довжину байтового об’єкта можна отримати за допомогою вбудованої функції len().

>>> by += b'\xff'
>>> by
b'abcde\xff'

Як і в рядках та списках оператор + здійснює конкатенацію, результатом якої є новий байтовий об’єкт.

>>> len(by)
6

Конкатенація п’ятибайтового об’єкта з однобайтовим дає нам об’єкт з шести байт.

>>> by[0]
97

Як і в рядках та списках можна використовувати індекси для отримання окремих байтів. Елементами рядка є символи, елементи байтового об’єкта - цілі числа. Якщо точніше, цілі числа в діапазоні 0–255.

>>> by[0] = 102
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

Об’єкт bytes - незмінний, ми не можемо робити присвоєння окремим байтам. Якщо потрібно змінити окремі байти, можна використати зрізи та конкатенацію (які працюватимуть так само як і в рядках), чи перетворити об’єкт bytes в об’єкт bytearray.

>>> by = b'abcd\x65'
>>> barr = bytearray(by)
>>> barr
bytearray(b'abcde')

Щоб перетворити об’єкт bytes в об’єкт bytearray використайте вбудовану функцію bytearray().

>>> len(barr)
5

Всі методи і операції що можна виконати над об’єктом bytes можна виконати і над об’єктом bytearray.

>>> barr[0] = 102
>>> barr
bytearray(b'fbcde')

Єдиною відмінністю є те що можна присвоювати значення окремим байтам. Присвоюване значення повинно бути цілим в діапазоні 0–255.


Одну річ ніколи не можна робити - змішувати байти і рядки.

>>> by = b'd'
>>> s = 'abcde'
>>> by + s
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: can't concat bytes to str

Не можна конкатенувати байти з рядками - це два різні типи.

>>> s.count(by)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: Can't convert 'bytes' object to str implicitly

Не можна рахувати кількість входжень байтів в рядок, тому що рядок не містить байтів. Рядок - послідовність символів. Можливо ви мали на увазі "підрахувати кількість входжень рядка що отримується декодуванням цієї послідовності байт таким-то кодуванням"? Ну, тоді потрібно було написати це явно. Python 3 ніколи не перетворить байти в рядки, чи навпаки неявно.

>>> s.count(by.decode('ascii'))
1

Завдяки дивовижному співпадінню цей рядок коду перекладається на українську як "підрахувати кількість входжень рядка що отримується декодуванням цієї послідовності байт таким-то кодуванням".

Ось це і є зв’язком між рядками і байтами: байтові об’єкти мають метод decode() що приймає кодування символів та повертає рядок, а рядки мають метод encode() що теж приймає кодування і повертає байтовий об’єкт. В попередньому прикладі розкодуванням було досить прямолінійним - перетворити послідовність байт що задають рядок в кодуванні ASCII в відповідний рядок. Але такі самі процеси працюють і для будь-якого кодування що підтримує символи в рядку - навіть застарілі (не Unicode) кодування.

>>> a_string = '深入 Python'
>>> len(a_string)
9

a_string - рядок. У ньому 9 символів.

>>> by = a_string.encode('utf-8')
>>> by
b'\xe6\xb7\xb1\xe5\x85\xa5 Python'
>>> len(by)
13

by - байтовий об’єкт. У ньому 13 байт. Таку послідовність байт ми отримаємо коли візьмемо a_string і закодуємо його в UTF-8.

>>> by = a_string.encode('gb18030')
>>> by
b'\xc9\xee\xc8\xeb Python'
>>> len(by)
11

Подібний байтовий об’єкт з 11 байт ми отримаємо якщо закодуємо a_string в кодуванні GB18030.

>>> by = a_string.encode('big5')
>>> by b'\xb2`\xa4J Python'
>>> len(by)
11

Подібний байтовий об’єкт, що складається з зовсім іншої послідовності байт отримаємо якщо візьмемо a_string і закодуємо її в Big5.

>>> roundtrip = by.decode('big5')
>>> roundtrip
'深入 Python'
>>> a_string == roundtrip
True

roundtrip - рядок. Він має 9 символів. Таку послідовність символів можна отримати якщо взяти by і розкодувати її за допомогою алгоритму кодування Big5. Цей рядок збігається з початковим.


* * *


Постскриптум: Кодування коду Python[ред.]

Python 3 припускає що ваш код - тобто кожен файл .py записаний в кодуванні UTF-8.

В Python 2, кодуванням за замовчуванням для файлів .py було ASCII. В Python 3, кодуванням за замовчуванням є UTF-8.

Якщо ви захочете використати для свого коду інше кодування, то можете вставити оголошення кодування в перший рядок кожного файла. Наприклад такий запис встановлює для файла кодування windows-1252:

# -*- coding: windows-1252 -*-

Технічно, опис кодування файла може розміщуватись також і на другому рядку, якщо перший - UNIX-подібна команда hash-bang.

#!/usr/bin/python3
# -*- coding: windows-1252 -*-

Для детальнішої інформації читайте PEP 263: Defining Python Source Code Encodings.


* * *


Для подальшого читання[ред.]

Про Unicode в Python:

Про Unicode загалом:

Про кодування символів в форматах файлів:

Про рядки та форматування рядків:

Зноски[ред.]

  1. Тут автор робить натяк на майстер-перстень з "Володаря Перснів". Хто може глянути в книжці вірш про те як три персні віддали ельфам, ще скількись там людям і гномам, і ... рядок про один перстень має стояти замість цього речення, тільки замість слова перстень - кодування

Регулярні вирази[ред.]

Деякі люди, коли cтикаються з проблемою, думають: "Я знаю, я використаю регулярні вирази". Після цього вони мають дві проблеми.
Джемі Завінські


Отримання потрібної частки з великого масиву тексту може стати викликом. Рядки в Python мають методи для пошуку та заміни: index(), find(), split(), count(), replace() та подібні. Але ці методи обмежені найпростішими випадками. Наприклад, метод index() шукає єдиний, жорстко зафіксований підрядок, і пошук завжди чутливий до регістру. Щоб здійснити нечутливі до регістру пошуки в рядку s, потрібно застосувати s.lower(), чи s.upper() і переконатись що запит написаний в тому ж регістрі. Методи replace() та split() мають ті ж обмеження.

Якщо вашої цілі можна досягти використовуючи методи для роботи з рядками, ви повинні використовувати їх. Вони швидкі, прості, їх просто читати. Про швидкий, простий і читабельний код можна розказувати багато гарного. Але якщо ви використовуєте багато цих функцій в перемішку з операторами if для обробки особливих випадків, чи якщо ви з'єднуєте виклики split() та join() щоб різати та перемішувати ваші рядки, можливо, вам варто буде перейти до регулярних виразів.

Регулярні вирази - це потужний і (переважно) стандартизований спосіб пошуку, заміни та лексичного аналізу тексту з складними шаблонами символів. Хоча, синтаксис регулярних виразів досить щільний і не схожий на звичайний код, але результат може виявитись більш читабельним ніж саморобне рішення, що використовує велику кількість рядкових функцій. Існує навіть спосіб вставити в регулярний вираз коментарів, тому ви можете включати в них документацію.

Якщо ви користувались регулярними виразами в інших мовах (таких як Perl, JavaScript, чи PHP), їх синтаксис в Python може видатись знайомим. Прочитайте документацію до модуля re, щоб отримати огляд доступних функцій та їх аргументів.


* * *


Приклад: Адреси[ред.]

Ця серія прикладів викликана проблемою з реального життя, з якою я стикнувся на моїй роботі кілька років тому, коли я хотів отримати та стандартизувати адреси вулиць з старої системи перед тим, як імпортувати її в нову. (Бачте, я не просто даю висмоктані з пальця приклади, вони справді корисні). Цей приклад показує як я підійшов до проблеми.

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                
'100 NORTH MAIN RD.'

Моєю метою було стандартизувати адреси вулиць так, щоб 'ROAD' завжди записувалось скорочено, як 'RD.'. На перший погляд, це виглядає просто, і я подумав, що можна використати метод для роботи з рядками replace(). В будь-якому випадку дані були завжди в верхньому регістрі, тому чутливість до регістру не мала бути проблемою. І в цьому надзвичайно простому прикладі, s.replace() чудово виконала свою роботу.

>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                
'100 NORTH BRD. RD.'

Життя, на жаль, повне контрприкладів, і я швидко виявив це. Проблемою є те, що 'ROAD' з'являється в адресі двічі, один раз як частина імені вулиці 'BROAD', і один раз як ціле слово. Метод replace() бачить ці два випадки, і сліпо замінює обидвох, тим часом, я бачу як мої адреси псуються.

>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  
'100 NORTH BROAD RD.'

Щоб розв'язати проблему адрес з більш ніж одним підрядком 'ROAD', можна, наприклад, зробити так: шукати 'ROAD' лише в останніх чотирьох символах адреси (s[-4:1]), і не чіпати решту рядка (s[:-4]). Але ви можете бачити, що це вже досить заплутано. (Якщо б ви замінювали 'STREET' на 'ST.', ви повинні були б використовувати s[:-6] та s[-6:].replace(...).) Ви б хотіли повернутись через пів року щоб зневаджувати це? Я знаю що я б не хотів.

>>> import re

Настав час перейти до регулярних виразів. В мові Python, вся функціональність пов'язана з регулярними виразами міститься в модулі re.

>>> re.sub('ROAD$', 'RD.', s)               
'100 NORTH BROAD RD.'

Погляньте на перший параметр: 'ROAD$'. Це простий регулярний вираз, який співставляється 'ROAD' лише тоді, коли знаходить його в кінці рядка. $ означає "кінець рядка". (Існує також протилежний за значенням символ - ^, який означає "початок рядка"). Використовуючи метод re.sub() шукаємо в рядку s співпадіння з регулярним виразом 'ROAD$' та замінюємо його на 'RD.'. Це спрацьовує для ROAD в кінці рядка s, але не спрацьовує для слова BROAD, тому що воно знаходиться посередині.

^ відповідає початку рядка. $ відповідає кінцю рядка.

Продовжуючи історію з очисткою адрес: я невдовзі відкрив, що в попередньому прикладі, співставляння 'ROAD' кінцю рядка, не достатньо добре, бо деякі адреси просто закінчуються назвою вулиці. Це працювало в більшості випадків, але коли назвою вулиці було 'BROAD', регулярний вираз виявляв співпадіння з кінцівкою 'ROAD', а це не те що мені було потрібно.

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.' 
>>> re.sub('\\bROAD$', 'RD.', s)
'100 BROAD'

Що мені справді було потрібно - це співпадіння з словом ROAD на кінці рядка, але тільки тоді, коли це окреме слово, а не частина якогось іншого слова. Щоб виразити це в регулярному виразі використовують символ \b який означає що в цьому місці повинна знаходитись межа (boundary) слова. В Python, це ускладнюється тим фактом, що символ '\' в рядку сам повинен екрануватись. Це іноді називаються "чумою бекслешів", і це є причиною того що писати регулярні вирази в Perl простіше ніж в Python. З іншого боку, Perl змішує регулярні вирази з рештою синтаксису, тому іноді важко відрізнити синтаксичну помилку від помилки в регулярному виразі.

>>> re.sub(r'\bROAD$', 'RD.', s) 
'100 BROAD'

Щоб позбутись надлишку бекслешів, ви можете використати те, що називається "чистим (raw) рядком", додавши перед рядком символ r. Це повідомляє інтерпретатору Python, що нічого в цьому рядку не повинно екрануватись. '\t' - це рядок з одного символу табуляції, а r'\t' - це рядок з двох символів: бекслеша та букви t. Раджу вам завжди використовувати чисті рядки для опису регулярних виразів, бо інакше все робиться занадто заплутаним занадто швидко (а регулярні вирази з самого початку достатньо заплутані).

>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s) 
'100 BROAD ROAD APT. 3'

Ох! На жаль, я пізніше знайшов ще більше прикладів, які суперечать моїм міркуванням. В цьому випадку, адреса вулиці містила слово 'ROAD' як окреме слово, але воно не було на кінці рядка, бо адреса містила номер квартири. Через те що 'ROAD' не в кінці рядка виклик re.sub() не здійснював взагалі ніяких замін, і ви отримували незмінний рядок назад, що не те що вам було потрібно.

>>> re.sub(r'\bROAD\b', 'RD.', s)
'100 BROAD RD. APT 3'

Щоб розв’язати цю проблему, я вилучив симовол $ і додав ще один \b. Тепер регулярний вираз читається як "знайди відповідність шаблону 'ROAD' коли це окреме слово де-небудь в рядку", не залежно від того на початку, в кінці чи десь посередині.


* * *


Приклад: Римські числа[ред.]

Швидше за все ви вже бачили римські числа, навіть якщо ви їх і не впізнали. Їх можна побачити на фасадах старих будинків (MCMXLVI замість "збудовано в 1946), чи при нумерації розділів в деяких книгах. Така система запису чисел з’явилась ще в римській імперії, звідки й назва.

В римській системі числення існує кілька символів, які повторююься і комбінуються різним чином щоб утворювати числа:

  • I - 1
  • V - 5
  • X - 10
  • L - 50
  • C - 100
  • D - 500
  • M - 1000

А ось деякі загальні правила утворення римських чисел:

  • Іноді символи аддитивні. I це 1, II це 2, і III це 3. VI це 6 (буквально, "5 і 1"), VII це 7, і VIII - 8.
  • Символи що позначають степені десяти (I, X, C, та M) можуть повторюватись до трьох разів. Далі потрібно віднімати від наступного за значенням символа, зазвичай кратного 5. Не можна записувати 4 як IIII, замість цього пишуть IV (на 1 менше ніж 5). 40 записується як XL (на 10 менше ніж 50), 41 як XLI, 42 як XLII, 43 як XLIII, а 44 як XLIV (на 10 менше ніж 50, і ще на одиницю менше ніж 5).
  • Іноді символи ... пряма протилежність аддитивним. Ставлячи деякі символи перед іншими, ви віднімаєте їх від загального значення. Наприклад, щоб записати 9, ви віднімаєте 1 від 10, записуючи це як IX. 8 це VIII, але 9 - це IX, бо не можна писати VIIII (дивись попереднє правило). 90 це XC, 900 - CM.
  • Символи що не є степенями 10 не повторюються двічі підряд. 10 записується як X, і ніколи як VV. 100 - завжди C, і ніколи не LL.
  • Римські цифри читаються зліва направо, і порядок символів має велике значення. DC - це 600, а CD - зовсім інше значення - 400 (на 100 менше ніж 500). CI це 101, а IC навіть не є правильним римським числом, бо не можна віднімати 1 одразу від 100. Число 99 записується як XCIX (на 10 менше ніж 100, і ще на 1 менше ніж 10).

Перевірка тисяч[ред.]

Що потрібно щоб перевірити що рядок є правильним римським числом? Давайте проаналізуємо це по одній цифрі за раз. Так як римські числа завжди записуються від найбільших до найменших цифр, почнемо з найбільших - тисяч. Для чисел починаючи від значення 1000 і вище, тисячі в них записуються послідовністю символів M.

>>> import re 
>>> pattern = '^M?M?M?$'

Цей шаблон складається з трьох частин. ^ співставляється тільки з тим що знаходиться на початку рядка. Без нього відповідності шаблону знаходились би в довільних місцях рядка, а це не те що вам потрібно. Вам навпаки, потрібно переконатись, що символи M, якщо вони є, знаходяться на початку рядка. M? відповідає або символу M, або відсутності будь-яких символів. Так як воно повторене тричі, то шаблону відповідатиме від нуля до трьох символів M підряд. $ співставляється з кінцем рядка. В поєднанні з символом ^ на початку, він означає, що рядок повинен відповідати шаблону повністю, без жодних невідповідних символів на початку чи в кінці.

>>> re.search(pattern, 'M') 
<_sre.SRE_Match object at 0106FB58>

Основою модуля re є функція search(), яка бере регулярний вираз (pattern), і рядок ('M'), і намагається знайти відповідності. Якщо знаходить, то повертає об’єкт який містить методи що описують знайдену відповідність, інакше повертає None. Нас на поки що цікавить лише наявність чи відсутність співпадіння, а це можна вияснити лише просто за значенням яке повертається. 'M' відповідає шаблону, тому що співпадає з першим символом M в шаблоні, а решта (як і він сам) позначені як не обов’язкові.

>>> re.search(pattern, 'MM') 
<_sre.SRE_Match object at 0106C290>

'MM' теж відповідає шаблону, бо співставляється першим двом M?M?, а третє не враховується.

>>> re.search(pattern, 'MMM') 
<_sre.SRE_Match object at 0106AA38>

'МММ' відповідає всім трьом символам шаблону.

>>> re.search(pattern, 'MMMM')

'MMMM' не відповідає шаблону. Перші три символи M утворюють відповідність з шаблоном, але далі регулярний вираз вимагає закінчення рядка (через символ $), а так як рядок ще не закінчився (є четверте M), то повертається None.

>>> re.search(pattern, '') 
<_sre.SRE_Match object at 0106F4A8>

Що цікаво, порожній рядок теж відповідає шаблону, бо всі три символи в ньому необов’язкові.

Перевірка сотень[ред.]

? означає що входження шаблону перед ним необов’язкове

Сотні перевіряти дещо складніше ніж тисячі, тому що існує кілька взаємновиключних способів їх запису залежно від значення.

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

Тому, існує чотири можливі шаблони:

  • CM
  • CD
  • Від нуля до трьох символів C
  • D після якого іде від нуля до трьох символів C

Останні два можна поєднати:

  • Необов’язкове D після якого іде від нуля до трьох символів C

Наступний приклад покаже як перевірити сотні в римських числах.

>>> import re 
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'

Цей шаблон починається як попередній, символом відповідності початку рядка ^, і перевіркою тисяч M?M?M?. Далі в дужках іде нова частина, яка описує набір трьох взаємновиключних шаблонів розділених вертикальними рисками: CM, CD, та D?C?C?C?. Парсер регулярних виразів перевіряє ці три шаблони по порядку, бере перший, який містить відповідність рядку, і ігнорує всі інші.

>>> re.search(pattern, 'MCM') 
<_sre.SRE_Match object at 01070390>

'MCM' відповідає шаблону, тому що перше M відповідає, друге і третє необов’язкові, і CM відповідає (тому частини CD і D?C?C?C? навіть не беруться до розгляду). MCM це римський запис числа 1900.

>>> re.search(pattern, 'MD') 
<_sre.SRE_Match object at 01073A50>

'MD' містить відповідність, тому що перше M відповідає рядку, друге і третє ігноруються, а шаблону D?C?C?C? відповідає символу D (три символи C необов’язкові і ігноруються). MD - це римський запис числа 1500.

>>> re.search(pattern, 'MMMCCC') 
<_sre.SRE_Match object at 010748A8>

'MMMCCC' відповідає шаблону, тому що всі три M знаходять відповідність, і шаблон D?C?C?C? відповідає CCC (D необов’язкове і ігнорується). MMMCCC - римський запис числа 3300.

>>> re.search(pattern, 'MCMC')

'MCMC' не відповідає шаблону. Перше M знаходить відповідність, два наступні ігноруються, потім знаходиться відповідність шаблону CM, проте далі іде символ $ який відповідає кінцю рядка, але ми ще не на кінці рядка (є ще один символ C, який не має відповідності). Він не співставляється шаблону D?C?C?C?, тому що вже був використаний взаємновиключний шаблон CM.

>>> re.search(pattern, '') 
<_sre.SRE_Match object at 01071D98>

Що цікаво, порожній рядок все ще відповідає нашому шаблону, бо всі символи M необов’язкові, а порожній рядок відповідає шаблону D?C?C?C? де всі символи теж необов’язкові.

Хух! Бачите як швидко регулярні вирази стають паскудними? А ми розглянули тільки тисячі і сотні в римських числах. Щоправда якщо ви з цим розібрались, то розібратись з десятками і одиницями буде просто, бо вони працюють за такою ж схемою. Але давайте розглянемо інший спосіб запису шаблону.


* * *


Використання синтаксису {n,m}[ред.]

{1,4} відповідає від 1 до 4 повторень шаблону що знаходиться перед ним

В попередньому розділі ми розглядали шаблони де символам дозволялось входити в рядок від нуля до трьох разів. Існує інший спосіб запису такого шаблону, який вважається більш читабельним. Але давайте спершу глянемо на метод яким ми користувались в попередньому розділі.

>>> pattern = '^M?M?M?$'

Цей шаблон означає що спочатку рядка може стояти M, а може і не обов’язково, і так само необов’язково можуть стояти ще дві букви M, і далі рядок повинен закінчитись.

Альтернативний спосіб записати цей же шаблон виглядає так:

pattern = '^M{0,3}$'

Він означає: на початку рядка має стояти від 0 до 3 символів M, і потім рядок закінчується. Замість 0 і 3 можна ставити будь-які інші числа.

Перевірка десятків та одиниць[ред.]

Спочатку додамо перевірку десятків:

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$' 
>>> re.search(pattern, 'MCMXL')
<_sre.SRE_Match object at 0x008EEB48>

Спробую проілюструвати як відбувається зіставлення шаблона з рядком 'MCMXL' дещо іншим способом ніж в оригінальній книжці. Бо при всій повазі до автора оригіналу, він пише багато слів, але з них важко видобувати сенс. Думаю так як напишу я буде наочніше.

'^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'

>>> re.search(pattern, 'MCML')
<_sre.SRE_Match object at 0x008EEB48>

'^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'

>>> re.search(pattern, 'MCMLX')
<_sre.SRE_Match object at 0x008EEB48>

'^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'

>>> re.search(pattern, 'MCMLXXX')
<_sre.SRE_Match object at 0x008EEB48>

'^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'

>>> re.search(pattern, 'MCMLXXXX') 
>>>

'^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'

Цей рядок не відповідає шаблону, тому що шаблон очікує кінець рядка (виділено червоним), а рядок натомість містить ще один X

(A|B) відповідає або шаблону A або шаблону B, але не обидвом водночас.

Вираз для одиниць працює так само. Я опущу деталі, і покажу одразу кінцевий результат:

>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Як би він виглядав, якби ми використали новий синтаксис з фігурними дужками? Так як в прикладі нижче.

>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$' 
>>> re.search(pattern, 'MDLV')
<_sre.SRE_Match object at 0x008EEB48>

'^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'

>>> re.search(pattern, 'MMDCLXVI')
<_sre.SRE_Match object at 0x008EEB48>

'^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'

>>> re.search(pattern, 'MMMDCCCLXXXVIII')
<_sre.SRE_Match object at 0x008EEB48>

'^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'

>>> re.search(pattern, 'I')
<_sre.SRE_Match object at 0x008EEB48>

'^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'

Якщо ви читаючи це зрозуміли кожен вираз з першого разу - ви розумніші за автора цієї книжки. Але регулярні вирази можуть бути ще заплутанішими, і інколи зрозуміти їх призначення в програмі дуже важко.

Тому давайте розглянемо інший спосіб запису регулярних виразів, який спрощує їх прочитання іншими людьми.


* * *


Багатослівні регулярні вирази[ред.]

Дотепер ми мали справу лише з тим, що я називаю "компактними регулярними виразами". Ви вже могли помітити що їх не просто читати, і навіть якщо ви розумієте як вони працюють, це ще не гарантує що ви зможете зробити це через місяці після їх написання. Тому нам потрібна вбудована документація.

Python дозволяє зробити це за допомогою багатослівних регулярних виразів (verbose regular expressions). Вони відрізняються від звичайних двома особливостями:

  • Розділювальні символи ігноруються. Прогалики, табуляції, символи нового рядка в шаблоні не відповідають прогаликам, табуляціям і символам нового рядка в тексті що порівнюється з шаблоном. Вони взагалі не співставляються з жодними символами в тексті. (Якщо вам потрібний прогалик в багатослівному регулярному виразі, доведеться його екранувати поставивши перед ним зворотній слеш.)
  • Коментарі ігноруються. Коментарі в багатослівних регулярних виразах такі самі як коментарі в коді: це все що знаходиться між символом # і кінцем рядка. В даному випадку це коментарі всередині багаторядкового літералу, а не всередині вашого коду, але принцип їх роботи залишається таким самим.

Спробую пояснити все на прикладі. Давайте повернемось до компактного регулярного виразу з яким ми працювали, і спробуємо зробити його багатослівним. Ось так:

>>> pattern = '''
    ^                   # початок рядка
    M{0,3}              # тисячі - від 0 до 3 M
    (CM|CD|D?C{0,3})    # сотні - 900 (CM), 400 (CD), 0-300 (від 0 до 3 C),
                        #            або 500-800 (D, після якого від 0 до 3 C)
    (XC|XL|L?X{0,3})    # десятки - 90 (XC), 40 (XL), 0-30 (від 0 до 3 X),
                        #        або 50-80 (L, після якого від 0 до 3 X)
    (IX|IV|V?I{0,3})    # одиниці - 9 (IX), 4 (IV), 0-3 (I{0,3}),
                        #        або 5-8 (VI{0-3})
    $                   # кінець рядка
    '''
>>> re.search(pattern, 'M', re.VERBOSE) 
<_sre.SRE_Match object at 0x008EEB48>

Найважливіше про що треба пам’ятати при роботі з багатослівними регулярними виразами - що функціям треба передавати додатковий аргумент: re.VERBOSE. Це константа описана в модулі re для позначення того що регулярний вираз який ми передаємо функції - багатослівний. Як ви можете бачити, такий регулярний вираз містить багато розділювальних символів і коментарів що ігноруються. А якщо видалити всі розділювачі та коментар, це буде точно такий самий регулярний вираз, як ми бачили в попередньому параграфі.

>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE) 
<_sre.SRE_Match object at 0x008EEB48>
   
   ^                   # початок рядка
   M{0,3}              # тисячі - від 0 до 3 M
   (CM|CD|D?C{0,3})    # сотні - 900 (CM), 400 (CD), 0-300 (від 0 до 3 C),
                       #            або 500-800 (D, після якого від 0 до 3 C)
   (XC|XL|L?X{0,3})    # десятки - 90 (XC), 40 (XL), 0-30 (від 0 до 3 X),
                       #        або 50-80 (L, після якого від 0 до 3 X)
   (IX|IV|V?I{0,3})    # одиниці - 9 (IX), 4 (IV), 0-3 (I{0,3}),
                       #        або 5-8 (VI{0-3})
   $                   # кінець рядка
   
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE) 
<_sre.SRE_Match object at 0x008EEB48>
   
   ^                   # початок рядка
   M{0,3}              # тисячі - від 0 до 3 M
   (CM|CD|D?C{0,3})    # сотні - 900 (CM), 400 (CD), 0-300 (від 0 до 3 C),
                       #            або 500-800 (D, після якого від 0 до 3 C)
   (XC|XL|L?X{0,3})    # десятки - 90 (XC), 40 (XL), 0-30 (від 0 до 3 X),
                       #        або 50-80 (L, після якого від 0 до 3 X)
   (IX|IV|V?I{0,3})    # одиниці - 9 (IX), 4 (IV), 0-3 (I{0,3}),
                       #        або 5-8 (VI{0-3})
   $                   # кінець рядка
   

MMMDCCCLXXXVIII - римський запис числа 3888, і найдовше римське число яке можна записати не використовуючи розширений синтаксис.

>>> re.search(pattern, 'M') 
>>>

А цей рядок не співставляється з шаблоном. Чому? Бо ми не передали йому параметр re.VERBOSE, тому функція re.search розглядає шаблон як компактний регулярний вираз, з значущими розділювачами, і буквальними символами дієзів. Python не вміє автоматично визначати чи є регулярний вираз багатослівним, тому вважає кожен регулярний вираз компактним, якщо ви явно не вкажете протилежне.


* * *


Приклад: аналіз телефонних номерів[ред.]

\d відповідає будь-якій цифрі (0–9). \D відповідає будь-чому окрім цифр.

Поки що ми зосереджувались лише на повному співставленні шаблонів. Або рядок відповідає шаблону або ні. Але регулярні вирази мають набагато більше можливостей. Коли рядок відповідає шаблону, ми можемо виділяти певні його частини, і визначати що саме співпало.

Цей приклад з’явився з іншої реальної задачі, яку мені довелось розв’язувати на попередній роботі. Задача: аналіз американських телефонних номерів. Клієнт хоче мати можливість вводити номер в будь-якій формі (в єдиному полі), але хоче мати можливість зберігати код регіону, код міста, номер і необов’язковий додаток в базі даних окремо. Я обнишпорив веб і знайшов багато прикладів регулярних виразів створених з цією метою, але жоден з них не був достатньо гнучким.

Ось номери які я повинен був розбирати:

  • 800-555-1212
  • 800 555 1212
  • 800.555.1212
  • (800) 555-1212
  • 1-800-555-1212
  • 800-555-1212-1234
  • 800-555-1212x1234
  • 800-555-1212 ext. 1234
  • work 1-(800) 555.1212 #1234

Досить різні! В кожному з цих випадків мені потрібно визначити що код регіону - 800, код міста 555, а решта номера - 1212. У випадках наявності додатку мені потрібно знати, що додаток - 1234.

Що ж, давайте почнемо розробку методу аналізу телефонних номерів. Наступний приклад демонструє перший крок:

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')

Регулярні вирази варто читати зліва направо. Цей регулярний вираз починає співставлення з початку рядка, а потім шукає шаблон (\d{3}). Що таке \d{3}? Ну, \d означає "будь-яка десяткова цифра" (від 0 до 9). {3} означає "застосувати попередній шаблон рівно три рази". Це інший вид синтаксису {n,m} який ви бачили раніше. Поміщення шаблону в дужки, означає "запам’ятати цей шматочок як групу, яку я можу отримати пізніше". Потім іде співставлення з дефісом. Потім співставлення з іншою групою з рівно трьох цифр. Потім знову дефіс. Після цього регулярний вираз співставлятиметься з групою з рівно чотирма цифрами, і далі буде вимагатись кінець рядка.

>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212')

Щоб отримати доступ до груп які парсер регулярних виразів запам’ятав під час роботи, використовуйте метод groups() об’єкта який повертається методом search(). Він поверне кортеж з стількох груп, скільки їх було задано в регулярному виразі. В нашому випадку задано три групи - дві з трьома цифрами, і одна з чотирма.

>>> phonePattern.search('800-555-1212-1234')

Цей регулярний вираз не є кінцевим, бо як ми бачимо, він не розпізнає телефони з розширенням. Тому нам доведеться його доповнити.

>>> phonePattern.search('800-555-1212-1234').groups()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module> 
AttributeError: 'NoneType' object has no attribute 'groups'

І це є причиною, через яку вам не рекомендується створювати в коді ланцюжки з методів search() та groups(). Якщо метод search() не поверне жодних співпадінь, він поверне None, який не є об’єктом що описує співпадіння з регулярним виразом. Виклик None.groups() поверне цілком очевидне виключення: None не містить методу groups(). (Звичайно, це набагато менш очевидно, коли ви отримуєте таке виключення десь з глибини свого коду. Тут я говорю зі свого досвіду.)

>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')

Цей регулярний вираз майже ідентичний попередньому, за винятком одного. Як і раніше, він співставляється з початком рядка, потім група трьох цифр, дефіс, ще одна група з трьох, дефіс і група з чотирьох цифр. Новим тут є ще один дефіс і ще одна група з чотирьох цифр.

>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')

Оскільки тепер регулярний вираз містить чотири групи, метод groups() повертає кортеж з чотирьох елементів.

>>> phonePattern.search('800 555 1212 1234')
>>>

Та, на жаль, цей регулярний вираз все ще не є кінцевим, бо він припускає що цифри в телефонному номері розділені дефісами. А що якщо їх будуть розділяти прогаликами, комами чи крапками? Нам потрібне більш загальне рішення для співставляння з кількома типами розділювачів.

>>> phonePattern.search('800-555-1212')
>>>

Уупс! Цей регулярний вираз крім того, ще й не розпізнає ті номери, які розпізнавав попередній, а це не те чого ми хочемо. Якщо телефон має розширення, ми хочемо знати яке воно, але, якщо його нема, ми все одно хочемо отримати інші частини номера.

Наступний приклад демонструє регулярний вираз, здатний справитись із розділювачами між різними частинами номера.

>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')

А зараз приготуйтесь. Цей регулярний вираз співставляється з початком рядка, потім групою з трьох цифр, потім \D+. Це ще що за чортівня? Ну, \D відповідає будь-якому символу окрім десяткової цифри, а + означає "один чи більше разів". Тому, \D+ співставляється з одним чи більше символами, які не є цифрами. Його ми поставили замість дефіса, щоб розпізнавати найрізноманітніші форми розділювачів.

>>> phonePattern.search('800 555 1212 1234').groups()
('800', '555', '1212', '1234')

Застосування \D+ замість дефіса означає що ви тепер можете розпізнавати номери з прогаликами замість дефісів.

>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')

Звісно, дефіси також все ще працюють.

>>> phonePattern.search('80055512121234')
>>>

На жаль, це все ще не кінцева відповідь, бо вона припускає, що повинен бути хоч якийсь розділювач. А що, якщо телефонний номер введено взагалі без прогаликів чи дефісів?

>>> phonePattern.search('800-555-1212')
>>>

Уупс! Це також все ще не розв’язує проблему з необов’язковістю розширення. Тепер в нас є дві проблеми, але ми можемо розв’язати кожну з них за допомогою одної техніки.

Наступний приклад демонструє регулярний вираз для обробки телефонних номерів без розділювачів.

>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')

Єдине що ми робимо на цьому кроці - замінюємо всі + на *. Замість \D+ між частинами телефонного номера знаходиться \D*. Пам’ятаєте що + означає "один чи більше"? Так от, * означає "нуль чи більше". Тому зараз ми повинні б розпізнавати навіть телефонні номери, в яких взагалі немає розділювальних символів.

>>> phonePattern.search('80055512121234').groups()
('800', '555', '1212', '1234')

Погляньте-но, воно справді працює. Чому? Регулярний вираз співставляється з початком рядка, потім з групою з трьох цифр (800), потім з нулем нецифрових символів, потім з групою з трьох цифр (555), потім з нулем нецифрових символів, потім з групою з чотирьох цифр (1212), потім з нулем нецифрових символів, потім з групою довільної кількості цифр (1234), і нарешті з кінцем рядка.

>>> phonePattern.search('800.555.1212 x1234').groups()
('800', '555', '1212', '1234')

Інші варіації тепер теж проходять: крапки замість дефісів і прогалик з символом x перед розширенням.

>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')

Ну, і нарешті, ми розв’язали іншу проблему, яка діставала нас довгий час: розширення знову необов’язкове. Якщо розширення номера не знайдене, метод groups() все ще повертає кортеж з чотирьох елементів, але четвертий елемент є просто порожнім рядком.

>>> phonePattern.search('(800)5551212 x1234')
>>>

Ненавиджу приносити погані новини, але ми все ще не закінчили. І яка проблема цього разу? Перед кодом регіону можуть бути додаткові символи, але регулярний вираз припускає, що код регіону - перше, що стоїть на початку рядка. Жодних проблем, ви можете використовувати ту ж техніку "нуль, чи більше нецифрових символів", щоб пропустити початкові символи перед кодом регіону.


Наступний приклад продемонструє як можна справитись з символами перед кодом регіону.

>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')

Регулярний вираз аналогічний виразу в попередньому прикладі, тільки тепер ми намагаємось знайти відповідність шаблону \D* (нуль чи більше нецифрових символів) перед першою групою що запам’ятовується (код регіону). Зауважте, що регулярний вираз не запам’ятовує ці нецифрові символи (бо вони не в дужках). Якщо вони зустрічаються він просто їх пропускає і починає запам’ятовувати групу, як тільки до неї добереться.

>>> phonePattern.search('(800)5551212 ext. 1234').groups()
('800', '555', '1212', '1234')

Тепер ми можемо розпізнавати код регіону, навіть якщо йому передує відкриваюча дужка. (Закриваюча дужка оброблялась і раніше, тому що вона відповідає шаблону "будь-яка кількість нецифрових символів".

>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')

Просто перевірка щоб переконатись, що ми не поламали нічого, що працювало раніше. Оскільки перші символи є необов’язковими, шаблон співставляється з рядком так:

^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$
>>> phonePattern.search('work 1-(800) 555.1212 #1234')
>>>

А ось момент, коли через регулярні вирази мені хочеться виколупати собі очі тупим предметом. Чому цей номер не розпізнається? Тому що перед кодом регіону стоїть цифра 1, але ми припустили що всі символи перед кодом регіону будуть нецифровими (\D*). Агррр!

Давайте на секунду повернемось назад. Досі всі регулярні вирази починали співставлятись з початку рядка. Але зараз ви бачите, що на початку рядка може бути невизначена кількість символів, які ми хочемо проігнорувати. Давайте замість того щоб придумати шаблон який відповідатиме тим символам застосуємо інший підхід: просто перестанемо вимагати, аби співставлення відбувалось з початку рядка. Такий підхід демонструється в наступному прикладі:

>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')

Зверніть увагу на відсутність символа ^ на початку шаблону. Ми більше не вимагаємо починати співставлення з початку рядка. Регулярний вираз добре попрацює над тим, аби вияснити звідки починається потрібна нам частина рядка, і почне співставлення звідти.

>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')

Тепер ви можете успішно парсити телефонний номер що включає довільну кількість довільних символів на початку.

>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234').groups()
('800', '555', '1212', '1234')

І ніщо з старого функціоналу не повинно було поламатись. Ми перевірили - працює.

Бачите, як швидко регулярні вирази виходять з під контролю? Прогляньте попередні етапи створення нашого шаблону. Ви можете описати різницю між двома послідовними етапами?

І хоча ми поки що розуміємо наш кінцевий регулярний вираз (і він таки є кінцевим, якщо ви знайдете випадок, для якого він працює неправильно, я не хочу про нього знати), давайте запишемо регулярний вираз з коментарями, перш ніж ви забудете, чому ви прийняли ті рішення, які ви прийняли.

>>> phonePattern = re.compile(r'''
                # не вимагати початок рядка, номер може початись будь-де
    (\d{3})     # код регіону містить три цифри (наприклад '800')
    \D*         # необов’язковий розділювач - довільна кількість нецифрових символів
    (\d{3})     # ще три цифри (наприклад '555')
    \D*         # необов’язковий розділювач
    (\d{4})     # основна частина номера - чотири цифри (наприклад '1212')
    \D*         # необов’язковий розділювач
    (\d*)       # розширення необов’язкове і може містити будь-яку кількість символів
    $           # кінець рядка
    ''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')

Окрім того що регулярний вираз тепер розтягнувся на багато рядків, він нічим не відрізняється від попереднього, тому не дивно, що він поводиться аналогічно.

>>> phonePattern.search('800-555-1212')
('800', '555', '1212', '')

На всяк випадок, ще одна перевірка. Все працює. Ми завершили. Ура!


* * *


Підсумок[ред.]

Це тільки вершина айсберга того, що можуть робити регулярні вирази. Іншими словами: незважаючи на те, що я вас ними щойно сильно загрузив, повірте, ви ще нічого не бачили.

Після прочитання цього розділу ви повинні знати наступне:

  • ^ відповідає початку рядка.
  • $ відповідає кінцю рядка.
  • \b відповідає межі слова.
  • \d відповідає будь-якій десятковій цифрі.
  • \D відповідає будь-чому крім цифр.
  • x? відповідає необов’язковому символу x (іншими словами x нуль чи один раз).
  • x* відповідає нулю чи більше символів x.
  • x+ відповідає одному чи більше символів x.
  • x{n,m} відповідає від n до m включно входжень символа x в рядок.
  • (a|b|c) відповідає одному з виразів a, b чи c.
  • (x) загалом створює запам’ятовувану групу. Пізніше можна отримати рядок, який співставився з шаблоном в групі за допомогою методу groups() об’єкта, який повертається з re.search.

Регулярні вирази є надзвичайно потужними, але вони не є коректним рішенням будь-якої проблеми. Вам потрібно вивчити про них достатньо для того, щоб знати коли їх використання окупається, і вони розв’язують ваші проблеми, і коли вони навпаки створюють більше проблем ніж вирішують.

Для подальшого читання[ред.]

Замикання та генератори[ред.]

Мій правопис якийсь кульгавий. Узагалі він гарний правопис, тільки от чогось трохи накульгує, і букви заскакують не на свої місця...
Вінні Пух


Вирісши як син бібліотекаря та вчителя англійської, я завжди був зачарований мовами. Не мовами програмування. Ну, так, і мовами програмування, але і природніми мовами теж. Візьмімо наприклад англійську. Англійська - це шизофренічна мова яка запозичує слова з німецької, французької, іспанської та латинської (і це не всі). Насправді "позичила" - не те слово, "награбувала" підходить більше. Чи можливо "асимілювала" - як Борґ. Так, мені подобається ця аналогія.

Ми Борґ. Ваша лінгвістична та етимологічна відмінність буде додана до нашої. Опір марний.

В цьому розділі ми вивчимо множину іменників в англійській мові. А також, функції що повертають інші функції, генератори та поглибимо знання регулярних виразів. Але спершу, давайте поговоримо про те як створювати множину іменників. (Якщо ви ще не прочитали розділ про Регулярні вирази зараз саме пора. Цей розділ передбачає знання основ регулярних виразів, і швидко переходить до деяких поглиблених речей.)

Якщо ви вчили англійську в школі, ви напевне знайомі з основними правилами:

  • Якщо слово закінчується на S, X, чи Z, потрібно додати ES. Bass стає basses, fax стає faxes, а waltz стає waltzes.
  • Якщо слово закінчується на шумну H, додавайте ES; а якщо на тиху H, - просто S. Що таке шумна H? Така яка в комбінації з іншими літерами дає звук який можна чути. Отож coach стає coaches а rash стає rashes, тому що ми можемо чути CH та SH при вимові. Але cheetah стає cheetahs, тому що тут H - тиха.
  • Якщо слово закінчується на Y що звучить як I, змініть Y на IES; якщо ж Y комбінується з голосним щоб звучати якщо щось інше - просто додайте S. Тому vacancy стає vacancies, але day стає days.
  • Якщо ж жодне правило не підійшло, просто додайте S та сподівайтесь на краще.

(Я знаю, існує багато винятків. Man стає men і woman стає women, але human стає humans. Mouse стає mice і louse стає lice, але house стає houses. Knife стає knives і wife стає wives, але lowlife стає lowlifes. І навіть не дайте мені розпочати про слова що є одночасно в множині і однині, як наприклад sheep, deer, та haiku.)

Інші мови, звичайно, цілком інші.

Давайте створимо бібліотеку мови Python, яка буде автоматично створювати множину іменників в англійській мові. Ми почнемо з вищезгаданих чотирьох правил, але пам'ятайте що неминуче доведеться додати більше.


* * *


Я знаю, давайте використаємо регулярні вирази![ред.]

Отож, коли ви дивитесь на слова, принаймі в англійській мові це означає що ви дивитесь на послідовності символів. У нас є правила які кажуть що потрібно знаходити певні комбінації символів, а потім робити з ними різні речі. Це звучить як робота для регулярних виразів!


import re

def plural(noun):          
    if re.search('[sxz]$', noun):

Це регулярний вираз, але він використовує синтаксис який ви не побачите в регулярних виразах. Квадратні дужки означають "співставити з одним з перелічених символів". Отож, [sxz] означає "s, або x, або z", але лише один з них. $ повинен бути знайомим - він співставляється з кінцем рядка. Разом вони перевіряють чи рядок noun закінчується на s, x, або z.

        return re.sub('$', 'es', noun)
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)      
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)    
    else:
        return noun + 's'

Функція re.sub() виконує заміну рядків на основі регулярних виразів.


Давайте поглянемо на заміну з регулярними виразами трохи детальніше.

>>> import re
>>> re.search('[abc]', 'Mark')
<_sre.SRE_Match object at 0x001C1FA8>

Рядок 'Mark' містить a, b чи c.

>>> re.sub('[abc]', 'o', 'Mark')
'Mork'

Ок, тепер давайте знайдемо a, b чи c і замінимо на o. Mark перетворюється на Mork.

>>> re.sub('[abc]', 'o', 'rock')
'rook'

Ця ж функція замінює rock на rook.

>>> re.sub('[abc]', 'o', 'caps')
'oops'

Можна було б подумати що вона перетворить caps в oaps, але ні re.sub замінює всі співпадіння, а не тільки перше. Тому цей вираз перетворює caps в oops, тому що і c і a відповідають шаблону і замінюються на o.


А тепер повернімось назад до функції plural()...

def plural(noun):          
    if re.search('[sxz]$', noun):            
        return re.sub('$', 'es', noun)

Тут ми замінюємо кінець рядка (шаблон $) рядком es. Іншими словами, додаємо es до рядка. Можна досягти цього ж, за допомогою конкатенації рядків, наприклад noun + 'es', але я вирішив використати регулярні вирази, з причин які стануть зрозумілими пізніше в розділі.

    elif re.search('[^aeioudgkprt]h$', noun):

Уважно придивіться до цього нового регулярного виразу. Знак ^ розміщений першим всередині квадратних дужок означає дещо особливе: доповнення. [^abc] означає "всі символи крім a, b, чи ". Тому [^aeioudgkprt] означає будь-який символ, окрім a, e, i, o, u, d, g, k, p, r, t. Після одного з таких символів повинна йти h, за якою рядок закінчується. Ми шукаємо слова що закінчуються на H, і вона вимовляється вголос.

        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):

Аналогічно тут: знайти слова що закінчуються на Y, де символ перед Y не є a, e, i, o, чи u. Шукаємо слова що закінчуються на Y, яке звучить як I.

        return re.sub('y$', 'ies', noun)    
    else:
        return noun + 's'

Давайте розглянемо такі регулярні вирази більш детально.

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')
<_sre.SRE_Match object at 0x001C1FA8>

vacancy розпізнається цим регулярним виразом тому що закінчується на cy, а c не a, не e, i, o, чи u.

>>> re.search('[^aeiou]y$', 'boy')
>>>
>>> re.search('[^aeiou]y$', 'day')
>>>

boy не розпізнається, тому що закінчується на oy, а ми спеціально наголосили що перед y не повинно йти o. day не розпізнається тому що закінчується на ay.

>>> re.search('[^aeiou]y$', 'pita') >>>

pita не розпізнається тому що взагалі не закінчується на y.

>>> re.sub('y$', 'ies', 'vacancy')
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'

Цей регулярний вираз перетворює vacancy в vacancies і agency в agencies, а це якраз те що нам потрібно. Правда варто зауважити що він також перетворить boy на boies, але цього не станеться в функції тому що ми спочатку виконали re.search щоб з’ясувати чи взагалі потрібно викликати re.sub.

>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')
'vacancies'

Побічно хочеться вказати на те що можна об’єднати два регулярні вирази (один що перевіряє чи можна застосовувати правило і інший що застосовує правило) в єдиний регулярний вираз. Його можна побачити в коді вище. Більша його частина повинна б бути знайомою, він використовує запам’ятовувану групу, яку ми вивчили в прикладі з аналізом телефонних номерів. Ця група використовується для того щоб запам’ятати символ перед y. Після цього в рядку що підставляється ми використовуємо синтаксис \1, який означає "поклади першу групу яку ми запам’ятали прямо тут". В даному випадку перед y ми запам’ятали c, і коли робиться заміна на місці c так і залишається c, а замість y ставиться ies. (Якщо в нас є більше одної запам’ятовуваної групи, можна використати \2, \3 і так далі.)

Заміни з допомогою регулярних виразів досить гнучкі, і синтаксис \1 робить їх ще гнучкішими. Але об’єднання всієї операції в один регулярний вираз робить його важчим для читання, і не відповідає прямо способу нашого опису правил утворення множини. Ми з самого початку описали правила на зразок "Якщо слово закінчується на S, X, чи Z, потрібно додати ES." А якщо ми подивимось на нашу функцію то ми побачимо два рядки коду в яких написано "Якщо слово закінчується на S, X, чи Z, потрібно додати ES." Це важко виразити більш прямо.


* * *


Список функцій[ред.]

Зараз ви додасте рівень абстракції. Ви почали з опису списку правил: якщо це, зроби те, інакше переходь до наступного правила. Давайте тимчасово ускладнимо частину програми щоб зробити іншу частину простішою.

import re

def match_sxz(noun):
    return re.search('[sxz]$', noun)

def apply_sxz(noun):
    return re.sub('$', 'es', noun)

def match_h(noun):
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):
    return re.sub('$', 'es', noun)

def match_y(noun):
    return re.search('[^aeiou]y$', noun)
        
def apply_y(noun):
    return re.sub('y$', 'ies', noun)

def match_default(noun):
    return True

def apply_default(noun):
    return noun + 's'

Тепер кожен регулярний вираз що визначає чи можна застосовувати правило є окремою функцією що викликає метод re.search().

Кожне застосування правила теж окрема функція яка викликає re.sub() для виконання перетворення.

rules = ((match_sxz, apply_sxz),
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

Замість однієї функції (plural()) з багатьма правилами, ми маємо структуру даних з цими правилами, яка є просто послідовністю пар функцій.

def plural(noun):
    for matches_rule, apply_rule in rules:
        if matches_rule(noun):
            return apply_rule(noun)

Так як правила були винесені в окрему структуру даних, нова функція plural() зменшується аж до кількох рядків коду. Використовуючи цикл for можна вибирати з нашої структури по парі правил за раз (одне для визначення можливості застосування, інше для заміни). На першій ітерації циклу, matches_rule отримає значення match_sxz, а apply_rule отримає apply_sxz. На другій ітерації (якщо припустити що до неї дійде), matches_rule отримає значення match_h, а apply_rule - apply_h. Функція обов’язково колись щось поверне, тому що останнє правило (match_default) просто повертає True, що означає що відповідне правило apply_default обов’язково застосується.

Змінна rules є послідовністю пар функцій

Такий підхід працює тому що все в Python - об’єкти, включно з функціями. Структура даних rules місить функції - не імена функцій, а самі функції. Коли відбувається ітерація циклу for - змінні matches_rule та apply_rule стають функціями які можна викликати. На першій ітерації циклу це евівалентно виклику matches_sxz(noun), і якщо він поверне співпадіння, виклику apply_sxz(noun).

Якщо додатковий рівень абстракції вас заплутав, спробуйте розгорнути функцію, щоб побачити еквівалентність. Ввесь цикл for еквівалентний наступному

def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)

Перевагою тут є те, що функція plural() тепер спрощується. Вона бере послідовність правил що описані деінде, і ітерує по них узагальнено:

  1. Візьми наступне правило.
  2. Правило можна застосовувати? Тоді застосуй, і поверни результат.
  3. Не застосовується? Повернись до першого кроку.

Правила можна описувати будь-де. Функції plural() все одно.

Чи додавання додаткового рівня абстракції було того варте? Ну, поки що ні. Давайте подумаємо що потрібно щоб додати нове правило до функції. В першому прикладі це вимагатиме додавання оператора if в функцію plural(). А зараз це вимагатиме додавання двох функцій, і оновлення послідовності rules для того щоб визначити коли відносно інших функцій нові будуть викликатись.

Але це лише сходинка до наступного параграфу. Продовжуємо...


* * *


Список шаблонів[ред.]

Опис окремих іменованих функцій для кожного правила не обов’язковий, тому що ви ніколи не викликаєте їх напряму, а додаєте в послідовність правил, і викликаєте з цієї послідовності. Більше того, всі функції дуже подібні. Всі функції що визначають чи застосовувати правило викликають re.search(), а всі функції що застосовують правило викликають re.sub(). Давайте виокремимо регулярні вирази аби створення нових правил було простішим.


import re

def build_match_and_apply_functions(pattern, search, replace):
    def matches_rule(word):
        return re.search(pattern, word)

build_match_and_apply_functions() - функція що будує інші функції динамічно. Вона приймає параметри pattern, search та replace, а потім описує функцію matches_rule() яка викликає re.search() з шаблоном що був переданий функції параметром pattern, і word - параметром функції matches_rule() яку ми конструюємо.

    def apply_rule(word):
        return re.sub(search, replace, word)

Функція що застосовує правило будується аналогічно. Вона приймає один параметр, і викликає re.sub() з параметрами search та replace які передаються функції build_match_and_apply_functions(), та параметром функції apply_rule(), яку ми конструююємо - word. Підхід з використанням значень параметрів зовнішньої функції називається замиканням (англ. closure). По суті, ми описуємо константи всередині функції що будується: вона приймає один параметр, проте використовує додаткові два, значення яких задається при описі функції.

    return (matches_rule, apply_rule)

Ну, і насамкінець, функція build_match_and_apply_functions() повертає пару значень: дві функції які щойно були створені. Константи описані всередині цих функцій (pattern в функції matches_rule() та, search і replace в функції apply_rule()) залишаються разом з функціями навіть після того, як їх повернули назовні функції build_match_and_apply_functions(). Це неймовірно круто.


Якщо це вас заплутало (а воно й повинно було, бо це дуже дивні речі), можливо стане зрозуміліше, коли ви побачите як це використовувати.


patterns = \
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),

Наші "правила" утворення множини тепер задаються як кортеж кортежів рядків (не функцій). Перший рядок в кожній групі - це регулярний вираз який буде використовуватись в re.search() для визначення того чи можна застосовувати правило. Другий і третій рядок в кожній групі - регулярні вирази пошуку й заміни що будуть використовуватись в re.sub() для переведення іменника в множину.

    ('$',                '$',  's')
  )

А тут, в останньому правилі є маленька зміна. В попередньому прикладі, функція match_default() просто повертала True, що означало, що якщо жодне з правил не спрацювало, ми просто додамо s в кінець слова. Цей приклад робить дещо функціонально еквівалентне. Останній регулярний вираз перевіряє чи в слова є кінець ($ співставляється з кінцем рядка). Звичайно, кожен рядок має кінець, навіть порожній, тому пошук такого шаблону завжди дає позитивний результат. Тому, він служить тій же цілі що й функція match_default() яка завжди повертала True: гарантує що якщо жодне з попередніх правил не виконалось, то останнє обов’язково виконається, і додасть s в кінець слова.

rules = [build_match_and_apply_functions(pattern, search, replace)
         for (pattern, search, replace) in patterns]

Це магічний рядок. Він бере послідовність рядків в шаблонах, і перетворює їх в послідовність функцій. Як? Передаючи рядки в функцію build_match_and_apply_functions(). Тобто, бере кожну трійку рядків, і викликає build_match_and_apply_functions() з ними як аргументами. Функція build_match_and_apply_functions() повертає кортеж що містить дві функції. Це означає що змінна rules функціонально еквівалентна попередньому прикладу: список кортежів, в якому кожен кортеж це пара функцій. Перша функція це функція що визначає чи можна застосовувати правило, а друга його застосовує.


Закінчуючи описувати теперішню версію скрипта, поглянемо на функцію plural().

def plural(noun):
    for matches_rule, apply_rule in rules:
        if matches_rule(noun):
            return apply_rule(noun)

Так як список rules такий самий як і в попередньому прикладі (насправді, він точно такий самий), нічого дивного в тому що функція plural() зовсім не змінилась. Вона цілком загальна, приймає список функцій - правил, і викликає їх по порядку. Їй все одно як ці функції були описані. В попередньому прикладі вони описані як окремі іменовані функції. Зараз вони будуються динамічно, з результатів застосування функції build_match_and_apply_functions() до елементів списку що містить шаблони. Але це не має значення, бо функція plural() все одно працює так само як і раніше.


* * *


Файл шаблонів[ред.]

Ви виокремили ввесь код що дублюється, і додали достатньо абстрацій для того щоб правила утворення множини можна було описувати в списку рядків. Наступним логічним кроком було б взяти ці рядки, і винести їх в окремий файл, де їх можна буде редагувати окремо від коду який ними користується.

Спершу, давайте створимо текстовий файл який міститиме потрібні правила. Ніяких вигадливих структур даних, просто рядки розділені пропусками записані в три колонки. Назвемо цей файл plural4-rules.txt.

[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

Тепер давайте подивимось як можна використати цей файл.


import re

def build_match_and_apply_functions(pattern, search, replace):
    def matches_rule(word):
        return re.search(pattern, word)
    def apply_rule(word):
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)

Функція build_match_and_apply_functions() не змінилась. Ви все ще користуєтесь замикання щоб динамічно створювати пари функцій які використовують змінні передані ззовні.

rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file:

Глобальна функція open() відкриває файл, та повертає файловий об'єкт. В даному випадку, файл що відкривається містить вирази для переведення іменників в множину. Оператор with створює те що називають контекстом: коли блок закінчується, Python автоматично закриє файл, навіть якщо всередині блоку був згенерований виняток. Ви дізнаєтесь більше про блок with та файлові об'єкти у розділі Файли.

    for line in pattern_file:

Вираз for line in <fileobject> читає дані з відкритого файлу, по рядочку за раз, і присвоює текст рядка змінній line. Ви дізнаєтесь більше про читання з файлів у розділі Файли.

        pattern, search, replace = line.split(None, 3)

Кожен рядок в файлів насправді містить три значення, але вони відокремлені табуляцією чи пропусками. Щоб їх розділити використайте метод рядка split(). Першим аргументом у split() передається None, що означає "розділяти по будь-якому порожньму місці, немає значення з пробілів чи табуляцій”. Другий аргумент - 3, і він означає "розділювати тричі, а потім облишити решту рядка. Рядок на зразок [sxz]$ $ es буде розбито на список рядків ['[sxz]$', '$', 'es'], що означатиме що змінній pattern присвоять '[sxz]$', search присвоять '$', а replace - 'es'. Ось так багато робить такий короткий рядок коду.

        rules.append(build_match_and_apply_functions(
                pattern, search, replace))

Ну і нарешті ми передаємо змінні pattern, search, та replace в функцію build_match_and_apply_functions() яка повертає кортеж функцій. Ми додаємо цей кортеж в список правил, і цей список в кінцевому результаті rules містять список функцій який очікує функція plural().


Покращення тут полягає в тому що ми повністю виокремили правила в зовнішній файл, і він може розроблятись окремо від коду що його використовує. Код це код, дані це дані, і життя чудове.


* * *


Генератори[ред.]

Хіба б не було чудово мати загальну функцію plural() яка парсить файл правила? Отримати правила, перевірити на співпадіння, застосувати відповідне перетворення, перейти до наступного правила. Це все що що функція plural() повинна б робити, і це все що вона буде робити.

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)
            yield build_match_and_apply_functions(pattern, search, replace)

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))

Як вбіса це працює? Давайте спершу розглянемо інтерактивний приклад.


>>> def make_counter(x):
...     print('entering make_counter') 
...     while True: 
...         yield x 
...         print('incrementing x') 
...         x = x + 1 
...

The presence of the yield keyword in make_counter means that this is not a normal function. It is a special kind of function which generates values one at a time. You can think of it as a resumable function. Calling it will return a generator that can be used to generate successive values of x.

Присутність ключового слова yield в make_counter означає що ця функція незвичайна. Це особливий вид функції яка генерує значення по одному за раз. Можете думати про неї як про повторювану функцію. Її виклик повертає генератор який можна використовувати щоб згенерувати послідовні значення x.

>>> counter = make_counter(2)

Щоб створити екземпляр генератора make_counter просто викличте його як звичайну функцію. Зауважте що це насправді не виконує код функції. Ми знаємо про це тому що в першому рядку функції make_counter() ми викликаємо print(), але нічого не надрукувалось.

>>> counter
<generator object at 0x001C9C10>

Функція make_counter() повертає генераторний об'єкт.

>>> next(counter)
entering make_counter
2

Функція next() бере генераторний об'єкт і повертає наступне згенероване значення. Першого разу при виклику next() для генератора, він виконує код в make_counter() аж до першої команди yield, потім повертає значення яке було повернено. В нашому випадку це 2, тому що на самому початку ми створили генератор викликавши make_counter(2).

>>> next(counter)
incrementing x
3

Послідовно викликаючи next() з одним і тим самим генераторним об'єктом продовжує виконання з того місця де воно завершилось і продовжується аж поки не зустрінеться з наступним оператором yield. Всі змінні, локальнимй стан і т.п. зберігаються при виконанні yield і відновлюються при виклику next(). Наступний рядок коду що очікує виконання викликає print(), який друкує збільшуване значення x. Після чого виконується присвоєння x = x + 1. Після чого цикл продовжується, і перший оператор на який ми в ньому натикаємось - це знову yield x, який зберігає стан всього і повертає поточне значення x (тепер вже 3).

>>> next(counter)
incrementing x
4

Коли ми знову викликаємо next(counter), все відбувається так само, але цього разу x має значення 4.


Так як make_counter містить нескінченний цикл, ми можемо в теорії робити це безперервно, і він буде просто збільшувати x та видавати значення. Але давайте замість цього розглянемо більш продуктивне використання генераторів.

Генератор чисел Фібоначчі[ред.]

yield зупиняє виконання, next() продовжує з того місця на якому зупинились
def fib(max):
    a, b = 0, 1

Послідовність Фібоначі - це послідовність чисел де кожне число є сумою двох попередніх. Вона починається з 0 та 1, починає повільно зростати, а далі зростає все швидше і швидше. Щоб почати послідовність нам потрібно дві змінні зі значеннями 0 та 1.

    while a < max:
        yield a

a - поточне число послідовності, тому ми його повертаємо.

        a, b = b, a + b

b - наступне число послідовності, тому присвоюємо його a, але також обчислюємо наступне значення (a+b) і присвоюємо його b для подальшого використання. Зауважте що це відбувається одночасно: якщо a = 3 а b = 5, тоді a, b = b, a + b присвоїть a значення 5 (попереднє значення b) а b - 8 (сума попередніх значень a та b).

Отож в нас є функція що послідовно випльовує числа Фібоначчі. Звісно, це можна зробити і з рекурсією, але в такий спосіб це простіше читати. Також, це гарно працює з циклами for.


>>> from fibonacci import fib
>>> for n in fib(1000):

Можна використовувати генератори на зразок fib() прямо. Цикл for автоматично буде викликати функцію next() щоб отримувати значення з генератора fib() та присвоювати їх індексу циклу.

...     print(n, end=' ') 
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Кожного разу в тілі циклу n отримує нове значення з команди yield всередині функції fib(), і все що вам потрібно зробити це його надрукувати. Як тільки в fib() закінчаться числа (a стане більшим за max, яке в даному випадку дорівнює 1000), і цикл передбачливо закінчується.

>>> list(fib(1000)) 
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

Це корисна ідіома: передати генератор в функцію list() і вона проітерується по всьому генератору (як і цикл в попередньому прикладі) та поверне список зі всіма значеннями.

Генератор правил утворення множини[ред.]

Давайте повернемось і поглянемо як працює функція plural().

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)

Тут ніякої магії. Пам'ятаєте що рядки в файлі з правилами містять по три значення розділені пропусками, тому ми можемо використати line.split(None, 3) щоб отримати ці "колонки" та присвоїти їх локальним змінним.

            yield build_match_and_apply_functions(pattern, search, replace)

І ось тут ми використовуємо yield. Яке значення ми викидаємо? Дві функцкії, створені динамічно за допомогою нашого старого друга, функції build_match_and_apply_functions(). Іншими словами, rules() - це генератор що випльовує функції match та apply за вимогою.

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))

Так як rules() - це генератор, його можна використовувати прямо в циклі. При першій ітерації циклу ми викличемо функцію rules(), яка відкриє файл шаблонів, прочитає перший рядок, динамічно створить потрібні функції з шаблонів, і поверне створені функції нам. В наступній ітерації циклу for, ми продовжимо рівно з того місця де ми покинули функцію rules(). Перше що вона зробить - це прочитає наступний рядок з файлу (який все ще відкритий), динамічно збудує наступні функції на основі прочитаних шаблонів, і поверне ці дві функції.


Що ми виграли використовуючи генератори? Час запуску. В попередньому прикладі, коли ми імпортували модуль plural4, він читав ввесь файл шаблонів, і створював список всіх можливих функцій ще тоді коли ми навіть не думали викликати функцію plural(). З генераторами ви можете робити все ліниво: прочитати перше правило, створити функції, спробувати їх, і якщо вони підійдуть не потрібно буде навіть читати решту файла чи створювати будь-які інші функції.

Що ми втратили? Продуктивність! Щоразу як ми викликаємо функцію plural(), генератор rules() починає все з початку, що означає ще одне відкривання файлу з шаблонами, і читання з самого початку, по рядку за раз.

А що якщо б ми могли поєднати краще з обох підходів: мінімальний час запуску (не виконувати ніякий код при імпорті), і максимальну продуктивність (не створювати одні й ті самі функції заново). Ну, і все ще тримати правила в окремому файлі (тому що код це код, а дані це дані), поки звісно не доведеться читати один і той самий рядок вдруге.

Щоб це зробити, нам потрібно буде створити власний ітератор. Але перед цим потрібно буде вивчити класи мови Python.


* * *


Для подальшого читання[ред.]

Класи та ітератори[ред.]

Захід є Захід, а Схід є Схід, і їм не зійтися вдвох
Редьярд Кіплінг


Занурення[ред.]

Ітератори це "таємний соус" мови Pythоn 3. Вони всюди, в основі всього, просто зазвичай невидимі. Вирази над структурами - лише проста форма ітераторів. Генератори - лише проста форма ітераторів. Функції що використовують yield це лише гарний, компактний спосіб побудови ітератора без побудови ітератора. Давайте я покажу що я під цим маю на увазі.

Пам'ятаєте генератор чисел Фібоначчі? Ось побудований з нуля ітератор:

class Fib:
    '''iterator that yields numbers in the Fibonacci sequence'''

    def __init__(self, max):
        self.max = max

    def __iter__(self):
        self.a = 0
        self.b = 1
        return self

    def __next__(self):
        fib = self.a
        if fib > self.max:
            raise StopIteration
        self.a, self.b = self.b, self.a + self.b
        return fib

Щож давайте розбирати по рядку за раз.

class Fib:

class? Що таке клас?


* * *


Опис класів[ред.]

Python повністю об'єктно-орієнтований: ви можете описувати власні класи, наслідуватись від власних чи вбудованих класів, і створювати екземпляри описаних класів.

Описувати класи в Python просто. Як і з функціями, немає окремого опису інтерфейсу. Просто оголосіть клас і починайте кодити. Клас в Python починається з зарезервованого слова class, за яким іде ім'я класу. Технічно, це все що потрібно, тому що клас не обов'язково повинен наслідуватись від іншого класу.

class PapayaWhip:

Цей клас називається PapayaWhip[1], і не наслідується від інших класів. Імена класів зазвичай пишуться з великої букви, подібно до КожнеСловоЯкУЦьомуОтут, але це лише домовленість, а не вимога.

    pass

Ви можливо вже й самі здогадались, але все в класі пишеться з відступом, так само як і код всередині функцій, умовних операторів, циклів, чи інших блоків коду. Перший рядок без відступа вважається рядком за межами класу.


Наш клас PapayaWhip не описує жодних методів чи атрибутів, але синтаксично необхідно помістити що-небудь в опис, тому ми використали ключове слово pass. Це слово означає "проходьте, тут нема на що дивитись". Це команда яка нічого не робить, і її зручно підставляти коли потрібно зробити заготовку функції.

Інструкція pass в Python аналогічна парі порожніх фігурних дужок ({}) в Java чи C.

Багато класів наслідуються від інших класів, але даний ні. Багато класів описують методи, але цей ні. Взагалі немає нічого що клас в Python обов'язково повинен мати, окрім імені. Наприклад програмісти C++ можуть здивуватись що класи в Python не мають явних конструкторів та деструкторів. Щоправда хоча це й не обов'язково, класи в Python можуть мати щось подібне до конструктора: метод __init__().


Метод __init__()[ред.]

Цей приклад показує ініціалізацію класу Fib з використанням методу __init__.

class Fib:
    '''iterator that yields numbers in the Fibonacci sequence'''

Класи можуть (і повинні б) мати документацію, так само як модулі та функції.

    def __init__(self, max):

Метод __init__() викликається негайно після того як створюється екземпляр класу. Спокусливо називати цей метод "конструктором" класу, але це технічно невірно. Це спокусливо тому що він виглядає як конструктор в C++ (за домовленістюю, метод __init__() повинен бути першим в списку методів класу), поводиться як конструктор (це перший код що виконується в новоствореному екземплярі класу), і навіть звучить як конструктор. Але це не вірно, тому що об'єкт було створено ще до того як було викликано метод __init__(), і в нас вже є діюче посилання на новостворений екземпляр класу.

Перший аргумент будь-якого методу класу, включаючи метод __init__(), це завжди посилання на поточний екземпляр класу, за домовленістю його називають self. Цей аргумент замінює роль зарезервованого слова this в C++ чи Java, але self не є зарезервованим словом в Python, просто домовленістю з іменування. Тим не менш, будь-ласка не називайте його якось інакше ніж self, це дуже сильна домовленість.

У всіх методах класу, self посилається на екземпляр метод якого викликали. Але в особливому випадку метода __init__() екземпляр чий метод був викликаним також є новоствореним об'єктом. І хоча при описі метода необхідно описувати параметр self явно, його не потрібно передавати при виклику метода, Python передасть його для нас автоматично.


* * *


Створення екземплярів класу[ред.]

Створення екземплярів класу в Python досить прямолінійне. Щоб створити екземпляр класу, просто викличте клас так наче б він був функцією, передавши як аргументи все що необхідно функції __init__(). Значення яке буде повернутим і буде новоствореним об'єктом.

>>> import fibonacci2
>>> fib = fibonacci2.Fib(100)

Ми створюємо екземпляр класу Fib (описаного в модулі fibonacci2) і присвоюємо новостворений екземпляр змінній fib. Ми передаємо один параметр, 100, який буде переданий як аргумент max в метод __init__().

>>> fib
<fibonacci2.Fib object at 0x00DB8810>

fib тепер екземпляр класу Fib.

>>> fib.__class__
<class 'fibonacci2.Fib'>

Кожен екземпляр класу має вбудований атрибут __class__, який посилається на клас об'єкта. Програмісти Java можливо знайомі з класом Class, який містить методи на зразок getName() таd getSuperclass() щоб отримувати метаінформацію про об'єкт. В Python такі дані доступні через атрибути, але ідея загалом така сама.

>>> fib.__doc__
'iterator that yields numbers in the Fibonacci sequence'

Можна звертатись до документації екземпляра так само як і з функціями чи модулями. Всі екземпляри класу діляться спільним рядком документації.

В Python щоб створити екземпляр, просто викличте клас як функцію. Не потрібно явного оператора new як в C++ чи Java.


* * *


Змінні екземпляра[ред.]

І перейдемо до наступного рядка класу:

class Fib:
    def __init__(self, max):
        self.max = max

Що таке self.max? Це змінна екземпляра. Це зовсім інша змінна ніж max, яка була передана як аргумент в метод __init__(). self.max "глобальна" для екземпляра. Це означає що доступ до неї можна отримати з інших методів.

class Fib:
    def __init__(self, max):
        self.max = max

self.max створюється в методі __init__()...

    .
    .
    .
    def __next__(self):
        fib = self.a
        if fib > self.max:

... і використовується з метода __next__().

Змінні екземпляра пов'язані лише з одним екземпляром класу. Наприклад якщо ми створимо два екземпляри класу Fib з різними максимальними значеннями, вони пам'ятатимуть кожен про свою змінну.

>>> import fibonacci2
>>> fib1 = fibonacci2.Fib(100)
>>> fib2 = fibonacci2.Fib(200)
>>> fib1.max
100
>>> fib2.max
200


* * *


Ітератор Фібоначчі[ред.]

Всі три із згадуваних методів класу, __init__, __iter__, та __next__, починаються та закінчуються парою символів підкреслювання (_). Чому так? В цьому немає ніякої магії, але зазвичай це означає що це "спеціальні методи". Єдине що "спеціальне" в спеціальних методах - це те що вони не викликаються напряму, Python викликає їх коли ви використовуєте якийсь інший синтаксис на класі чи на його екземплярі. Більше про спеціальні методи.

Тепер ви готові до того щоб навчитись створювати ітератор. Ітератор це просто клас який описує метод __iter__().

class Fib:

Щоб створити ітератор з нуля, Fib повинен бути класом а не функцією.

    def __init__(self, max):
        self.max = max

"Виклик" Fib(max) це насправді створення екземпляру цього класу і виклик його метода __init__() з аргументом max. Метод __init__() зберігає максимальне значення як змінну екземпляру щоб інші методи могли використати її пізніше.

    def __iter__(self):
        self.a = 0
        self.b = 1
        return self

Метод __iter__() викликається щоразу як хтось викликає iter(fib). (Як ви побачите назабаром, цикл for може викликати це автоматично, але ви також можете зробити це вручну.) Піля виконання ініціалізації перед початком ітерації (в даному випадку встановлення значень лічильників self.a та self.b), метод __iter__() може повернути будь-який об'єкт що реалізує метод __next__(). В цьому (та й у більшості інших) випадку, __iter__() просто повертає self, так як цей клас реалізує власний метод __next__().

def __next__(self):
        fib = self.a

Метод __next__() викликається щоразу як хтось викликає next() на ітераторі чи екземплярі класу. Це стане більш зрозумілим за хвилину.

        if fib > self.max:
            raise StopIteration

Коли метод __next__() генерує виняток StopIteration це сигналізує тому хто його викликав про те що ітерація вичерпалась. На відміну від більшості інших винятків, це не помилка. це нормальна ситуація яка просто означає що ітератор більше не має значень для генерації. Якщо тим хто викликав next() є цикл for, він просто завершить ітерації і передасть потік виконання далі. (Іншими словами він просто проковтне виняток.) Цей маленький шматочок магії насправді є ключем до використання ітератоів в циклах for.

        self.a, self.b = self.b, self.a + self.b
        return fib

Щоб згенерувати наступне значення, метод ітератора __next__() просто повертає його за допомогою return. Він не використовує yield тому що це просто синтаксичний цукор який використовується лише в генераторах. Метод __next__ - метод ітератора а не генератор.

Вже добряче заплутались? Прекрасно. Давайте поглянемо як викликати цей ітератор:

>>> from fibonacci2 import Fib
>>> for n in Fib(1000):
...     print(n, end=' ')
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Але ж цей виклик точно такий самий! Байт в байт ідентичний з тим як ми викликали генератор чисел Фібоначчі (за винятком того що тепер він називається з великої букви). Але як?

З циклами for пов'язано трішки магії. Ось що відбувається:

  • Цикл for викликає Fib(1000), як написано. Це дає екземпляр класу Fib. Будемо називати його fib_inst.
  • Таємно, і дуже хитро, цикл for викликає iter(fib_inst), який повертає об'єкт ітератора. Давайте будемо називати його fib_iter. В даному випадку, fib_iter == fib_inst, тому що метод __iter__() повертає self, але цикл for про це не знає (чи взагалі не цікавиться).
  • Щоб "проітеруватись" ітератором, цикл for викликає next(fib_iter), який викликає метод __next__() на об'єкті fib_iter який робить обчислення наступного числа Фібоначчі і повертає значення. Цикл for бере це значення і присвоює його n, після чого виконує тіло циклу з даним значенням n.
  • Звідки цикл for знає коли зупинитись? Радий що ви спитали! Коли next(fib_iter) кидає виняток StopIteration, цикл for ковтає виняток і завершується. (Будь-який інший звичайний виняток прорветься і буде оброблятись як зазвичай.) А де ми бачили виняток StopIteration? Звичайно в методі __next__()!


* * *


Ітератор правил утворення множини[ред.]

iter(f) викликає f.__iter__
next(f) викликає f.__next__

Тепер час для завершення. Давайте перепишемо генератор правил утворення множини як ітератор.

class LazyRules:
    rules_filename = 'plural6-rules.txt'

    def __init__(self):
        self.pattern_file = open(self.rules_filename, encoding='utf-8')
        self.cache = []

    def __iter__(self):
        self.cache_index = 0
        return self

    def __next__(self):
        self.cache_index += 1
        if len(self.cache) >= self.cache_index:
            return self.cache[self.cache_index - 1]

        if self.pattern_file.closed:
            raise StopIteration

        line = self.pattern_file.readline()
        if not line:
            self.pattern_file.close()
            raise StopIteration

        pattern, search, replace = line.split(None, 3)
        funcs = build_match_and_apply_functions(
            pattern, search, replace)
        self.cache.append(funcs)
        return funcs

rules = LazyRules()

Цей клас реалізує методи __iter__() та __next__(), тому він може використовуватись як ітератор. Потім ми створюємо його екземпляр та відкриваємо для нього файл з правилами. Це відбувається лише раз при імпорті.

Давайте спробуємо розібратись у всьому по кусочку за раз.


class LazyRules:
    rules_filename = 'plural6-rules.txt'

    def __init__(self):
        self.pattern_file = open(self.rules_filename, encoding='utf-8')

Коли ми створюємо екземпляр класу LazyRules відкривається файл з шаблонами, але з нього нічого не читається (це буде потім).

        self.cache = []

Після відкриття файла з шаблонами, ми ініціалізуємо кеш. Ми використаємо його пізніше (в методі __next__()) протягом того як будемо читати рядки з файла з шаблонами.


Перед тим як продовжити давайте поближче глянемо на змінну rules_filename. Вона не описується всередині метода __iter__(). Насправді вона не описується всередині будь-якого метода. Вона описується на рівні класу. Це змінна класу, і хоча доступ до неї подібний на доступ до змінних екземпляра (через self.rules_filename), але вона спільна для всіх екземплярів класу LazyRules.


>>> import plural6
>>> r1 = plural6.LazyRules()
>>> r2 = plural6.LazyRules()
>>> r1.rules_filename 
'plural6-rules.txt'
>>> r2.rules_filename
'plural6-rules.txt'

Кожен екземпляр класу успадковує атрибут rules_filename зі значенням описаним всередині класу.

>>> r2.rules_filename = 'r2-override.txt'
>>> r2.rules_filename
'r2-override.txt'
>>> r1.rules_filename
'plural6-rules.txt'

Зміна значення цього атрибуту не змінює його в інших екземплярах...

>>> r2.__class__.rules_filename
'plural6-rules.txt'

... як і не змінює атрибут класу. Ви можете отримати доступ до атрибуту класу (на противагу атрибутам окремих екземплярів) використовуючи спеціальний атрибут __class__ для доступу до самого класу.

>>> r2.__class__.rules_filename = 'papayawhip.txt'
>>> r1.rules_filename
'papayawhip.txt'

Якщо ми змінюємо атрибут класу, він змінюється для всіх екземплярів які все ще його наслідують (як наприклад тут r1).

>>> r2.rules_filename
'r2-overridetxt'

Екзеплярів які переозначили атрибут (як r2 в цьому прикладі) це не стосується.


Тепер повернемось до наших баранів.


  def __iter__(self):

Метод __iter__() буде викликаним щоразу як хтось, наприклад цикл for викликатиме iter(rules).

        self.cache_index = 0
        return self

Одна річ яку можен метод __iter__() повинен робити - повертати ітератор. В даному випадку він повертає self що сигналізує про те що клас описує метод __next__() який потурбується про повернення значень під час ітерації.


    def __next__(self):

Метод __next__() викликається щоразу, як хтось (наприклад цикл for) викликає next(rules). Цей метод буде зрозумілішим якщо ми почнемо розглядати його з кінця і просуватись до початку, тому давайте так і зробимо.

        .
        .
        .
        pattern, search, replace = line.split(None, 3)
        funcs = build_match_and_apply_functions(        
            pattern, search, replace)
        self.cache.append(funcs)                        
        return funcs

Принаймі остання частина цієї функції повинна виглядати знайомо. Функція build_match_and_apply_functions() не змінилась, вона така ж як і була завжди.

Єдина відмінність в тому, що перед тим як повернути функції (що зберігаються в кортежі func) ми збираємось зберегти їх в self.cache.

Тепер рухаємось назад...

    def __next__(self):
        .
        .
        .
        line = self.pattern_file.readline()
        if not line:
            self.pattern_file.close()
            raise StopIteration
        .
        .
        .

Тут деякі особливі трюки з файлами. Метод readline() (зауважте, назва в однині, а не readlines()) читає рівно один рядок з відкритого файлу. Якщо точніше - наступний рядок. (Файлові об'єкти теж ітератори! Всюди ітератори)

Якщо readline() ще має рядки які можна читати, то це не порожні рядки. Навіть якщо файл і містить порожній рядок, то він буде зберігатись як один символ '\n' (повернення каретки). Якщо змінній line справді присвоїться порожній рядок, це означатиме що рядків в файлі більше нема.

Коли ми досягнемо кінця файла ми повинні його закрити, і згенерувати магічний виняток StopIteration. Ми написали такий код тому що нам потрібно отримати функції для наступного правила. А ці функції утворюються на основі прочитаного рядка. А якщо прочитаного рядка нема, значить і нема значень які потрібно повертати, тому це кінець ітерації. (♫ Кінець бенкету... ♫)

Далі назад...

    def __next__(self):
        self.cache_index += 1
        if len(self.cache) >= self.cache_index:
            return self.cache[self.cache_index - 1]

        if self.pattern_file.closed:
            raise StopIteration
        .
        .
        .

self.cache повинен бути списком функцій які потрібні нам для застосування правил. self.cache_index слідкує за тим який з елементів цього списку потрібно буде повертати наступним. Якщо ми ще не вичерпали кеш (тобто довжина self.cache більша за self.cache_index), то ми можемо взяти значення з кеша! Ура! Ми можемо повернути готові функції замість того щоб створювати їх з нуля.

З іншого боку, якщо значень в кеші нема, і файловий об'єкт закритий (що може трапитись далі в методі, в коді який ми бачили раніше), значить ми нічого більше не зможемо зробити. Якщо файл закритий, це означає що він вичерпався, ми вже прочитали всі його рядки, і побудували всі функції на їх основі. Файл вичерпався, кеш вичерпався, я виснажився. Стояти, що? Зачекайте тут, ми майже закінчили.

Якщо все підсумувати, ось що відбувається коли:

  • Коли модуль імпортується, він створює один екземпляр класу LazyRules названий rules, який відкриває файл шаблонів але не читає з нього.
  • Коли його запитують про першу пару функцій, він перевіряє кеш, але з'ясовує що кеш порожній. Тому він читає рядок з файлу шаблонів, створює функції з шаблонів і записує їх в кеш.
  • Давайте для прикладу скажеом що перші функції підійшли. Якщо так, тоді нові функції не будуються і нові рядки не читаються з файлу.
  • Давайте для прикладу також скажемо що користувач викликає функцію plural() знову для того щоб створити множину для іншого слова. Цикл for в функції plural() викличе iter(rules), який обнулить індекс кешу, але не чіпатиме відкритий файловий об'єкт.
  • Далі цикл for попросить значення з ітератора rules, який застосує свій метод __next__(). Тільки цього разу в кеші вже буде одна пара функцій що відповідають шаблонам в першому рядку файла. Так як вони вже були створені при утворенні множини для попереднього слова, їх буде взято з кешу. Індекс кешу збільшиться, а відкритий файл взагалі не буде зачеплено.
  • Тепер давайте для прикладу скажемо що цього разу перше правило не підійшло. Тоді цикл for спитає про наступне значення з rules. Це викличе метод __next__() вдруге. Цього разу кеш вичерпано - бо в ньому був лише один елемент, а нам потрібен другий, тому метод __next__() продовжить роботу далі, і прочитає наступний рядок з файлу, побудувавши відповідні функції та запам'ятавши їх в кеші.
  • Цей процес читання побудови й запам'ятовування продовжуватиметься доти доки правила що читаються з файла не застосовуватимуться до слова для якого ми намагаємось утворити множину. Якщо ми знайдемо підходяще правило ще до кінця файлу, ми просто його використаємо і зупинимось тримаючи файл відкритим. Посилання на файл залишатиметься, чекаючи наступної команди readline(). Тим часом кількість елементів в кеші буде більшати, і якщо нам буде потрібно утворити множину для ще одного слова, кожен з елементів кешу буде випробуваний до того як буде прочитаний наступний рядок файлу.

От ми й досягли множинної нірвани.

  1. Мінімальна ціна запуску. Єдине що відбувається при імпорті, це створення одного екземпляру класу та відкриття файлу (але не читання з нього).
  2. Максимальна продуктивність. В попередньому прикладі ми читали ввесь файл і будували функції динамічно для кожного слова заново. В цій версії ми запам'ятовуємо функці як тільки вони утворюються, і в найгіршому випадку ми читаємо файл з правилами лише раз, не залежно від того скільки слів нам потрібно буде опрацювати.
  3. Відокремлення коду й даних. Всі шаблони зберігаються в окремому файлі. Код це код, а дані це дані, і їм не зійтися вдвох.

Це справді нірвана? Ну, і так і ні. Є дещо що потрібно мати на увазі при роботі з класом LazyRules: файл шаблонів відкривається (в методі __init__()) і залишається відкритим поки не буде досягнено останнє правило. Python колись закриє файл коли програма завершуватиметься, чи після того як останній екземпляр класу LazyRules буде знищено, але все одно до цього моменту може пройти багато часу. Якщо цей клас є частиною довго працюючого процесу, інтерпретатор Python може ніколи не завершити свою роботу, і об'єкт класу LazyRules може ніколи не бути знищений. Є способи обійти це. Замість того щоб відкривати файл в методі __init__() і залишати його відкритим читаючи по одному правилу за раз, можна відкрити файл, прочитати всі правила і негайно його закрити. Або можна відкрити файл, прочитати одне правило, зберегти позицію за допомогою метода tell(), закрити файл, потім перевідкрити його і використати метод seek() щоб продовжити читати з того місця де завершили. Або можна не хвилюватись про це і просто залишити файл відкритим як ми й зробити в цьому випадку. Програмування це проектування, а суть проектування в компромісах та обмеженнях. Занадто довго відкритий файл може бути проблемою, більш складний код може бути проблемою. Що є більшою проблемою залежить від команди що розробляє вашу програму, вашої програми і середовища в якому вона працюватиме.


* * *


Для подальшого читання[ред.]

Примітки[ред.]

  1. Колір що утворюється при змішуванні пюре папаї з ванільним морозивом чи йогуртом. Або світло-оранжевий українською. :)

Детальніше про ітератори[ред.]

"Великі блохи мають на спинах маленьких блох, щоб їх кусали.
А малі мають ще менших, і так до нескінченності."

Августус Де Морган


Занурення[ред.]

Так як регулярні вирази - це операції з рядками на стероїдах, так само модуль itertools - ітератори на стероїдах. Але спершу я хочу показати вам одну класичну головоломку:

HAWAII + IDAHO + IOWA + OHIO == STATES
510199 + 98153 + 9301 + 3593 == 621246

H = 5
A = 1
W = 0
I = 9
D = 8
O = 3
S = 6
T = 2
E = 4
Один з найвідоміших математичних ребусів: SEND + MORE = MONEY.

Такі головоломки називаються математичними ребусами. Букви утворюють якісь слова, але якщо замінити кожну букву цифрою від 0 до 9, числа утворять правильну математичну тотожність. Задача в тому, аби вияснити яка цифра відповідає кожній букві. Всі випадки використання букви замінюються на одну й ту ж цифру, жодна цифра не замінює двох різних букв, і жодне "слово" не може починатись з цифри 0.

В цьому розділі ми розберемо неймовірну програму мовою Python, початково написану Реймондом Геттінґером. Ця програма розв’язує математичні ребуси використовуючи всього 14 рядків коду.

import re
import itertools

def solve(puzzle):
    words = re.findall('[A-Z]+', puzzle.upper())
    unique_characters = set(''.join(words))
    assert len(unique_characters) <= 10, 'Too many letters'
    first_letters = {word[0] for word in words}
    n = len(first_letters)
    sorted_characters = ''.join(first_letters) + \
        ''.join(unique_characters - first_letters)
    characters = tuple(ord(c) for c in sorted_characters)
    digits = tuple(ord(c) for c in '0123456789')
    zero = digits[0]
    for guess in itertools.permutations(digits, len(characters)):
        if zero not in guess[:n]:
            equation = puzzle.translate(dict(zip(characters, guess)))
            if eval(equation):
                return equation

if __name__ == '__main__':
    import sys
    for puzzle in sys.argv[1:]:
        print(puzzle)
        solution = solve(puzzle)
        if solution:
            print(solution)

Ви можете запустити програму з командного рядка. Як це виглядатиме в Лінукс продемонстровано нижче. (Пошук розв’язку може зайняти деякий час, тому наберіться терпіння).

you@localhost:~/diveintopython3/examples$ python3 alphametics.py "HAWAII + IDAHO + IOWA + OHIO == STATES"
HAWAII + IDAHO + IOWA + OHIO = STATES
510199 + 98153 + 9301 + 3593 == 621246
you@localhost:~/diveintopython3/examples$ python3 alphametics.py "I + LOVE + YOU == DORA"
I + LOVE + YOU == DORA
1 + 2784 + 975 == 3760
you@localhost:~/diveintopython3/examples$ python3 alphametics.py "SEND + MORE == MONEY"
SEND + MORE == MONEY
9567 + 1085 == 10652


* * *


Пошук всіх входжень шаблону[ред.]

Перше що робить наш розв’язувач головоломок - знаходить всі великі латинські літери в умові:

>>> import re
>>> re.findall('[0-9]+', '16 2-by-4s in rows of 8')
['16', '2', '4', '8']

Модуль re містить реалізацію регулярних виразів мови Python. Він має вправну функцію findall(), яка отримує регулярний вираз, і рядок, та повертає список зі всіма входженнями шаблону в рядок. В нашому випадку шаблону відповідають послідовності цифр.

>>> re.findall('[A-Z]+', 'SEND + MORE == MONEY')
['SEND', 'MORE', 'MONEY']

Це шаблон якому відповідають послідовності літер. Знову ж таки значенням що повертається - список, кожен елемент якого співставний з регулярним виразом.

А ось ще один приклад, щоб дещо розім’яти ваш мозок:

>>> re.findall(' s.*? s', "The sixth sick sheikh's sixth sheep's sick.")
[' sixth s', " sheikh's s", " sheep's s"]
Це найважча скоромовка в англійській мові.

Здивовані? Регулярний вираз шукає пробіл, s, найкорошту можливу послідовність довільних символів, знову пробіл, а потім іншу s. Але дивлячись на той рядок я бачу аж п’ять відповідностей:

  1. The sixth sick sheikh's sixth sheep's sick.
  2. The sixth sick sheikh's sixth sheep's sick.
  3. The sixth sick sheikh's sixth sheep's sick.
  4. The sixth sick sheikh's sixth sheep's sick.
  5. The sixth sick sheikh's sixth sheep's sick.

А re.findall() повертає список з трьома співпадіннями. Якщо конкретно, то з першим, третім і п’ятим. Чому б це? Тому що вона не повертає співпадіння які перекриваються. Перше співпадіння перекривається з другим, тому друге пропускається. Наступне третє повертається, але воно перетинається з четвертим, тому четверте пропускається, і насамкінець повертається п’яте. Таким чином отримуємо п’ять співпадінь.

Але це не має жодного відношення до розв’язування ребусів. Я просто подумав що це може бути цікаво.


* * *


Пошук унікальних елементів послідовності[ред.]

Множини роблять пошук унікальних елементів тривіальною справою.

>>> a_list = ['The', 'sixth', 'sick', "sheik's", 'sixth', "sheep's", 'sick'] 
>>> set(a_list) 
{'sixth', 'The', "sheep's", 'sick', "sheik's"}

Отримавши на вхід список елементів, функція set() поверне набір унікальних рядків в списку. Це можна собі уявити як цикл for: беремо перший елемент, додаємо в множину. Потім другий, третій, четвертий, п’ятий... Стоп! П’ятий вже є в множині, тому його пропускаємо. Додаємо ще шостий, і пропускаємо останній. В результаті отримуємо набір унікальних елементів списку. Для цього список навіть не довелось сортувати.

>>> a_string = 'EAST IS EAST' 
>>> set(a_string) 
{'A', ' ', 'E', 'I', 'S', 'T'}

Така ж техніка працює з рядками, тому що рядки - просто послідовності символів.

>>> words = ['SEND', 'MORE', 'MONEY'] 
>>> ''.join(words)
'SENDMOREMONEY'

.join(a_list) склеює список рядків a_list в один рядок.

>>> set(''.join(words))
{'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}

Цей код повертає множину унікальних символів в усіх рядках списку.

Розв’язувач ребусів використовує такий підхід при побудові множини всіх символів головоломки:

unique_characters = set(''.join(words))

Цей набір пізніше використовується щоб присвоювати символам числові значення, в процесі перебору розв’язків.


* * *


Пересвідчення (assert)[ред.]

Як і багато інших мов, Python містить інструкцію assert. Вона працює так:

>>> assert 1 + 1 == 2

Після інструкції assert іде довільний вираз мови Python. В цьому випадку твердження 1 + 1 == 2 істинне, тому інструкція assert нічого не робить.

>>> assert 1 + 1 == 3 
Traceback (most recent call last): 
 File "<stdin>", line 1, in <module> 
AssertionError

Тим не менш, якщо вираз приймає хибне значення в булевому контексті, assert згенерує виняток AssertionError

>>> assert 2 + 2 == 5, "Only for very large values of 2"
Traceback (most recent call last): 
 File "<stdin>", line 1, in <module>
AssertionError: Only for very large values of 2

Також ви можете додати після виразу пояснення того що сталось для інших людей.

Коротше кажучи, рядок коду:

assert len(unique_characters) <= 10, 'Too many letters'

... є еквівалентним оцьому:

if len(unique_characters) > 10:
    raise AssertionError('Too many letters')

Розв’язувач ребусів використовує таке пересвідчення, щоб не почати розв’язувати головоломку, яка містить більше десяти різних символів. Бо так як цифр всього десять головоломка що має більше символів не може мати розв’язку.


* * *



Генераторні вирази[ред.]

Генераторні вирази - це як генераторні функції тільки без функцій.

>>> unique_characters = {'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'} 
>>> gen = (ord(c) for c in unique_characters)

Генераторний вираз схожий на анонімну функцію що генерує значення. Він схожий на списковий вираз, але замість квадратних дужок використовує круглі.

>>> gen 
<generator object <genexpr> at 0x00BADC10>

Генераторний вираз повертає ітератор.

>>> next(gen) 69 
>>> next(gen) 
68

Виклик функції next повертає наступне значення переданого їй ітератора.

>>> tuple(ord(c) for c in unique_characters) 
(69, 68, 77, 79, 78, 83, 82, 89)

За бажанням ви можете здійснити ітерацію крізь всі значення, та повернути список, кортеж, чи множину, передаючи генераторний вираз як аргумент у функції list(), tuple() чи set(). В цьому випадку вам навіть не потрібна зайва пара дужок - просто передайте "голий" вираз ord(c) for c in unique_characters в функцію tuple(), і Python сам здогадається що це генераторний вираз.

Використання генераторних виразів замість спискових економить як пам’ять так і процесорний час. Якщо ви будуєте список тільки для того аби його невдовзі викинути (тобто передати функціям tuple() чи set()) - краще натомість використовуйте генератори.

Ось інший спосіб досягнути того ж, використовуючи генераторну функцію:

def ord_map(a_string):
    for c in a_string:
        yield ord(c)

gen = ord_map(unique_characters)

Генераторний вираз набагато компактніший, хоча є функціонально еквівалентним.


* * *


Перебір перестановок... Спосіб для лінивих![ред.]

Перш за все - що перестановки взагалі таке? Перестановки - це таке математичне поняття. (Взагалі то, існує кілька означень, залежно від того який розділ математики вам потрібен. Тут я розглядаю комбінаторику, але якщо це для вас взагалі нічого не означає - не хвилюйтесь. Як завжди - вікіпедія це ваша помічниця).

Ідея полягає в тому, що ви берете послідовність якихось об’єктів (чисел, рядків чи навіть танцюючих ведмедів) і перебираєте всі можливі способи утворити з неї менші послідовності. Ці менші послідовності повинні бути однакової, проте довільної довжини - від одного до кількості елементів в початковій послідовності. (Примітка: англійське слово permutations в українській комбінаторній термінології означає два поняття: розміщення та перестановки. Перестановки - це розміщення довжини n, де n - довжина початкової послідовності, тому надалі напевне краще буде перекладати термін permutations як розміщення. Не знаю.) Також - два однакові розміщення не повинні повторюватись. Математики кажуть: "давайте знайдемо всі розміщення з трьох елементів по два", що означає: давайте знайдемо всі різні послідовності довжини 2, з трьох можливих елементів.

>>> import itertools

Модуль itertools містить багато цікавих речей, і в тому числі функцію permutations(), яка робить за нас всю важку роботу з генерації розміщень.

>>> perms = itertools.permutations([1, 2, 3], 2)

Функція permutations() приймає на вхід послідовність (в даному випадку список з трьох чисел), і число - розмір розміщень які ми будемо генерувати. Функція повертає ітератор, який можна використовувати в циклах for, чи інших місцях де потрібні послідовності. Тут я перевірю всі значення ітератора вручну.

>>> next(perms)
(1, 2) 
>>> next(perms)
(1, 3) 
>>> next(perms)
(2, 1) 
>>> next(perms)
(2, 3) 
>>> next(perms)
(3, 1) 
>>> next(perms)
(3, 2)

Зауважте що розміщення впорядковані. (2, 1) - це інша не те саме розміщення що й (1, 2).

>>> next(perms)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module> 
StopIteration

І це всі розміщення з елементів [1, 2, 3] взятих поодинці. Такі пари як (1, 1) не з’являються, бо вони містять повторення, тому не є правильними розміщеннями. Коли ітерації закінчуються ітератор генерує виняток StopIteration

Модуль itertools містить ще купу цікавих речей.

Функція permutations() приймає не тільки списки. Вона може приймати довільні послідовності - навіть рядки.

>>> import itertools 
>>> perms = itertools.permutations('ABC', 3)

Рядок - це всього лиш послідовність символів. При генеруванні перестановок рядок 'ABC' еквівалентний списку ['A', 'B', 'C']

>>> next(perms)
('A', 'B', 'C') 
>>> next(perms)
('A', 'C', 'B') 
>>> next(perms)
('B', 'A', 'C') 
>>> next(perms)
('B', 'C', 'A') 
>>> next(perms)
('C', 'A', 'B') 
>>> next(perms)
('C', 'B', 'A') 
>>> next(perms)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

Перше розміщення (тут прийнятний і термін перестановка) з елементів ['A', 'B', 'C'], взятих по три штуки - ('A', 'B', 'C'). Решта п’ять - ці ж три елементи які розміщуються у всіх мислимих порядках.

>>> list(itertools.permutations('ABC', 3))
[('A', 'B', 'C'), ('A', 'C', 'B'),
 ('B', 'A', 'C'), ('B', 'C', 'A'),
 ('C', 'A', 'B'), ('C', 'B', 'A')]

Так як функція permutations() завжди повертає ітератор, то найпростіший спосіб її зневадження - передати цей ітератор функції list(), щоб отримати всі перестановки одразу.


* * *


Інші веселі речі з модуля itertools[ред.]

>>> import itertools 
>>> list(itertools.product('ABC', '123')) 
[('A', '1'), ('A', '2'), ('A', '3'),
 ('B', '1'), ('B', '2'), ('B', '3'),
 ('C', '1'), ('C', '2'), ('C', '3')]

itertools.product() повертає ітератор що генерує декартовий добуток двох послідовностей.

>>> list(itertools.combinations('ABC', 2)) 
[('A', 'B'), ('A', 'C'), ('B', 'C')]

Функція itertools.combinations() повертає ітератор що містить всі можливі комбінації певної кількості елементів даної послідовності. Це майже так само як itertools.permutations(), тільки до списку комбінацій не входять комбінації які містять такі ж елементи але в іншому порядку. Тобто itertools.permutations('ABC', 2) поверне як і ('A', 'B') так і ('B', 'A') (окрім інших), а itertools.combinations('ABC', 2) не поверне ('B', 'A') тому що це дублікат ('A', 'B') в іншому порядку.


>>> names = list(open('examples/favorite-people.txt', encoding='utf-8')) 
>>> names ['Dora\n', 'Ethan\n', 'Wesley\n', 'John\n', 'Anne\n',
'Mike\n', 'Chris\n', 'Sarah\n', 'Alex\n', 'Lizzie\n']

У такий спосіб можна отримати список рядків файла.

>>> names = [name.rstrip() for name in names] 
>>> names 
['Dora', 'Ethan', 'Wesley', 'John', 'Anne',
'Mike', 'Chris', 'Sarah', 'Alex', 'Lizzie']

На жаль, попередній спосіб також читає з файлу символи переходу на новий рядок, тому ми застосовуємо списковий вираз та рядковий метод rstrip(), щоб видалити зайві розмежовувальні символи з кінця рядка. (Рядки також мають метод lstrip() для видалення їх на початку та метод strip() для очистки невидимих символів з обох кінців).

>>> names = sorted(names) 
>>> names 
['Alex', 'Anne', 'Chris', 'Dora', 'Ethan',
'John', 'Lizzie', 'Mike', 'Sarah', 'Wesley']

Функція sorted() приймає список, і повертає його відсортованим. За замовчуванням, вона сортує за алфавітом. В даному випадку ми передали функцію len(), тому рядки сортуються за довжиною.

>>> names = sorted(names, key=len) 
>>> names ['Alex', 'Anne', 'Dora', 'John', 'Mike',
'Chris', 'Ethan', 'Sarah', 'Lizzie', 'Wesley']

Але також їй в параметр key функцію, і тоді список відсортується за зростанням значень цієї функції.


Але яким чином це пов’язано з модулем itertools? Радий що ви спитали.

…продовжуючи попередній сеанс інтерактивної оболонки…
>>> import itertools
>>> groups = itertools.groupby(names, len)  
>>> groups
<itertools.groupby object at 0x00BB20C0>
>>> list(groups)
[(4, <itertools._grouper object at 0x00BA8BF0>),
 (5, <itertools._grouper object at 0x00BB4050>),
 (6, <itertools._grouper object at 0x00BB4030>)]

Функція itertools.groupby() приймає два значення - ітератор, і функцію, і повертає ітератор з парами. Першим елементом пари є значення функції, а другим - ітератор що містить всі елементи вхідної послідовності з таким самим значенням функції.

>>> groups = itertools.groupby(names, len)

Виклик функції list() "спорожнив" ітератор, тобто ви вже згенерували кожен елемент в ітераторі для того щоб утворити список, і більше їх там немає. Ніякої кнопки "перезавантажити" на ітераторі немає, і почати все заново неможливо, тому доводиться створювати ітератор спочатку.

>>> for name_length, name_iter in groups:    
...     print('Names with {0:d} letters:'.format(name_length))
...     for name in name_iter:
...         print(name)
... 
Names with 4 letters:
Alex
Anne
Dora
John
Mike
Names with 5 letters:
Chris
Ethan
Sarah
Names with 6 letters:
Lizzie
Wesley

В цьому прикладі, коли функція itertools.groupby() отримує вже відсортований список значень names, і поміщає всі чотирибуквенні імена в один ітератор, всі п’ятибуквенні в інший, і так далі. Функція groupby цілком загальна. Вона може групувати рядки за першою буквою, числа за кількістю дільників, чи за будь-якою іншою функцією яку ви придумаєте.

Функція itertools.groupby() працює лише коли вхідна послідовність вже відсортована за фунцією за якою групується. В прикладі вище ми групували її за функцією len(). Це працювало лише тому, що вхідний список вже був відсортованим за цією функцією.


Ви дивитесь уважно?

>>> list(range(0, 3))
[0, 1, 2] 
>>> list(range(10, 13))
[10, 11, 12] 
>>> list(itertools.chain(range(0, 3), range(10, 13)))
[0, 1, 2, 10, 11, 12]

Функція chain() бере два ітератори, і повертає ітератор, який генерує спочатку всі елементи з першого ітератора, а потім з другого. (Взагалі вона може брати довільне число ітераторів і з’єднувати їх послідовно в тому порядку в якому їх передали).

>>> list(zip(range(0, 3), range(10, 13)))
[(0, 10), (1, 11), (2, 12)]

Функція zip() робить дещо прозаїчне, яке щоправда іноді виявляється надзвичайно корисним: вона бере довільне число послідовностей, і повертає ітератор який генерує кортежі що містять перші елементи всіх послідовностей, потім другі елементи, потім треті, і так далі.

>>> list(zip(range(0, 3), range(10, 14)))
[(0, 10), (1, 11), (2, 12)]

Функція zip() зупиняється на кінці найкоротшої послідовності. range(10, 14) має чотири елементи: (10, 11, 12, 13), а range(0, 3) всього лиш 3, тому функція zip() поверне ітератор з трьох елементів.

>>> list(itertools.zip_longest(range(0, 3), range(10, 14)))
[(0, 10), (1, 11), (2, 12), (None, 13)]

Правда є функція itertools.zip_longest(), яка повертає ітератор що містить стільки елементів, скільки їх є в найдовшій послідовності, заповнюючи нестачу в інших послідовностях значеннями None.

Гаразд, це все було дуже цікаво. Але як воно пов’язане з розв’язувачем ребусів? А ось як:

>>> characters = ('S', 'M', 'E', 'D', 'O', 'N', 'R', 'Y') 
>>> guess = ('1', '2', '0', '3', '4', '5', '6', '7') 
>>> tuple(zip(characters, guess))
(('S', '1'), ('M', '2'), ('E', '0'), ('D', '3'),
 ('O', '4'), ('N', '5'), ('R', '6'), ('Y', '7'))

Отримавши список букв та цифр, функція zip() створює впорядкований список пар.

>>> dict(zip(characters, guess))
{'E': '0', 'D': '3', 'M': '2', 'O': '4',
 'N': '5', 'S': '1', 'R': '6', 'Y': '7'}

І це добре тому що така структура даних підходяща для функції dict() і створення словника, в якому кожній букві відповідає цифра. (Це не єдиний спосіб створити такий словник. Можна також використати словникові вирази, аби створити словник напряму). І хоча як видно з лістингу вище в словнику пари йдуть зовсім в іншому порядку (словники не мають порядку за означенням), але ми можемо бачити що кожному символу відповідає та буква, яка відповідала йому в порядку який був в оригінальних списках characters і guess.

Розв’язувач ребусів використовує такий метод щоб створити словник відповідностей між буквами в головоломці і цифрами в розв’язку.

characters = tuple(ord(c) for c in sorted_characters)
digits = tuple(ord(c) for c in '0123456789')
...
for guess in itertools.permutations(digits, len(characters)):
    ...
    equation = puzzle.translate(dict(zip(characters, guess)))

Але що це за метод translate()? Ах, тут ми нарешті добрались до справді цікавої частини.


* * *


Новий спосіб маніпуляцій над рядками[ред.]

Рядки в мові Python мають багато методів. Ви дізнались про деякі з них в розділі Текст. Зараз я хочу показати вам зручну, проте маловідому техніку роботи з рядками: метод translate().

>>> translation_table = {ord('A'): ord('O')}

Перетворення рядка починається з створення таблиці перетворення, яка є простим словником який відображає один символ на інший. Щоправда вживати тут слово "символ" не цілком вірно. Таблиця насправді задає відношення між байтами.

>>> translation_table 
{65: 79}

Пам’ятайте, байти в Python 3 - це цілі числа. Функція ord() повертає номер в символа в таблиці ASCII, який, у випадку символів від A до Z, завжди є байтом між 65 і 90.

>>> 'MARK'.translate(translation_table)
'MORK'

Метод рядка translate() отримує таблицю перетворення і пробігається по рядку з нею. Як тільки він значення для якого є ключ в таблиці, він замінює його значенням з таблиці. В даному випадку замінює рядок 'MARK' на 'MORK'.

Нарешті ми добрались до справді цікавого.

І що тут має відношення до розв’язування ребусів? Як виявляється - все.

>>> characters = tuple(ord(c) for c in 'SMEDONRY') 
>>> characters 
(83, 77, 69, 68, 79, 78, 82, 89)

Використовуючи генераторний вираз ми швидко обчислюємо значення байтів для символів в рядку.

>>> guess = tuple(ord(c) for c in '91570682') 
>>> guess 
(57, 49, 53, 55, 48, 54, 56, 50)

Іншим генераторним виразом обчислюємо значення байтів для символів цифр які їм відповідають.

>>> translation_table = dict(zip(characters, guess)) 
>>> translation_table 
{68: 55, 69: 53, 77: 49, 78: 54, 79: 48, 82: 56, 83: 57, 89: 50}

Таблиця перетворення будується за допомогою з’єдання цих двох послідовностей в пари, і конструювання з цих пар словника.

>>> 'SEND + MORE == MONEY'.translate(translation_table)
'9567 + 1085 == 10652'

І нарешті, ми передаємо таблицю перетврення в метод translate() оригінального тексту головоломки. Це перетворює кожну букву головоломки на відповідну цифру. Результатом є правильний вираз мови Python, записаний як рядок.

Це досить вражаюче. Але що робити з рядком який є правильним виразом мови Python?


* * *


Обчислення рядків як виразів Python[ред.]

Це остання частина головоломки (чи точніше остання частина розв'язувача головоломок). Після всіх тих незвичних маніпуляцій, ми отримуємо рядок на зразок '9567 + 1085 == 10652'. Але це рядок, а що доброго в рядку? Тут з’являється eval() - універсальний інструмент обчислень в Python.

>>> eval('1 + 1 == 2')
True
>>> eval('1 + 1 == 3')
False
>>> eval('9567 + 1085 == 10652')
True

Але зачекайте, є ще! eval() не обмежується булевими виразами. Вона може обробляти будь-який вираз, та повертати будь-який тип даних.

>>> eval('"A" + "B"')
'AB'
>>> eval('"MARK".translate({65: 79})')
'MORK'
>>> eval('"AAAAA".count("A")')
5
>>> eval('["*"] * 5')
['*', '*', '*', '*', '*']

Але зачекайте, це ще не все!

>>> x = 5
>>> eval("x * 5")
25

Вираз який показує, що eval() може використовувати глобальні змінні описані за його межами. А також локальні змінні, якщо виклик відбувається з функції.

>>> eval("pow(x, 2)")
25

Та функції.

>>> import math 
>>> eval("math.sqrt(x)")  
2.2360679774997898

Та модулі.

Ей, зачекайте хвилинку…

>>> import subprocess 
>>> eval("subprocess.getoutput('ls ~')")
'Desktop         Library         Pictures \
 Documents       Movies          Public   \
 Music           Sites'

Модуль subprocess дозволяє запускати консольні команди, та отримувати результат в вигляді рядка.

>>> eval("subprocess.getoutput('rm /some/random/file')")

Деякі консольні команди можуть мати незворотні наслідки.

Ситуація стає ще гіршою від того, що існує глобальна функція __import__(), яка отримує ім'я модуля як рядок, імпортує модуль, та повертає посилання на нього. Якщо це поєднати з силою eval() можна створити один єдиний вираз що знищить всі ваші файли.

>>> eval("__import__('subprocess').getoutput('rm /some/random/file')")

Тепер уявіть вивід 'rm -rf ~'. Хоча насправді не буде ніякого виводу, але також у вас більше не буде ніяких файлів.


eval() - зло


Емм, злою частиною є обчислення довільних виразів з ненадійних джерел. Ви маєте використовувати eval() тільки на довірених джерелах. Звичайно, фокус в тому щоб визначити що є "довіреним". Але є щось, що я знаю точно: ви НЕ маєте брати цей зручний калькулятор, та розміщувати його в інтернет як милий маленький веб-сервіс. Не робіть помилку думаючи, "Ох, функція робить багато маніпуляцій з рядками перед тим як почати їх обчислення; Важко уявити, як хтось може проексплоїтити це." Хтось ВИЯСНИТЬ як протягнути небезпечний код крізь всі маніпуляції з рядками (ставались і дивніші речі), а після цього ви можете попрощатись з сервером.

Чи існує хоч якийсь спосіб безпечного обчислення виразів? Щоб поставити eval() в певні обмеження, за які він не зможе вийти, щоб зашкодити зовнішньому світу? Ну, насправді і так, і ні.

>>> x = 5 
>>> eval("x * 5", {}, {})
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "<string>", line 1, in <module> 
NameError: name 'x' is not defined

Другий та третій параметри, що передаються функції eval(), стають глобальним та локальним просторами імен для виразу. В даному випадку вони обидва порожні. Це означає, що при обчисленні рядка "x * 5" немає посилання на змінну x ні в глобальному ні в локальному просторі імен, тому eval() закінчить роботу і згенерує виняток.

>>> eval("x * 5", {"x": x}, {})

Можна вибірково включати певні змінні в глобальний простір імен, перелічивши кожну індивідуально. Тоді ті, і тільки ті змінні будуть доступними під час обчислення.

>>> import math >>> eval("math.sqrt(x)", {"x": x}, {}) Traceback (most recent call last):

 File "<stdin>", line 1, in <module>
 File "<string>", line 1, in <module> 

NameError: name 'math' is not defined </source>

Незважаючи на те, що ви імпортували модуль math, ви не додали його в простір імен, що передається функції eval(), тому обчислення не вдається.

Ех, це ж просто! Давайте я тепер зроблю веб-сервіс для розв’язування ребусів!

>>> eval("pow(5, 2)", {}, {})
25

Незважаючи на те, що ви передали порожні словники, як глобальний та локальний простори імен, всі вбудовані функції Python досі доступні в процесі обчислень. Тому pow(5,2), працює, тому що 5 та 2 - літерали, а pow() - вбудована функція.

>>> eval("__import__('math').sqrt(5)", {}, {})  
2.2360679774997898

На жаль (і якщо ви не зрозуміли чому "на жаль" - прочитайте розділ спочатку), функція __import__() - також є вбудованою, та також працює.

Ага, це означає, що ви все ще можете нашкодити, навіть якщо будете передавати в eval() порожні простори імен.

>>> eval("__import__('subprocess').getoutput('rm /some/random/file')", {}, {})

Ойойой, який я радий що ще не зробив веб-сервіс. Так чи існує якийсь спосіб безпечного використання eval()?

>>> eval("__import__('math').sqrt(5)",
...     {"__builtins__":None}, {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module> 
NameError: name '__import__' is not defined

Щоб безпечно обчислити вирази яким не довіряєте, ви маєте описати глобальний простір імен, що присвоює "__builtins__" None, тобто відсутність будь-яких значень. Загалом всі вбудовані функції зберігаються всередині псевдо-модуля названого "__builtins__". Цей псевдо-модуль (набір вбудованих функцій) доступний функції eval() за замовчуванням, якщо ви не заміните його явно.

>>> eval("__import__('subprocess').getoutput('rm -rf /')",
...     {"__builtins__":None}, {})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module> 
NameError: name '__import__' is not defined

Переконайтесь що ви перевантажили __builtins__. Не __builtin__, __built-ins__, чи ще якийсь варіант, що буде працювати прекрасно, але зовсім не зачепить набору вбудованих функцій.

То eval() тепер безпечний? Ну, і так і ні.

>>> eval("2 ** 2147483647",
...     {"__builtins__":None}, {})

Навіть без доступу до __builtins__, все ще можна запустити DoS атаку. Наприклад спроба піднести 2 до великого степеня, повністю завантажить процесор вашого комп'ютера на деякий час. (Якщо ви пробуєте це в консолі інтерпретатора, натисніть Ctrl-C кілька разів, щоб перервати це). Технічно цей вираз швидко поверне значення, проте в той час ваш сервер не зможе робити нічого іншого.

В кінці кінців, можливо безпечно виконувати невідомі вирази Python, для певного визначення "безпеки", яке виявляється не надто корисним в реальному житті. Це нормально, якщо ви просто експериментуєте, і нормально, якщо ви передаєте на обчислення текст якому довіряєте. Але все інше - лиш напрошуватись на зайві неприємності.


* * *


Зібравши все до купи[ред.]

Нагадаю: ця програма розв’язує математичні ребуси грубою силою, тобто повним перебором всіх можливих розв’язків. Щоб це здійснити, вона:

  1. Знаходить всі букви головоломки за допомогою функції re.findall().
  2. Знаходить множину різних букв за допомогою функції set().
  3. Пересвідчується що букв не більше 10 (що означатиме нерозв’язність головоломки) за допомогою інструкції assert
  4. Генерує ASCII коди букв.
  5. Перебирає всі можливі розв’язки за допомогою функції itertools.permutations()
  6. Перетворює всі можливі розв’язки на вирази мови Python використовуючи метод рядкових об’єктів translate().
  7. Перевіряє кожен розв’язок обчислюючи його вираз функцією eval()
  8. Повертає перший розв’язок вираз для якого є істинним.

... всього лише за 14 рядків коду.


* * *


Що читати далі?[ред.]

Модульне тестування[ред.]

Переконаності недостатньо для впевеності. Ми були цілком переконані в багатьох речах, які насправді не були.

Certitude is not the test of certainty. We have been cocksure of many things that were not so.
Олівер Венделл Холмс молодший

(Не) занурення[ред.]

Ох сьогоднішні діти. Зіпсуті цими швидкими комп'ютерами і модними "динамічними" мовами. Спочатку напиши, потім публікуй, і вже (якщо дійдуть руки) на третьому місці - зневадження. В наші часи в нас була дисципліна. Дисципліна, Я сказав! Ми мусили писати програми від руки, на папері, і згодовувати їх комп'ютеру на пефокартах. І нам це подобалось!

У цьому розділі, ми збираємось написати та зневадити набір допоміжних функцій для перетворення чисел в римську систему та навпаки. Ви бачили механізми створення та перевірки чисел на приналежність до римських в попередньому прикладі. Тепер давайте відійдемо трохи назад і подумаємо над тим що потрібно зробити щоб перетворити це на двосторонній інструмент.

Правила для римських чисел приводять до багатьох цікавих спостережень.

  1. Існує тільки один коректний спосіб представити конкретне число як римське.
  2. Аналогічно і навпаки: якщо рядок символів є правильним римським числом, він представляє собою лише одне число (може бути прочитане однозначно).
  3. Існує обмежений діапазон чисел що можуть бути представлені в римській системі, якщо конкретно - то цілі від 1 до 3999. Римляни мали кілька способів запису більших чисел, наприклад поміщуючи вертикальну риску над числом щоб позначити те що його звичайне значення слід домножити на 1000. Та протягом цього розділу будемо вважати що римські числа простягаються лише від 1 до 3999.
  4. Не існує способу представити 0 в римській системі.
  5. Також римляни не вміли записувати від'ємні числа.
  6. Як і частки дробових чисел.

Давайте почнемо окреслювати те що повинен робити модуль roman.py. Він матиме дві головні функції, to_roman() та from_roman(). Функція to_roman() повинна отримати на вхід ціле число в діапазоні 1..3999 і повернути рядок з записом числа в римській системі...

Зупиніться прямо тут. Тепер давайте зробимо дещо неочікуване: напишемо тест який пеервіряє що функція to_roman() робить те що ми хочемо. Ви це правильно прочитали, ми збираємось написати код що тестує код який ми ще навіть не написали.

Це називається розробкою керованою тестами (англ. test-driven development), чи TDD. Набір двох функцій перетворення - to_roman() та from_roman(), можна написати і тестувати як модуль, окремо від будь-якої більшої програми що їх використовує, і в такому випадку тести називаються модульними. Python має фреймворк для модульного тестування - відповідно названий модуль unittest.

Модульне тестування є важливою частиною стратегії розробки що фокусується на тестуванні. Коли ви пишете модульні тести, важливо писати їх рано, і постійно їх оновлювати протягом того як код та вимоги до нього змінюються. Багато людей пропагують написання тестів перед написанням коду який тестуєтьс, і це підхід який я збираюсь продемонструвати в цьому розділі. Але модульні тести корисні не залежно від того коли ви їх напишете.

  • Написання тестів перед написанням коду змушує вас деталізувати вимоги в досить корисній формі.
  • Написання тестів перед написанням коду запобігає надмірній розробці. Якщо всі тести проходяться значить функцію завершено.
  • При рефакторингу вони допомагають довести що нова версія поводиться так само як і стара.
  • При підтримці коду тести допомагають прикрити власну задницю коли хтось підходить з криком що ваша остання зміна поламала роботу їх старого коду. ("Але пане, всі тести проходяться, я перевірив..")
  • При написанні коду в команді, підтримка вичерпного набору тестів на порядок зменшує шанси того що ваш код поламає якийсь інший код, тому що ви можете спершу запустити їхні модульні тести. (Я спостерігав такі речі під час спринтів розробки. Команда розбиває завдання, кожен бере специфікацію для своєї частини, пише тести для неї, потім ділиться своїми тестами з рештою команди. Таким чином, ніхто не заходить занадто далеко в розробці коду який не надто добре працює з кодом інших).


* * *


Єдине питання[ред.]

Кожен тест - це острів.

Тест відповідає на єдине запитання про код що тестується. Тест повинен бути здатним...

  • ... працювати цілком самостійно, без жодного людського вводу. Модульне тестування - це автоматизація.
  • ... самостійно визначати чи функція що він тестує пройшла тест чи його завалила, без людини що інтерпретує результат.
  • ... запускатись повністю окремо від інших тестів (навіть якщо вони тестують ту саму функцію). Кожен тест - це острів.

Знаючи це, давайте створимо тест для першої вимоги:

  1. Функція to_roman() повинна повертати римський запис будь-якого цілого числа від 1 до 3999.

Зразу не дуже ясно як код нижче робить..., ну хоч що-небудь. Він описує клас без методу __init__(). Клас має інший метод, але він ніде не викликається. Скрипт має блок __main__, але цей блок не звертається до класу чи його методу. Але дещо він робить, я обіцяю.

import roman1
import unittest

class KnownValues(unittest.TestCase):               
    known_values = ( (1, 'I'),
                     (2, 'II'),
                     (3, 'III'),
                     (4, 'IV'),
                     (5, 'V'),
                     (6, 'VI'),
                     (7, 'VII'),
                     (8, 'VIII'),
                     (9, 'IX'),
                     (10, 'X'),
                     (50, 'L'),
                     (100, 'C'),
                     (500, 'D'),
                     (1000, 'M'),
                     (31, 'XXXI'),
                     (148, 'CXLVIII'),
                     (294, 'CCXCIV'),
                     (312, 'CCCXII'),
                     (421, 'CDXXI'),
                     (528, 'DXXVIII'),
                     (621, 'DCXXI'),
                     (782, 'DCCLXXXII'),
                     (870, 'DCCCLXX'),
                     (941, 'CMXLI'),
                     (1043, 'MXLIII'),
                     (1110, 'MCX'),
                     (1226, 'MCCXXVI'),
                     (1301, 'MCCCI'),
                     (1485, 'MCDLXXXV'),
                     (1509, 'MDIX'),
                     (1607, 'MDCVII'),
                     (1754, 'MDCCLIV'),
                     (1832, 'MDCCCXXXII'),
                     (1993, 'MCMXCIII'),
                     (2074, 'MMLXXIV'),
                     (2152, 'MMCLII'),
                     (2212, 'MMCCXII'),
                     (2343, 'MMCCCXLIII'),
                     (2499, 'MMCDXCIX'),
                     (2574, 'MMDLXXIV'),
                     (2646, 'MMDCXLVI'),
                     (2723, 'MMDCCXXIII'),
                     (2892, 'MMDCCCXCII'),
                     (2975, 'MMCMLXXV'),
                     (3051, 'MMMLI'),
                     (3185, 'MMMCLXXXV'),
                     (3250, 'MMMCCL'),
                     (3313, 'MMMCCCXIII'),
                     (3408, 'MMMCDVIII'),
                     (3501, 'MMMDI'),
                     (3610, 'MMMDCX'),
                     (3743, 'MMMDCCXLIII'),
                     (3844, 'MMMDCCCXLIV'),
                     (3888, 'MMMDCCCLXXXVIII'),
                     (3940, 'MMMCMXL'),
                     (3999, 'MMMCMXCIX'))           

    def test_to_roman_known_values(self):           
        '''to_roman should give known result with known input'''
        for integer, numeral in self.known_values:
            result = roman1.to_roman(integer)       
            self.assertEqual(numeral, result)       

if __name__ == '__main__':
    unittest.main()

① Щоб створити тест, спочатку створіть підклас класу TestCase з модуля unittest. Цей клас надає багато корисних методів які можна використовувати в тестах для перевірки різноманітних умов.

② Це кортеж з пар число/його римський запис, які я перевірив вручну. Він включає десять найменших чисел, найбільше число, кожне римське число що записується одним символом, і випадковий набір інших чисел. Не потрібно тестувати будь-який можливий випадок, але потрібно намагатись протестувати всі очевидні крайні випадки.

③ Кожен тест - це окремий метод. Метод тесту не приймає параметрів, не повертає даних, і повинен мати ім'я що починається з чотирьох символів: test. Якщо виклик методу завершується без винятків, тест вважається пройденим, якщо метод кидає виняток, тест вважається проваленим.

④ Ось тут ми робимо виклик до справжньої функції to_roman(). (Ну, функція все ще не написана, але як тільки вона буде, цей рядок її буде викликати.) Зауважте, що зараз ви описали API функції to_roman(): вона повинна приймати ціле значення (число для перетворення) і повертати рядок (його римський запис). Якщо API буде іншим, тест провалиться. Також зауважте що ми не ловимо жодних винятків при виклику to_roman(). Це навмисне. to_roman не повинен генерувати виняток коли йому передають дозволені значення, а всі перелічені значення дозволені. Якщо to_roman() згенерує виняток тест також провалиться.

⑤ Вважаючи що функція to_roman() була описана вірно, викликана вірно, завершилась успішно, і повернула значення, останнім кроком є перевірка того що повернене значення правильне. Це типова перевірка, і клас TestCase надає метод, assertEqual, для перевірки рівності двох значень. Якщо результат повернений з to_roman() не співпадає з очікуваним значенням, assertEqual, згенерує виняток і тест провалиться. Якщо два значення рівні, assertEqual нічого не зробить. Якщо кожне значення повернуте функцією to_roman() співпадає з очікуваним, assertEqual ніколи не згенерує виняток, тому test_to_roman_known_values з часом завершиться нормально, що означатиме що to_roman() пройшла тест.

Напишіть тест який валиться, тоді пишіть код поки тест не буде пройдено.

Після того як ви написали тест, можна починати кодити функцію to_roman(). Спершу, ви повинні створити заглушку з порожньої функції, та переконатись що тест валиться. Якщо тест успішний до того як ви написати код, ваші тести взагалі не тестують код! Модульне тестування - як танці: тести ведуть, код слідує. Напишіть тест який валиться, тоді програмуйте поки тест не буде пройдено.

# roman1.py

def to_roman(n):
    '''convert integer to Roman numeral'''
    pass

На даному етапі, ми хочемо описати сигнатуру функції to_roman(), але ми ще не хочемо писати її код. (Спершу потрібно щоб тест запустився). Щоб створити заглушку використаємо ключове слово pass, яке вказує на те, що нічого не потрібно робити.

Виконайте romantest1.py в командному рядку щоб запустити тест. Якщо передати при виклику опцію -v, ми отримаємо більш детальний вивід, і зможемо побачити що відбувається при запуску кожного тесту. Наш вивід виглядатиме приблизно так, навіть якщо пощастить:

you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
test_to_roman_known_values (__main__.KnownValues) ①
to_roman should give known result with known input ... FAIL ②

======================================================================
FAIL: to_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest1.py", line 73, in test_to_roman_known_values
    self.assertEqual(numeral, result)
AssertionError: 'I' != None ③
----------------------------------------------------------------------
Ran 1 test in 0.016s ④

FAILED (failures=1) ⑤

① Запуск скрипта запускає unittest.main(), який виконує кожен тест. Кожен тестовий приклад є окремим методом класу. Обов'язкового формату організації цих класів немає, кожен з них може містити єдиний тестовий метод, чи ви можете мати один клас що містить багато методів. Єдина вимога - кожен клас повинен наслідуватись від unittest.TestCase.

② Для кожного тестового методу модуль unittest роздрукує його рядок документації та результат проходження тесту. Як і очікувалось наш тест валиться.

③ Для кожного провального тесту, unittest виводить трейсбек що показує що саме трапилось. В даному випадку, виклик assertEqual() згенерував AssertionError тому що очікувалось що to_roman(1) поверне 'I', а вона не повернула. (Так як явного виклику return не було, функція повернула None - позначення "нічого" в Python).

④ Після детального опису кожного тесту, unittest показує підсумок того скільки тестів було виконано, і скільки часу це зайняло.

⑤ Загалом, тестування провалено, тому що принаймі один з тестів не пройдено. Коли тест валиться, unittest відрізняє провали та помилки. Провал - це виклик методів assertXYZ, таких як assertEqual чи assertRaises, які провалюютьсяч тому що умова не виконана, наприклад виняток не згенерований. Помилка - будь-який інший виняток згенерований з коду що тестується, чи самого методу що тестує.

Ну, і нарешті, ми можемо написати функцію to_roman().

roman_numeral_map = (('M',  1000),
                     ('CM', 900),
                     ('D',  500),
                     ('CD', 400),
                     ('C',  100),
                     ('XC', 90),
                     ('L',  50),
                     ('XL', 40),
                     ('X',  10),
                     ('IX', 9),
                     ('V',  5),
                     ('IV', 4),
                     ('I',  1))                 

def to_roman(n):
    '''convert integer to Roman numeral'''
    result = ''
    for numeral, integer in roman_numeral_map:
        while n >= integer:                     
            result += numeral
            n -= integer
    return result

roman_numeral_map - кортеж з кортежів який описує три речі: символьне представлення базових римських чисел, їх порядок (спадний, від M аж до I), та значення кожного числа. Кожен внутрішній кортеж - це пара (numeral, value). Це не тільки односимвольні римські числа, тут також описуються двосимвольні пари на зразок CM (“на сотню менше ніж тисяча”). Це дозволяє спростити код функції to_roman(). ② В цьому місці структура даних roman_numeral_map себе окуповує, тому що нам не потрібна жодна спеціальна логіка для опрацювання правила віднімання. Для перетворення числа в римське, просто будемо проходитись по roman_numeral_map шукаючи найбільше ціле число що менше чи рівне за вхідне. Як тільки ми його знайдемо, треба додати його в кінець результату, відняти відповідне ціле від вхідного значення, намилити, змити і повторити :).

Якщо все ще не ясно як працює функція to_roman(), додайте виклик print() до кінця циклу while:

while n >= integer:
    result += numeral
    n -= integer
    print('subtracting {0} from input, adding {1} to output'.format(integer, numeral))

Результат роботи цього коду виглядатиме якось так:

>>> import roman1
>>> roman1.to_roman(1424)
subtracting 1000 from input, adding M to output
subtracting 400 from input, adding CD to output
subtracting 10 from input, adding X to output
subtracting 10 from input, adding X to output
subtracting 4 from input, adding IV to output
'MCDXXIV'

Отож, функція to_roman() ніби працює, щонайменше при цій ручній перевірці. Але чи вона пройде тест що ми написали?

you@localhost:~/diveintopython3/examples$ python3 romantest1.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok               ①

----------------------------------------------------------------------
Ran 1 test in 0.016s

OK

① Ура! Функція to_roman() проходить тест з відомими значеннями. Це звичайно не повний тест, але він прогяняє функцію крізь різноманітні вхідні дані, зокрема вхідні що перетворюються на кожне однолітерне римське число, найбільший можливий ввід, і ввід що створює найдовше можливе римське число (3888). В даний момент ви можете мати цілком обґрунтовану впевненість що функція працює для будь-яких дозволених значень які ви їй передасте.

"Дозволений" вхід? Хммм. А як там з недозволеним?


* * *


Зупинитись та загорітись[ред.]

Пітонівський спосіб зупинитись і загорітись - кинути виняток.

Недостатньо тестувати те що функція працює успішно при правильному вводі, також потрібно перевірити те що функція відмовляється працювати на недозволених вхідних даних. І це не повинна бути якась випадкова відмова, вони повинні провалюватись в спосіб який ми очікуємо.

>>> import roman1
>>> roman1.to_roman(4000)
'MMMM'
>>> roman1.to_roman(5000)
'MMMMM'
>>> roman1.to_roman(9000) 
'MMMMMMMMM'

① Це явно те не що ми хотіли - таких римських чисел взагалі не існує! Насправді кожне з вхідних неможливо представити в римській системі, але те що повертає функція все одно неприпустимо. Тихо повертати неправильні результати дууууже погааааано. Якщо програма збирається зробити помилку, набагато краще коли вона впаде швидко і шумно. "Halt and catch fire", як то кажуть. В Python аналогом інструкції halt and catch fire є генерація винятку.

Питання яке ми мусимо собі задати "Як виразити це як вимогу яку можна протестувати?". Для початку ось як:

Функція to_roman() повинна згенерувати виняток OutOfRangeError коли отримає на вхід ціле число більше ніж 3999.

Як виглядатиме тест?

import unittest, roman2
class ToRomanBadInput(unittest.TestCase):                                 
    def test_too_large(self):                                             
        '''to_roman should fail with large input'''
        self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)  

① Як і в попередньому тесті, ми створємо клас що наслідується від unittest.TestCase. Ви можете вкладати більш ніж один тест всередину класу (як буде продемонстровано далі в цьому розділі), але я б надав перевагу окремому клас, тому що цей тест перевіряє дещо інше ніж попередній. Ми зберемо всі тести з гарними вхідними даними в одному класі, а всі тести з поганими вхідними даними - в іншому.

② Як і в попередньому тесті, сам тест - це метод класу, ім’я якого починається з префіксу test.

unittest.TestCase має метод assertRaises, який приймає такі аргументи: тип очікуваного винятку, функція що тестується, і аргументи що потрібно передати функції. (Якщо функція приймає багато різних аргументів передавайте їх в такому ж порядку і вони будуть передані далі.)

Добре придивіться до останнього рядка. Замість того щоб викликати to_roman() прямо, і вручну перевірити що вона генерує конкретний виняток (загортанням у блок try...except), ми дозволяємо методу assertRaises зробити все за нас. Єдине що ми робимо - уточняємо якого винятку ми очікуємо (roman2.OutOfRangeError), яку функцію викликати (to_roman()), та з якими аргументами (4000).

Також зверніть увагу що ви передаєте функцію to_roman() як аргумент, ви не викликаєте її, і не передаєте її ім’я як рядок. Я вже згадував як зручно те що в Python все - об’єкт?

Отож, що відбудеться коли ми запустимо тестування разом з новими тестами?

you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ERROR ①

======================================================================
ERROR: to_roman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
 File "romantest2.py", line 78, in test_too_large
  self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
AttributeError: 'module' object has no attribute 'OutOfRangeError' ②

----------------------------------------------------------------------
Ran 2 tests in 0.000s FAILED (errors=1)

① Ви повинні були очікувати що результатом тесту буде "fail" (провал) (так як ми ще не написали код який міг би його пройти), але насправді замість "fail" ми отримали "error". Різниця між ними тонка, але важлива. Тест може завершитись трьома способами: бути пройденим, провалитись, або повернути помилку. Провал тесту означає те що код, що тестується, повернув неправильні дані. Помилка тесту означає що код, що тестується, взагалі не вдалось виконати. ② Чому код не виконався правильно? Трейсбек все пояснює. Модуль який ми тестуємо не містить винятку що називається OutOfRangeError. Пам'ятаєте, ми передали цей виняток в метод assertRaises(), тому що це виняток який ми очікуємо від функції, що отримує дані в недозволеному діапазоні. Але такий виняток не існує, тому нам навіть не вдається викликати assertRaises(), який не отримує можливості протестувати функцію to_roman(), він просто до цього ще не дійшов.

Щоб розв'язати цю проблему потрібно описати OutOfRangeError в roman2.py.

class OutOfRangeError(ValueError):  
    pass                            

① Винятки є класами. Помилка "недозволений діапазон" - різновид помилки значення (ValueError) - значення аргументу не є дозволеним. Тому цей виняток наслідується від вбудованого винятку ValueError. Це не є обов'язковою вимогою (можна просто наслідуватись від базового класу Exception), але це видається правильним. ② Винятки нічого не роблять, але клас повинен мати хоча б один рядок коду. Виклик pass нічого не робить, але це рядок коду, тому клас створюється.

Тепер давайте знову запустимо тести.

you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... FAIL ①

======================================================================
FAIL: to_roman should fail with large input
----------------------------------------------------------------------
Traceback (most recent call last):
 File "romantest2.py", line 78, in test_too_large
  self.assertRaises(roman2.OutOfRangeError, roman2.to_roman, 4000)
AssertionError: OutOfRangeError not raised by to_roman ②

----------------------------------------------------------------------
Ran 2 tests in 0.016s

FAILED (failures=1)

① Новий тест все ще не проходиться, але принаймі виконується без помилок, і повідомляє про провал. Це прогрес! Це означає що виклик assertRaises() цього разу був успішним, і фреймворк unittest справді тестував функцію to_roman(). ② Звісно, функція to_roman() не генерує щойностворений виняток OutOfRangeError, бо ми поки що не наказували їй це робити. Але це добре! Це означає що це правильний тест, він провалюється до того як ви напишете код, що його проходить.

Тепер можна писати код щоб цей тест проходився.

def to_roman(n):
    '''convert integer to Roman numeral'''
    if n > 3999:
        raise OutOfRangeError('number out of range (must be less than 4000)')

    result = ''
    for numeral, integer in roman_numeral_map:
        while n >= integer:
            result += numeral
            n -= integer
    return result

Тут все прямолінійно: якщо даний ввід (n) більший за 3999, згенерувати виняток OutOfRangeError. Тест не перевіряє рядок з поясненням помилки для людей, хоча можна написати інший тест який це перевірятиме (правда потрібно стерегтись проблем з інтернаціоналізацією).

Цей код пройде тест? Давайте перевіримо.

you@localhost:~/diveintopython3/examples$ python3 romantest2.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_too_large (__main__.ToRomanBadInput) to_roman should fail with large input ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Ураа! Обидва тести пройдено. Завдяки тому, що ви працювали ітеративно, перідично займаючись то тестуванням, то кодом, ви можете бути впевненими що два рядки коду які ми щойно написали були причиною того що тест який спочатку провалювався почав проходитись. Цей вид впевненості не приходить дешево, але він окупиться впродовж життя вашого коду.


* * *


Більше зупинок, більше вогню[ред.]

Поряд з тестуванням чисел які занадто великі, потрібно тестувати числа які занадто малі. Як ми зауважили в функціональних вимогах, римський запис не може виражати 0 чи від'ємні числа.

>>> import roman2
>>> roman2.to_roman(0)
''
>>> roman2.to_roman(-1)
''

Ну, це не є добре. Давайте додамо тести для кожного з цих випадків.

class ToRomanBadInput(unittest.TestCase):
    def test_too_large(self):
        '''to_roman should fail with large input'''
        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 4000)  

    def test_zero(self):
        '''to_roman should fail with 0 input'''
        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)     

    def test_negative(self):
        '''to_roman should fail with negative input'''
        self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)    

① Метод test_too_large() не змінився з попереднього кроку. Я включаю його тут для того щоб показати куди додається новий код.

② Ось новий тетс: метод test_zero(). Як і метод test_too_large() він викликає метод assertRaises() що описаний в класі unittest.TestCase щоб викликати нашу функію to_roman() з параметром 0, і перевірити що вона генерує відповідний виняток, OutOfRangeError.

③ Метод test_negative() майже ідентичний, окрім того що він передає в функцію to_roman() значення -1. Якщо будь-який з цих методів не згенерує OutOfRangeError (тому що функція поверне якесь значення, чи тому що згенерує якийсь інший виняток), тест буде вважатись проваленим.

Тепер давайте перевіримо що тести валяться:

you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_negative (__main__.ToRomanBadInput)
to_roman should fail with negative input ... FAIL
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok
test_zero (__main__.ToRomanBadInput)
to_roman should fail with 0 input ... FAIL

======================================================================
FAIL: to_roman should fail with negative input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest3.py", line 86, in test_negative
    self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, -1)
AssertionError: OutOfRangeError not raised by to_roman

======================================================================
FAIL: to_roman should fail with 0 input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest3.py", line 82, in test_zero
    self.assertRaises(roman3.OutOfRangeError, roman3.to_roman, 0)
AssertionError: OutOfRangeError not raised by to_roman

----------------------------------------------------------------------
Ran 4 tests in 0.000s

FAILED (failures=2)

Чудово. Обидва тести провалились, як і очікувалось. Тепер пора переключитись на код, і глянути що можна зробити.

def to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 < n < 4000):                                              
        raise OutOfRangeError('number out of range (must be 1..3999)')  

    result = ''
    for numeral, integer in roman_numeral_map:
        while n >= integer:
            result += numeral
            n -= integer
    return result

① Це гарне скорочення Python: кілька порівнянь за раз. Воно еквівалентне if not ((0 < n) and (n < 4000)), але читається набагато простіше. Цей один рядок коду повинен виявляти ввід який завеликий, від'ємний чи нуль.

② Після того як ви змінили умову, змініть також і пояснення помилки. Фреймворку unittest все одно, але через неправильно описані винятки код буде важче зневаджувати.

Я міг би показати багато прикладів що демонструють те що багато порівнянь за раз працюють, але замість цього я просто запущу тести, і доведу це.

you@localhost:~/diveintopython3/examples$ python3 romantest3.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_negative (__main__.ToRomanBadInput)
to_roman should fail with negative input ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok
test_zero (__main__.ToRomanBadInput)
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.016s

OK


* * *


І ще одне[ред.]

Була ще одна функціональна вимога щодо перетворення чисел в римський запис: робота з нецілими числами.

>>> import roman3
>>> roman3.to_roman(0.5) 
''
>>> roman3.to_roman(1.0) 
'I'

① Ой, це погано.

② Ой, це навіть гірше. Обидва випадки повинні були б генерувати виняток, а замість цього вони повертають неприпустимі результати.

Тестуваня на нецілі нескладне. Спершу, давайте опишемо виняток NotIntegerError.

# roman4.py
class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass

Далі, напишімо тест що перевіряє на виняток NotIntegerError.

class ToRomanBadInput(unittest.TestCase):
    .
    .
    .
    def test_non_integer(self):
        '''to_roman should fail with non-integer input'''
        self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)

І перевіримо що тест правильно валиться.

you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_negative (__main__.ToRomanBadInput)
to_roman should fail with negative input ... ok
test_non_integer (__main__.ToRomanBadInput)
to_roman should fail with non-integer input ... FAIL
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok
test_zero (__main__.ToRomanBadInput)
to_roman should fail with 0 input ... ok

======================================================================
FAIL: to_roman should fail with non-integer input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest4.py", line 90, in test_non_integer
    self.assertRaises(roman4.NotIntegerError, roman4.to_roman, 0.5)
AssertionError: NotIntegerError not raised by to_roman

----------------------------------------------------------------------
Ran 5 tests in 0.000s

FAILED (failures=1)

І напишемо код що проходить тест.

def to_roman(n):
    '''convert integer to Roman numeral'''
    if not (0 < n < 4000):
        raise OutOfRangeError('number out of range (must be 1..3999)')
    if not isinstance(n, int):                                          
        raise NotIntegerError('non-integers can not be converted')      

    result = ''
    for numeral, integer in roman_numeral_map:
        while n >= integer:
            result += numeral
            n -= integer
    return result

① Вбудована функція isinstance() перевіряє чи належить змінна певному типу (чи, технічно, будь-якому типу нащадку).

② Якщо аргумент n - не типу int, згенерувати наш новий NotIntegerError.

І нарешті, перевірмо що наші зміни знову примусили тести проходитись.

you@localhost:~/diveintopython3/examples$ python3 romantest4.py -v
test_to_roman_known_values (__main__.KnownValues)
to_roman should give known result with known input ... ok
test_negative (__main__.ToRomanBadInput)
to_roman should fail with negative input ... ok
test_non_integer (__main__.ToRomanBadInput)
to_roman should fail with non-integer input ... ok
test_too_large (__main__.ToRomanBadInput)
to_roman should fail with large input ... ok
test_zero (__main__.ToRomanBadInput)
to_roman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

Функція to_roman() проходить всі тести, і я не можу придумати ніяких інших, тому час перейти до функції from_roman().


* * *


Приємна симетрія[ред.]

Перетворення рядка з Римського запису в арабський звучить складніше ніж перетворення з цілого в Римське. Звичайно є проблема з перевіркою правильності вводу. Простіше перевірити що ціле більше за нуль ніж перевірити що рядок - це дозволене римське число. Але ми вже сконструювали регулярний вираз для перевірки римських чисел, тому ця частина зроблена.

Але це залишає проблему самого перетворення. Як ми побачимо через хвилину, завдяки гарній структурі даних що ми описали для відображення окремих Римських чисел в цілі, дрібні деталі реалізації функції from_roman() такі ж прості як і в функції to_roman().

Але спочатку тести. Для першого тесту нам потрібні "відомі значення". Наш набір тестів, вже має такі значення, давайте використаємо їх повторно.

def test_from_roman_known_values(self):
        '''from_roman should give known result with known input'''
        for integer, numeral in self.known_values:
            result = roman5.from_roman(numeral)
            self.assertEqual(integer, result)

Тут є приємна симетрія. Функції to_roman() та from_roman() є оберненими одна до одної. Перша перетворює цілі числа в спеціальним чином відформатовані рядки, друга перетворює такі рядки в числа. В теорії, ми повинні бути здатними послати число "навкруг", передавши його в функцію to_roman() щоб отримати рядок, потім передавши цей рядок в функцію from_roman() щоб отримати ціле число, яке буде дорівнювати тому з якого ми починали

n = from_roman(to_roman(n)) для всіх n

В цьому випадку "для всіх" означає будь-яке число в межах 1..3999, так як тільки такі дозволено передавати в to_roman(). Цю симетрію можна виразити в тесті, що перебирає всі значення цього діапазону, викликає to_roman(), потім from_roman(), і перевіряє що отримане число збігається з тим яке ми намагались перевірити.

class RoundtripCheck(unittest.TestCase):
    def test_roundtrip(self):
        '''from_roman(to_roman(n))==n for all n'''
        for integer in range(1, 4000):
            numeral = roman5.to_roman(integer)
            result = roman5.from_roman(numeral)
            self.assertEqual(integer, result)

Ці нові тести поки що навіть не валяться. Ми ще взагалі не описували функцію from_roman(), тому вони генерують помилки.

you@localhost:~/diveintopython3/examples$ python3 romantest5.py
E.E....
======================================================================
ERROR: test_from_roman_known_values (__main__.KnownValues)
from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest5.py", line 78, in test_from_roman_known_values
    result = roman5.from_roman(numeral)
AttributeError: 'module' object has no attribute 'from_roman'

======================================================================
ERROR: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest5.py", line 103, in test_roundtrip
    result = roman5.from_roman(numeral)
AttributeError: 'module' object has no attribute 'from_roman'

----------------------------------------------------------------------
Ran 7 tests in 0.019s

FAILED (errors=2)

Нашвидкоруч вставлена заглушка вирішить цю проблему.

# roman5.py
def from_roman(s):
    '''convert Roman numeral to integer'''

(Хей, ви помітили? Я щойно описав функцію що не містить нічого крім докстрінґа. Це легальний Python. І деякі програмісти клянуться: "Не створюйте просто заглушки, документуйте!")

Тепер тести впадуть правильно.

you@localhost:~/diveintopython3/examples$ python3 romantest5.py
F.F....
======================================================================
FAIL: test_from_roman_known_values (__main__.KnownValues)
from_roman should give known result with known input
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest5.py", line 79, in test_from_roman_known_values
    self.assertEqual(integer, result)
AssertionError: 1 != None

======================================================================
FAIL: test_roundtrip (__main__.RoundtripCheck)
from_roman(to_roman(n))==n for all n
----------------------------------------------------------------------
Traceback (most recent call last):
  File "romantest5.py", line 104, in test_roundtrip
    self.assertEqual(integer, result)
AssertionError: 1 != None

----------------------------------------------------------------------
Ran 7 tests in 0.002s

FAILED (failures=2)

І прийшов час написати from_roman().

def from_roman(s):
    """convert Roman numeral to integer"""
    result = 0
    index = 0
    for numeral, integer in roman_numeral_map:
        while s[index:index+len(numeral)] == numeral:  
            result += integer
            index += len(numeral)
    return result

① Підхід тут такий самий як і в функції to_roman(). Ми ітеруємо крізь структуру даних (кортеж кортежів), але замість того щоб вибирати найбільше можливе ціле число, так часто як можемо, ми вибираємо "найбільшу" послідовність римських цифр, так часто як це можливо.

Якщо вам не ясно як працює from_roman(), додайте команду print в кінець циклу:

def from_roman(s):
    """convert Roman numeral to integer"""
    result = 0
    index = 0
    for