Модуль 8 · Урок 8.2
Дополнительные механизмы языка
Замыкания, собственные декораторы, контекстные менеджеры, match, байты и асинхронность.
Дополнительный урок — для продвинутых. Он для тех, кто хочет понимать устройство библиотек: в темах ЕГЭ из этих механизмов встречается только готовый декоратор @cache. Задачи основных уроков решаются без него.
Декоратор @cache из урока о рекурсии запоминает ответы, хотя код функции ты не менял. А with open(…) закрывает файл сам, даже при ошибке. Как это устроено? Функция в Python — такое же значение, как число: её создают внутри другой функции, возвращают и оборачивают; на этом держатся замыкания, декораторы и контекстные менеджеры.
Разберём эти три механизма, а в конце — ещё три полезные вещи: match, байты и асинхронное ожидание.
Замыкания
Внутренняя функция помнит имена внешней — это замыкание. Функцию можно определить внутри другой функции, и она видит локальные имена внешней. Если внутренняя функция пережила вызов внешней и всё ещё пользуется её именами, это замыкание:
def make_multiplier(factor): def multiply(number): return number * factor return multiply triple = make_multiplier(3)double = make_multiplier(2)print(triple(7), double(7))Возвращается сама функция multiply, не её вызов. Она сохраняет доступ к factor этого конкретного вызова make_multiplier: у triple это 3, у double — 2.
make_multiplier — две функции, у каждой свой запомненный factor.Замыкание помнит саму переменную, а не её значение в момент создания. Поэтому функции, созданные в цикле, видят одну переменную цикла — и после цикла все получают её последнее значение:
late = [lambda: i for i in range(3)]snapshot = [lambda i=i: i for i in range(3)]print([f() for f in late], [f() for f in snapshot])Если нужен снимок, часто используют параметр по умолчанию, как во втором списке, или отдельный вызов создающей функции.
Декораторы
Декоратор получает функцию и возвращает обёртку с дополнительным поведением. Запись @announce над def add означает add = announce(add): имя add теперь указывает на то, что вернул декоратор.
from functools import wraps def announce(function): @wraps(function) def wrapper(*args, **kwargs): print("Вызов", function.__name__) result = function(*args, **kwargs) return result return wrapper @announcedef add(a, b): return a + b print(add(2, 5))Сначала сообщение о вызове, затем 7. wrapper — замыкание: он помнит исходную function. *args и **kwargs из урока об аргументах принимают любые аргументы и передают их дальше без изменений:
@announce вызов add(2, 5) попадает в обёртку, она печатает сообщение и вызывает исходную функцию.Тонкости:зачем wraps
Без @wraps(function) у обёрнутой функции было бы имя wrapper: атрибут __name__ и описание взялись бы от обёртки. wraps копирует их с исходной функции.
Если забыть return result, обёртка будет всегда возвращать None, даже если исходная функция вычисляла число. Сохранение поведения — часть договорённости декоратора. Готовый @cache из урока о рекурсии устроен так же: обёртка сначала ищет ответ в словаре и вызывает исходную функцию, только если его там нет.
Контекстные менеджеры
Контекстный менеджер задаёт, что делать при входе в блок with и при выходе из него. Проще всего написать его генератором с @contextmanager: код до yield — вход, после — выход:
from contextlib import contextmanager @contextmanagerdef section(title): print("начало:", title) try: yield finally: print("конец:", title) with section("подсчёт"): print(sum(range(10)))При входе в with выполняется код до yield, затем блок, затем часть после yield. finally из урока об исключениях гарантирует выход и при ошибке в блоке. Так же можно замерить время: perf_counter() до yield и после. И так же with open(…) закрывает файл.
Тонкости:контекстный менеджер классом
Без декоратора контекстный менеджер — класс с методами __enter__ (вход) и __exit__ (выход). Генератор с @contextmanager должен выполнить yield ровно один раз.
match
match проверяет форму значения и раздаёт его части по именам. Она удобна для разбора небольших команд:
for line in ["move 3 5", "quit", "jump"]: command = line.split() match command: case ["move", x, y]: print(int(x), int(y)) case ["quit"]: print("Завершение") case _: print("Неизвестная команда")Шаблон ["move", x, y] подходит для списка из трёх элементов, где первый — "move", и связывает два остальных с именами x и y. case _ ловит всё остальное. Шаблоны проверяются сверху вниз до первого подходящего:
"jump" проверены все три шаблона — подошёл только последний.Имя в шаблоне не сравнивается с переменной, а захватывает значение. Если нужно просто сравнить с несколькими константами, понятнее обычный if.
Текст и байты
Текст и байты — разные данные, между ними стоит кодировка. Строка str — символы. bytes — последовательность целых значений от 0 до 255, представляющих сырые байты. Для перевода между ними нужна кодировка:
text = "Привет"data = text.encode("utf-8")restored = data.decode("utf-8")print(restored == text)print(len(text), len(data))Количество байтов и количество символов не обязаны совпадать: в UTF-8 каждая русская буква занимает два байта. Чтение файла в режиме rb даёт байты. Преобразование str(data) не заменяет декодирование: оно создаёт текстовое представление байтового объекта.
Асинхронность
Асинхронность позволяет ждать несколько внешних операций сразу, но не ускоряет вычисления. Когда программа ждёт сеть или другой внешний источник, она могла бы в это время обслужить другую задачу. Асинхронное выполнение позволяет описать такие ожидания и передавать управление между задачами. Это не обязательно одновременные вычисления на разных ядрах.
import asyncio async def report(name, delay): await asyncio.sleep(delay) return name async def main(): results = await asyncio.gather( report("A", 0.02), report("B", 0.01), ) print(results) if __name__ == "__main__": asyncio.run(main())async def создаёт асинхронную функцию; её вызов создаёт объект корутины, выполнение которого надо организовать. await ожидает результат, позволяя циклу событий выполнять другую готовую работу. gather организует совместное выполнение переданных операций и возвращает результаты в порядке аргументов, а не порядке завершения: B закончилась раньше, но стоит второй. asyncio.run запускает всё это в обычном скрипте.
Обычное тяжёлое вычисление без точек ожидания блокирует такой цикл. Замена def на async def сама по себе не ускоряет перебор делителей.
Тонкости:ноутбуки, потоки и процессы
В средах, где цикл событий уже запущен (например, некоторые ноутбуки), вместо asyncio.run(main()) пишут await main(). Потоки и процессы — другие способы выполнять работу одновременно, со своими правилами общих данных.
Итог
Функция — значение: её создают, возвращают и оборачивают. На этом стоят замыкания, декораторы и контекстные менеджеры. Вот что понадобится дальше.
- ЗамыканиеВнутренняя функция помнит переменные внешней — саму переменную, а не снимок.
- Декоратор
@dнад функцией —f = d(f); обёртка должна вернуть результат. withВход — доyield, выход — после;finallyвыполняет выход и при ошибке.matchПроверяет форму значения сверху вниз;encodeиdecode— текст и байты.asyncОжидания идут вместе, вычисления быстрее не становятся.
Проверьте себя
Ответь сначала сам, потом открой ответ.
Что вернёт double(triple(2)) для функций из урока?
ОтветСкрыть ответ
12: triple(2) = 6, затем double(6) = 12.
Что напечатает with section("x"): print(1)?
ОтветСкрыть ответ
«начало: x», 1 и «конец: x» — вход, блок и выход.
Почему список late из урока даёт [2, 2, 2], а не [0, 1, 2]?
ОтветСкрыть ответ
Все три функции обращаются к одной переменной i, а к моменту вызова цикл закончился и i равно 2.
Что выведет print(mul(3, 4)), если mul умножает два числа и обёрнута @announce?
ОтветСкрыть ответ
Сначала «Вызов mul», затем 12.
Что напечатает разбор команд из урока для строк "move 1 2" и "move 1"?
ОтветСкрыть ответ
1 2 и «Неизвестная команда»: во второй строке два элемента, а шаблон требует три.
Практика
Для проверки ответов нужен JavaScript. Условия, подсказки и решения доступны ниже.
Что напечатает программа?
def make_multiplier(factor): def multiply(number): return number * factor return multiply print(make_multiplier(5)(4))Введите число.
Подсказка
Первые скобки создают функцию с factor = 5, вторые сразу её вызывают.
Разбор
make_multiplier(5) возвращает multiply, которая помнит factor = 5; вызов с 4 даёт 20.
Ответ: 20
Что напечатает программа?
def announce(function): def wrapper(*args, **kwargs): print(function.__name__) function(*args, **kwargs) return wrapper @announcedef add(a, b): return a + b print(add(2, 5))Запишите напечатанные строки через пробел в порядке вывода.
Подсказка
Обёртка вызывает add, но не возвращает её результат.
Разбор
wrapper печатает имя функции и вызывает исходную add, но без return сама возвращает None — его и печатает print.
Ответ: add None
Что напечатает программа?
print(len("Hi".encode("utf-8")), len("Пока".encode("utf-8")))Запишите напечатанные числа через пробел в порядке вывода.
Подсказка
В UTF-8 латинская буква занимает один байт, русская — два.
Разбор
«Hi» — 2 байта, «Пока» — 4 · 2 = 8 байтов.
Ответ: 2 8
Что напечатает программа?
import asyncio async def report(name, delay): await asyncio.sleep(delay) return name async def main(): results = await asyncio.gather( report("A", 0.03), report("B", 0.01), report("C", 0.02), ) print(results) asyncio.run(main())Запишите список так, как его печатает Python.
Подсказка
gather возвращает результаты в порядке аргументов, а не в порядке завершения.
Разбор
B закончится первой, но в списке результатов стоит на своём месте — второй.
Ответ: ['A', 'B', 'C']