В давно написанных сценариях Python все еще можно увидеть, как аргументы со значениями по умолчанию используются для передачи ссылок в объемлющую область видимости, но в настоящее время предпочтение отдается приему автоматического получения ссылок. Может сложиться впечатление, что новые правила поиска во вложенных областях видимости, реализованные в Python, автоматизируют и делают ненужной ручную операцию передачи значений из объемлющей области видимости в виде аргументов со значениями по умолчанию.
Все это так, но не совсем. Есть одна особенность. Оказывается, что внутри lambda-выражения (или внутри инструкции def) ссылки на переменные в объемлющей области видимости разрешаются в момент вызова сгенерированной функции, а не в момент ее создания. Вследствие этого, когда функция будет вызвана позднее, такие ссылки будут отражать последние значения переменных, присвоенные им где бы то ни было в объемлющей области видимости, которые не обязательно будут совпадать со значениями, присвоенными переменным на момент создания функции. Это справедливо не только для вложенных функций обратного вызова, но и для находящихся в глобальной области видимости модуля. В любом случае, все ссылки на переменные в объемлющей области видимости разрешаются в момент вызова функции, а не в момент ее создания.
Аргументы со значениями по умолчанию отличаются тем, что их значения определяются в момент создания функции, а не в момент ее вызова. Вследствие этого они по-прежнему могут быть полезным способом сохранения значений переменных из объемлющей области видимости, имевших место на момент создания функции. В отличие от ссылок на переменные в объемлющей области видимости, аргументы со значениями по умолчанию не получат другое значение, даже если переменная в объемлющей области видимости изменится между моментами создания и вызова функции. (Фактически именно этим объясняется, почему изменяемые объекты, такие как списки, передаваемые по умолчанию, сохраняются между вызовами — они создаются только один раз, в момент создания функции, и присоединяются к самой функции.)
Обычно это отличие не вызывает проблем, потому что в большинстве случаев используются ссылки на переменные в объемлющей области видимости, значения которым присваиваются только один раз (примером может служить аргумент self в методах классов). Но при непонимании этого отличия можно допустить программные ошибки, особенно если функции создаются в цикле, — при использовании в этих функциях ссылок на переменную цикла все они будут получать значение, полученное в последней итерации. Напротив, при использовании значений по умолчанию каждая функция будет получать текущее значение переменной цикла, а не последнее.
Из-за этого отличия одних только ссылок на переменные в объемлющей области видимости может оказаться недостаточно для сохранения значений, и аргументы со значениями по умолчанию все еще могут оказаться востребованными. Посмотрим, что это означает с точки зрения программного кода. Взгляните на следующую вложенную функцию (фрагменты программного кода для этого раздела вы найдете в файле defaults.py в дереве примеров на случай, если у вас появится желание поэкспериментировать с ними).
def simple(): spam = ‘ni’ def action(): print(spam) # ссылка на переменную в объемлющей функции return action
act = simple() # создать и вернуть вложенную функцию
act() # затем вызвать ее: выведет ‘ni’
Это простейший случай ссылки на переменную в объемлющей области видимости, и он одинаково действует для вложенных функций, сгенерированных с помощью инструкций def и lambda. Но обратите внимание, что он будет действовать точно так же, если значение переменной spam будет присвоено после определения вложенной функции:
def normal():
def action():
return spam # поиск переменной будет выполняться в момент вызова spam = ‘ni’
return action
act = normal()
print(act()) # также выведет ‘ni’
Из этого примера следует, что разрешение имен в объемлющей области видимости не выполняется в момент создания вложенной функции — фактически в этот момент переменная вообще не была определена. Разрешение имени выполняется в момент вызова вложенной функции. То же справедливо и для lambda-выражений:
def weird(): spam = 42 return (lambda: spam * 2) # запомнит ссылку на spam в объемлющей # области видимости
act = weird()
print(act()) # выведет 84
Пока все неплохо. Ссылка на переменную spam внутри вложенной lambda— функции сохраняет значение, полученное переменной в объемлющей области видимости, которое остается доступным даже после выхода из объемлющей функции. Этот пример соответствует зарегистрированному обработчику в графическом интерфейсе, который будет вызываться позднее, по событиям. И снова ссылка из вложенной области видимости разрешается не в тот момент, когда в lambda-выражение создает функцию, а когда сгенерированная функция будет вызвана. Следующий фрагмент более наглядно демонстрирует действие этого правила:
def weird():
tmp = (lambda: spam * 2) # запоминает ссылку на spam, даже при том, spam = 42 # что здесь она еще не установлена
return tmp
act = weird() print(act()) # выведет 84
Здесь вложенная функция ссылается на переменную, которой на момент создания функции еще не было присвоено значение. Действительно, при каждом вызове вложенной функции ссылки в объемлющую область видимости возвращают последние значения, присвоенные переменным. Взгляните, что происходит в следующем фрагменте:
def weird(): spam = 42 handler = (lambda: spam * 2) # функция не сохраняет текущее значение 42 spam = 50
print(handler()) # выведет 100: поиск spam выполняется именно сейчас spam = 60
print(handler()) # выведет 120: поиск spam снова выполняется именно сейчас
weird()
Теперь значение ссылки на spam внутри lambda-функции изменяется при каждом вызове сгенерированной функции! Фактически ссылка на переменную возвращает последнее значение, присвоенное в объемлющей области видимости на момент вызова вложенной функции, потому что ссылки разрешаются в момент вызова функции, а не в момент ее создания.
С точки зрения графических интерфейсов, это становится существенным чаще всего, когда функции обработчиков генерируются в циклах и при этом используются ссылки в объемлющую область видимости для сохранения дополнительных данных, создаваемых внутри циклов. Если вы собираетесь генерировать функции внутри цикла, вы должны учитывать, что продемонстрированные выше особенности поведения применяются и к переменной цикла:
def odd(): funcs = [] for c in ‘abcdefg’: funcs.append((lambda: c)) # поиск переменной c будет выполнен позднее return funcs # не сохраняет текущее значение c
for func in odd():
print(func(), end=’ ‘) # Опа!: выведет 7 символов g, а не a,b,c,… !
Здесь список func имитирует зарегистрированные обработчики событий графического интерфейса, подключенные к виджетам. Этот пример работает совсем не так, как могли бы ожидать многие из вас. Переменная c внутри вложенной функции здесь всегда будет иметь значение ‘g’, то есть значение, установленное последней итерацией цикла в объемлющей области видимости. В результате все семь сгенерированных lambda-функций будут получать при вызове одно и то же значение.
Аналогичная реализация использования дополнительных данных в lambda-функциях, вызывающих действительные обработчики графического интерфейса, будет испытывать похожие проблемы. Например, все обработчики событий от кнопок, созданных в цикле, могут в конечном итоге давать один и тот же результат! Чтобы исправить положение, необходимо передавать значения вложенным функциям в виде значений аргументов по умолчанию, которые сохраняют текущее значение переменной цикла (а не то, которое будет в будущем):
def odd(): funcs = [] for c in ‘abcdefg’: funcs.append((lambda c=c: c)) # запомнить текущее значение c return funcs # значения по умолчанию вычисляются
# немедленно
for func in odd():
print(func(), end=’ ‘)
Теперь мы получили ожидаемый результат благодаря тому что значения по умолчанию, в отличие от ссылок во внешнюю область видимости, вычисляются в момент создания функции, а не в момент ее вызова. При таком подходе сохраняется значение, которое переменная в объемлющей области видимости имела на момент создания функции, а не последнее, присвоенное ей значение. То же правило действует, даже если объемлющей областью видимости для функции является модуль, а не какая-то другая функция — если в следующем фрагменте не использовать аргумент со значением по умолчанию, при обращении к переменной цикла все семь функций получат одно и то же значение:
funcs = [] # объемлющая область видимости — модуль
for c in ‘abcdefg’: # запомнить текущее значение c,
funcs.append((lambda c=c: c)) # иначе снова выведет 7 символов g
for func in funcs: print(func(), end=’ ‘) # OK: выведет a,b,c,…
Из всего вышеизложенного следует вывод, что ссылка на переменную в объемлющей области видимости может служить заменой передачи данных в виде аргумента со значением по умолчанию, но только при условии, что эта переменная не получит новое значение, которое вам не нужно, после создания вложенной функции. Вы вообще не должны использовать внутри вложенных функций ссылки на переменные циклов в объемлющей области видимости, потому что они изменяются в процессе выполнения цикла. Однако в большинстве других случаев переменные в объемлющей области видимости могут принимать только одно значение, поэтому ссылки на них можно использовать без опаски. Мы еще будем сталкиваться с этим явлением в последующих примерах, создающих более сложные графические интерфейсы. А пока запомните, что ссылки в объемлющую область видимости не могут служить полной заменой аргументов со значениями по умолчанию — в некоторых ситуациях значения по умолчанию незаменимы для передачи значений функциям обратного вызова. Кроме того, имейте в виду, что зачастую классы обеспечивают более простой способ сохранения информации для использования в обработчиках, чем вложенные функции. Поскольку информация в классах сохраняется более явным способом, им не свойственны описанные проблемы областей видимости. Следующие два раздела детально рассказывают об использовании классов.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011