Изначально этот пост опубликован в моем личном блоге. Если у вас возникли проблемы с чтением примеров кода, обязательно загляните в мой блог. У меня там есть подсветка синтаксиса.
«Замыкание - это черепаха, несущая панцирь»,
Мое любимое объяснение закрытий, цитата Раймонда Хеттингера. Благодаря замыканиям в Python возможно множество замечательных функций, таких как функции и декораторы более высокого порядка.
Я наткнулся на этот пост на StackOverflow - Как я могу вернуть функцию, использующую значение переменной?, Который помог мне восполнить пробел в моих знаниях, пробел, который я вроде бы понял, но не мог объяснить. Что ж. Мост - это простое предложение: закрытие связывается поздно. Что именно означает закрытие выполняется с опозданием? Рассмотрим генератор функций, который генерирует серию функций умножения, начиная с 0 до max
:
def mult_function_generator(max): for i in range(max): yield lambda x: x * i mult_functions = list(mult_function_generator(3)) print([func(3) for func in mult_functions]) # Oh crap, it’s [6, 6, 6], not [0, 3, 6]
Похоже, что все i
в сгенерированных функциях присвоены 2, что является последним значением i
после создания всех функций. Давайте внимательнее посмотрим, что происходит за кулисами. Прежде всего, как представляет собой замыкание в Python или как выглядит панцирь черепахи:
mult_functions = mult_function_generator(3) mult_zero = next(mult_functions) mult_zero # <function …>
Чтобы посмотреть на замыкание, в каждой функции есть специальный атрибут __closure__
(совершенно очевидно, да?)
mult_zero.__closure__ # (<cell at 0x…, int object at 0x…>,)
Ладно, мы приближаемся. Атрибут __closure__
- это набор из cell
объектов. Давайте посмотрим, что внутри ячейки, у которой есть атрибут cell_contents
, который нам поможет:
cell_zero = mult_zero.__closure__[0] cell_zero.cell_contents # 0
Бинго! Это объект типа int i
, который застрял внутри закрытия функции. (Я не понял, как Python находит ссылку на i
, поэтому не буду углубляться в подробности. Более подробное обсуждение см. в разделе "Приложение".)
Интересно то, что в наших функциях умножения используется один и тот же объект ячейки. Таким образом, когда мы получаем вторую функцию:
mult_one = next(mult_functions) cell_one = mult_one.__closure__[0] cell_one.cell_contents # 1 # So far so good. But… cell_zero.cell_contents # 1 # Crap! cell_zero is cell_one # True # Okay fine
Один и тот же объект ячейки означает такую же ссылку на i
. Это объясняет, что произошло в этом примере. Или, если хотите, вы можете объяснить все это, сказав: «Закрытие выполняется поздно». Они оба верны, но имеют разные ментальные модели.
Теперь мы понимаем проблему, вызванную поздним связыванием. Давайте посмотрим, как связать переменную во время объявления функции. Идея состоит в том, чтобы i
также был аргументом, передаваемым в лямбда-функцию, а затем мы немедленно присваиваем значение:
def mult_function_generator(max): for i in range(max): yield (lambda i: lambda x: x * i)(i)
Или, если вам нравятся частичные функции:
import functools def mult_function_generator(max): for i in range(max): yield functools.partial(lambda x, i: x * i, i=i)
Или мой любимый:
def mult_function_generator(max): for i in range(max): yield lambda x, i=i: x * i
Последний работает, потому что аргументы по умолчанию назначаются при определении функции.
Бонус
Мой любимый пример использования преимущества позднего закрытия привязки - из 20 библиотек Python, которые вы не используете (но должны):
from time import perf_counter from array import array from contextlib import contextmanager @contextmanager def timing(label: str): t0 = perf_counter() yield lambda: (label, t1 — t0) t1 = perf_counter() with timing(‘Array tests’) as total: with timing(‘Array creation innermul’) as inner: x = array(‘d’, [0] * 1000000) with timing(‘Array creation outermul’) as outer: x = array(‘d’, [0]) * 1000000 print(‘Total [%s]: %.6f s’ % total()) print(‘ Timing [%s]: %.6f s’ % inner()) print(‘ Timing [%s]: %.6f s’ % outer()) # Total [Array tests]: 0.064896 s # Timing [Array creation innermul]: 0.064195 s # Timing [Array creation outermul]: 0.000659 s
Приложение - обсуждение загрузки разыменованной переменной
Возьмем, к примеру, функцию mult_zero
. Дисэмблированный код выглядит:
3 0 LOAD_FAST 0 (x) 2 LOAD_DEREF 0 (i) 4 BINARY_MULTIPLY 6 RETURN_VALUE
Вторая строка вызывает код операции LOAD_DEREF
в Python. Я предполагаю, что имя i
никогда не разыменовывалось, потому что объект ячейки все еще содержит ссылку на него. Однако с i
нужно обращаться особым образом, поскольку i
не виден области за пределами функций закрытия. Следующим шагом будет изучение кода CPython, чтобы увидеть, как именно работает этот операционный код.