У темі 14 ми з'ясували річ,
яка тоді виглядала цікавинкою: функція — такий самий об'єкт, як число чи список. Її
можна присвоїти другому імені, покласти в список, передати іншій функції аргументом.
У темі 15 у врізці промайнуло слово
замикання й обіцянка розібратися з ним у темі 31. Це вона. Обидві
нитки сходяться тут, в одному механізмі — і з нього виростає значок @,
який ти вже бачив над класом у темі 28.
Наскрізний приклад лишається тим самим, що в темі 14, — розрахунок позиції чека:
01 / ЗадачаЗадача, яку нічим закрити
Крамниця виросла. Тепер від коду хочуть речей, які до самого розрахунку не мають жодного стосунку: знати, скільки мілісекунд працює кожна функція; записувати в журнал, кого і з чим викликали; не рахувати вдруге те, що вже рахували; повторити запит до довідника, якщо він відмовив. Чотири вимоги, і кожна стосується десятків функцій одразу.
Перший інстинкт — дописати потрібне в тіло. Виглядає це так:
Функція з чотирьох рядків стала шестирядковою, і два з шести до чека не належать.
Помнож це на тридцять функцій — і в кожній буде однакова пара рядків, скопійована
тридцять разів. Проблему копії ми вже проходили в темі 14: коли захочеш міряти час не
секундоміром perf_counter, а чимось іншим, правити доведеться тридцять
місць. І це ще не найгірше. Гірше те, що читач функції більше не бачить, що вона
робить: намір потонув у службовому.
Другий інстинкт — написати функцію, яка приймає іншу функцію. Ми вміємо це з теми 14:
Уже краще: службовий код живе в одному місці. Але ціна теж є, і вона висока —
усі місця виклику тепер треба переписати. Замість підсумок(28.5, 2)
всюди має стояти з_таймером(підсумок, 28.5, 2). А ще ця функція вміє
викликати лише те, що приймає рівно два аргументи.
Потрібне щось третє: спосіб підмінити саму функцію так, щоб місця виклику лишились недоторканими, а поведінка додалась. Саме це роблять декоратори — і щоб дійти до них, нам бракує однієї цеглинки.
02 / ЦеглинкаФункція, що повертає функцію
Ми звикли, що функція повертає число, рядок або список. Але return віддає
об'єкт — будь-який. А функція, як ми з'ясували в темі 14, — це об'єкт. Отже,
функція може повернути функцію.
Тут два def, вкладені один в одного. Зовнішній зробити_множник
нічого не рахує — він виготовляє функцію й віддає її. Внутрішній
помножити у момент виготовлення не виконується: рядок def
лише створює об'єкт-функцію, як ми бачили в темі 14.
Найважливіший рядок — останній. Там написано return помножити, а не
return помножити(). Дужки означають «виклич», і якби вони там стояли, Python
спробував би викликати внутрішню функцію без аргументу й одразу впав би з
TypeError. Ми ж хочемо віддати саму функцію, тому дужок немає.
Зупинись і подивись на це уважно. Ми викликали зробити_множник двічі й
дістали дві різні функції. Кожна пам'ятає своє значення
коефіцієнт — двійку й трійку відповідно. Але сам виклик
зробити_множник(2) давно завершився. За правилами
теми 15 усе локальне при завершенні
функції зникає. Чому ж двійка жива?
03 / ЗамиканняЗамикання: памʼять, що пережила виклик
Відповідь має назву. Внутрішня функція разом із захопленими іменами зовнішньої називається замиканням (closure). Коли Python створює внутрішню функцію й бачить, що та користується іменем із зовнішньої, він не просто запам'ятовує значення. Він кладе його в окрему довговічну комірку (cell) і чіпляє цю комірку до об'єкта-функції.
Кадр виклику справді знищується — правило з теми 15 не скасовано. Але комірка живе окремо від кадру, і поки на неї дивиться жива функція, збирач сміття її не чіпає.
Схема 1 · Кадр помирає, комірка живе
Ліворуч — момент, коли зовнішня функція ще працює. Праворуч — те, що лишилось після return.
func, обгорткаобгортка через __closure__Замикання — не виняток із правил теми 15, а їх наслідок. Зникає кадр, а не об'єкти, на які з нього хтось ще дивиться.
підсумок, до якої більше не веде жодне імʼя.Захоплені імена можна побачити на власні очі. У кожної функції є поле
__code__.co_freevars — кортеж імен, узятих ззовні, і поле
__closure__ — кортеж самих комірок:
зробити_множник зіпсував би першу функцію. Замикання
дає стан, який ніхто ззовні не бачить і не може випадково перезаписати — тому за
nonlocal критикують значно менше, ніж за global.04 / СинтаксисЩо насправді означає @
Тепер можна зібрати все докупи. Декоратор — це функція, яка приймає функцію й повертає нову функцію. Усе. Жодного окремого синтаксису для цього не потрібно, жодного нового типу об'єкта мова не додає.
Порівняй із зробити_множник — будова один в один. Зовнішня функція
приймає func, виготовляє внутрішню обгортка й повертає її.
Замикання тримає func живим. Усередині обгортки — три частини: щось
до, виклик справжньої функції, щось після.
Скористатися цим можна вже зараз, без жодного @:
Рядок виглядає дивно, але робить рівно те, чого ми хотіли в розділі 01. Ім'я
підсумок тепер вказує на обгортку, а сама справжня функція нікуди не
поділась — вона в комірці замикання під ім'ям func. Усі місця виклику
лишились незмінними: там як стояло підсумок(28.5, 2), так і стоїть.
Оскільки конструкція виявилась потрібною постійно, у мову додали для неї короткий запис. Він не додає нічого нового — це синтаксичний цукор, інший спосіб написати той самий рядок:
Схема 2 · Що означає рядок з @
Ліворуч — як пишуть. Праворуч — те саме без цукру. Рожевим — рядок, який робить усю роботу.
def — при читанні файлу, а не при викликупідсумок перечіпляється на те, що повернув таймер@таймер — це не магія й не «мітка». Це буквально рядок
підсумок = таймер(підсумок), записаний коротше й до тіла,
а не після нього.@ кращий: він стоїть над функцією, тому
видно одразу, ще до читання тіла. Варіант із присвоєнням доводиться шукати внизу —
а коли функція на пів екрана, його там просто не помічають.Тепер зрозуміло й те, що ти бачив у темі 28. @dataclass над класом — та
сама конструкція: Продаж = dataclass(Продаж). Просто там декоратор приймає
не функцію, а клас, і повертає той самий клас із дописаними методами.
05 / МеханікаКрок за кроком крізь обгортку
Найважливіше в декораторі — зрозуміти, де в який момент перебуває керування. Тут легко заплутатись, бо функцій дві, а ім'я одне. Пройди виклик по кроках і подивись на стек: рожевим позначено обгортку, бірюзовим — справжню функцію.
Інтерактив 1 · Один виклик, дві функції
Тисни «Наступний крок». Ліворуч підсвічується рядок, що виконується; праворуч видно стек викликів і те, що вже надруковано.
підсумок при цьому
не змінилось ані на символ — воно й досі не знає, що його хтось міряє.Зверни увагу на крок 10: обгортка повертає результат незміненим. Це не
дрібниця, а обов'язок. Забути return результат у кінці
обгортки — найпоширеніша помилка при написанні першого декоратора: функція раптом
починає повертати None, і причину шукають у самій функції, де її немає.
06 / УніверсальністьОбгортка для будь-якої функції
Наш таймер має ваду: його обгортка приймає рівно (ціна, кількість).
Спробуй почепити його на функцію з трьома параметрами — отримаєш TypeError.
А сенс декоратора саме в тому, щоб чіпляти його на що завгодно.
Рятує зірочка з теми 15. Там ми
казали, що *args, **kwargs «про всяк випадок» — погана звичка, і називали
єдиний випадок, де без них ніяк: обгортка, яка передає аргументи далі, не знаючи їх.
Ось цей випадок:
Тепер обгортка збирає позиційні аргументи в кортеж args, іменовані — у
словник kwargs і одразу розпаковує їх назад при виклику
func. Це та сама пара зірочок, що в темі 15, у двох різних ролях: у рядку
def вони пакують, у рядку виклику — розпаковують. Декоратору байдуже, скільки
параметрів у функції та як їх назвали: він нічого з ними не робить, тільки передає далі.
07 / Ремонтfunctools.wraps
Задекоруй функцію й спитай, як її звати:
Нічого дивного тут немає — це прямий наслідок розділу 04. Ім'я підсумок
тепер вказує на об'єкт-функцію обгортка, а в неї своє власне
__name__ і немає жодного докстрінга. Справжня функція жива, але вона сховалась
у комірці замикання, і жодні розпитування ім'я підсумок до неї не доводять.
Проблема здається косметичною рівно доти, доки не заважає всерйоз. Заважає вона так:
help() показує безглузде; автодоповнення в редакторі підказує
(*args, **kwargs) замість справжніх параметрів; журнал помилок пише
«обгортка» замість імені функції — і з нього неможливо зрозуміти, що зламалось;
бібліотеки тестів шукають функції за іменем і перестають їх бачити.
Лагодиться це одним рядком. У модулі functools є декоратор
wraps, який копіює службові поля зі справжньої функції в обгортку:
Схема 3 · Що губиться без functools.wraps
Одна й та сама функція підсумок після декорування — без рядка @wraps і з ним.
__name__, __doc__, __module__, __qualname__, __dict__ і додає __wrapped__Пишеш декоратор — пиши
@wraps(func). Це не стиль і не смак:
без нього функція втрачає власне ім'я й документацію.__wrapped__ веде
до справжньої функції, і саме за ним inspect.signature показує правильні
параметри замість (*args, **kwargs).08 / НайважчеДекоратор з аргументами
Попередження чесно: це найважче місце теми. Якщо після першого читання буде каша в голові — це нормально, така реакція в усіх. Ідея проста, важка тільки будова.
Хочеться писати не просто @повторити, а @повторити(спроб=3) —
з налаштуванням. Питання: як це може працювати, якщо @ вимагає функцію,
яка приймає функцію?
Розгорни цукор, як у розділі 04, — і все стане на місце:
Придивись до двох пар дужок поспіль. Спершу виконується
повторити(спроб=3) — звичайний виклик звичайної функції. Вона повертає
декоратор. І вже цей декоратор застосовується до
завантажити. Тобто повторити — не декоратор, а
виробник декораторів. Звідси й три рівні вкладеності:
Звичайна функція зі звичайним параметром. Про декорування вона ще нічого не знає — просто запам'ятовує число в замиканні.
А ось це вже справжній декоратор — той самий, що в розділі 04: приймає функцію,
повертає нову. У його замиканні лежать одразу два імені: func і
спроб з рівня вище.
Знайома обгортка. Вона єдина з трьох виконується при кожному виклику; два зовнішніх рівні спрацювали по одному разу — тоді, коли Python читав файл.
Повністю це виглядає так:
Три def, три return, і кожен return віддає
рівень нижче. Читай знизу вгору: обгортка робить роботу, декоратор віддає обгортку,
повторити віддає декоратор. Механіка обробки помилок — з
теми 18: return усередині
try виходить з функції одразу, тому наступної спроби не буде, якщо ця
вдалась.
@повторити замість
@повторити(спроб=3). Помилки в цьому рядку не буде — Python слухняно передасть
функцію в параметр спроб, і зламається все пізніше й в іншому місці:
декоратор ніколи не викличеться, а завантажити стане
об'єктом-функцією декоратор. Симптом — TypeError про несподівані
аргументи там, де все виглядає правильним.09 / З коробкиГотові декоратори: lru_cache
Свої декоратори пишуть рідше, ніж користуються чужими. Найкорисніший у стандартній
бібліотеці — functools.lru_cache. Він запам'ятовує результати викликів і на
повторний виклик із тими самими аргументами віддає збережену відповідь, не рахуючи вдруге.
Уяви довідник вартості доставки: щоб дізнатися ціну для міста, треба звернутись до повільного джерела — приблизно 40 мілісекунд на запит. Замовлень за день сотні, а міст у нашій крамниці всього вісім, і повторюються вони постійно:
Параметр maxsize — скільки різних відповідей кеш тримає одночасно. Коли
місць бракує, викидається та, до якої найдовше не зверталися (це і є LRU — least recently
used, «найдавніше вживана»). Число тут не косметичне: покрути обидва повзунки й подивись,
як воно змінює висновок.
Інтерактив 2 · Скільки обчислень економить кеш
Послідовність замовлень фіксована й однакова для всіх. Бірюзова клітинка — відповідь узята з кешу, рожева — справжнє звернення до довідника.
maxsize=1 — і кеш майже перестане допомагати.
Міст вісім, замовлення чергуються, і єдине збережене місто встигає вилетіти до того,
як його спитають удруге. Кеш розміром менше за кількість різних запитів — це не
маленький кеш, а майже жодного.Скільки саме кеш зекономив, можна спитати в нього самого — без жодних вимірювань:
Поле hits — скільки разів відповідь знайшлась у кеші, misses —
скільки разів довелося рахувати насправді. Є й cache_clear(), щоб усе забути.
Обидва методи lru_cache чіпляє прямо на задекоровану функцію — це, до речі,
ще один спосіб побачити, що декоратор повернув інший об'єкт: у звичайної
функції жодних cache_info немає.
вартість_доставки(["Київ", "Львів"])
впаде з TypeError: unhashable type: 'list' — і не в тілі функції, а ще на
вході, в обгортці. Список як аргумент доводиться перетворювати на кортеж. Друга ціна —
пам'ять: кеш тримає посилання і на аргументи, і на результати, тому
@lru_cache на методі об'єкта не дає цьому об'єкту померти, поки живий кеш
класу. maxsize=None (або декоратор @cache) знімає межу зовсім —
зручно для чистих обчислень і небезпечно для всього іншого.І ще одне обмеження, яке випливає з визначення: кешувати можна лише
чисту функцію в сенсі теми 14 — таку, що на однакових аргументах завжди
дає однакову відповідь. Задекоруєш @lru_cache функцію, яка читає файл або
питає поточний час, — і вона назавжди застрягне на першій відповіді.
10 / ПорядокКілька декораторів одразу
Декоратори складаються. Над однією функцією їх може стояти скільки завгодно — і тоді важливо, у якому порядку вони застосовуються.
Схема 4 · Хто кого обгортає
Ліворуч — код і його розгортка. Праворуч — цибулина: хто всередині кого. Знизу — порядок виводу при одному виклику.
таймер, потім логуватилогуватиНайближчий до
def декоратор обгортає функцію першим — і тому
опиняється найглибше, найближче до неї ж.@lru_cache над таймером зміряє
лише перший, справжній виклик — далі відповідь віддає кеш, і таймер не запуститься
зовсім. Поміняй їх місцями — і таймер міритиме роботу кешу. Обидва варіанти
правильні, але відповідають на різні питання.Правило коротко: застосування — знизу вгору, виконання — ззовні всередину.
Легко запам'ятати через розгортку: підсумок = логувати(таймер(підсумок)).
Внутрішні дужки рахуються першими — це той декоратор, що стоїть нижче.
11 / Не лише функціїКласи й методи
Декоратор працює з будь-яким об'єктом, який можна викликати, — отже, і з класами теж.
Механіка та сама: @dataclass над класом означає
Продаж = dataclass(Продаж). Тепер зрозуміло, чому декоратор класу вимагав
анотацій: він отримує готовий клас і читає його __annotations__, як звичайна
функція читала б словник.
Методи декоруються так само, як функції, — вони й є функції всередині класу. Три готових декоратори зустрінуться тобі в чужому коді постійно:
| декоратор | що робить |
|---|---|
| @property | метод читається як звичайне поле: чек.сума замість чек.сума() — зручно для обчислюваних значень |
| @staticmethod | функція живе в класі за змістом, але не отримує self і нічого про об'єкт не знає |
| @classmethod | першим аргументом отримує сам клас, а не об'єкт — типово для альтернативних конструкторів |
Усі три — звичайні вбудовані декоратори без жодної магії. Коли пишеш свій декоратор
для методу, пам'ятай лише одне: self прийде в обгортку першим позиційним
аргументом і поїде далі в *args сам собою. Нічого спеціального робити не
треба.
12 / МежаМежа розумного
Тепер про те, чого не пишуть у підручниках. Декоратор — інструмент із побічним
ефектом: він ховає поведінку. Читач бачить виклик
підсумок(28.5, 2), а насправді виконується щось інше, і дізнатися про це
можна лише піднявши очі на рядок над def — або, якщо декоратор чужий, пішовши
читати його код.
Просте правило перевірки: якщо читач мусить піти подивитись на декоратор, щоб зрозуміти, що робить функція, — краще звичайний виклик.
Ознаки того, що межу перейдено:
- Декоратор змінює результат. Обгортка, яка мовчки округляє, підміняє значення чи ковтає винятки, робить функцію непередбачуваною. Тест на неї пройде, а в програмі поведінка буде іншою.
- Декоратор потрібен рівно одній функції. Тоді це просто складний спосіб дописати два рядки в тіло. Декоратор виправданий, коли поведінка спільна для багатьох.
- Декораторів стало більше трьох. Стовпчик із п'яти
@перетворює просту функцію на загадку, а порядок у ньому починає значити — і мовчки ламатись при перестановці. - Обгортка нетривіально розбирає аргументи. Якщо всередині з'явився код на кшталт «якщо третій позиційний аргумент — рядок, то...» — декоратор узявся не за своє.
Хороший декоратор — прозорий: прибери його, і функція працюватиме як
раніше, просто без журналу, без кешу, без таймера. Саме тому @lru_cache,
@wraps і @dataclass прижились, а декоратор, що змінює формат
відповіді, — ні.
13 / ПідсумокЩо лишається
- Функція може повертати функцію. Це не окрема можливість мови, а наслідок того, що функція — об'єкт (тема 14).
- Замикання — внутрішня функція разом із захопленими іменами
зовнішньої. Кадр виклику зникає, а комірка з ім'ям живе, поки жива внутрішня функція.
Побачити захоплене можна через
__code__.co_freevarsі__closure__. - Декоратор — це функція, яка приймає функцію й повертає нову.
Рядок
@таймернадdef підсумок— це рівнопідсумок = таймер(підсумок), виконане один раз при читанні файлу. - Обгортка приймає
*args, **kwargs— єдиний випадок, коли зірочки справді доречні, і обов'язково повертає результат. @wraps(func)— не косметика. Без нього функція втрачає ім'я, докстрінг і правильну сигнатуру вhelp().- Декоратор з аргументами — три рівні: налаштування → функція →
аргументи виклику. Розгортка
f = повторити(3)(f)пояснює це краще за будь-який опис. lru_cacheекономить справжні обчислення, вимагає хешованих аргументів (тема 08) і має сенс лише для чистих функцій.- Порядок: застосування знизу вгору, виконання ззовні всередину.
- Межа: декоратор ховає поведінку. Якщо без нього функцію не зрозуміти — краще звичайний виклик.
Ця тема закриває обіцянку, дану в темі 15, і остаточно пояснює @dataclass
з теми 28. Лишилось одне велике питання блоку: усе, що ми писали досі, виконується
по черзі — рядок за рядком, виклик за викликом. Але коли програма чекає на відповідь
з мережі, вона просто стоїть і нічого не робить. У темі 32 ми розберемо
асинхронність — спосіб дозволити програмі зайнятися чимось іншим, поки триває очікування.
І виявиться, що async def — теж не магія, а щось дуже схоже на те, що ти
щойно розібрав.
practice.ipynb ти напишеш
замикання-лічильник і подивишся на його __closure__; зробиш свій декоратор
часу двома способами — через = і через @ — і доведеш
assert-ом, що результат не змінився; побачиш на власні очі,
як без wraps функція втрачає ім'я, і полагодиш це; напишеш
@повторити(спроб=3) й змусиш його спрацювати з третьої спроби; зміряєш
секундоміром, скільки часу економить lru_cache; напишеш власний кеш і
assert-ом порівняєш його з бібліотечним; і зловиш TypeError
на нехешованому аргументі.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.