Когда вы определяете функцию внутри другой функции и используете локальные переменные внешней функции во вложенной, вы создаете замыкание. Время жизни этих переменных «продляется» в особой области видимости enclosing даже после завершения работы внешней функции. Пример: make_adder возвращает функцию-прибавлятор. Объект из переменной a будет жить и работать даже после выхода из make_adder:
def make_adder(a): def adder(x): return a + x return adder plus_5 = make_adder(5) print(plus_5(3)) # 8
Здесь я хочу коснуться одной популярной проблемы. Дело в том, что если мы создадим несколько функций внутри одного контекста, то они будут разделять одну область видимости enclosing. Рассмотрим пример создания трех функций в цикле:
def make_adders(): adders = [] for a in range(3): def adder(x): return a + x adders.append(adder) return adders adders = make_adders() for adder in adders: print(adder(2)) # 4 4 4
Вместо функций прибавляющих разные числа от 0 до 2, мы получили 3 одинаковых функции, потому что внутри себя они поддерживают ссылку на одну и ту же переменную a, значение которой останется равным 2 после выполнения всего цикла целиком.
Есть простой прием, помогающий «зафиксировать» значения переменной в моменте: достаточно добавить во вложенную функцию дополнительный аргумент со значением по умолчанию, равным нужной переменной a=a
:
def make_adders(): adders = [] for a in range(3): def adder(x, a=a): # FIX! return a + x adders.append(adder) return adders adders = make_adders() for adder in adders: print(adder(2)) # 2 3 4
Еще лучше переименовать аргумент, чтобы избежать конфликтов имен и замечаний IDE, например, так:
def adder(x, that_a=a): # FIX! return that_a + x
yield
Пока писал код для этого поста, я наткнулся на одну обманку. Люблю оформлять функции, возвращающие коллекции, как генераторы с ключевым словом yield. Вот так:
def make_adders(): for a in range(3): def adder(x): return a + x yield adder adders = make_adders() for adder in adders: print(adder(2)) # 2 3 4
Видите, тут нет фикса a=a
! Казалось бы, что код должен также содержать в себе баг и выводить «4 4 4», но он работает, как задумано изначально.
Однако, если мы применим list к генератору, извлекая все значения разом, то баг вернется:
adders = list(make_adders()) for adder in adders: print(adder(2)) # 4 4 4
Разгадка. В первом случае происходят следующие действия:
- a = 0
- yield функцию (a + x), make_adders становится на паузу
- печать adder(2) = 0 + 2 = 2
- make_adders запускается
- a = 1
- yield функцию (a + x), пауза
- печать adder(2) = 1 + 2 = 2
- … и так далее…
То есть мы запускаем adder только один раз в тот момент, пока переменная a еще равна нужному значению.
Во втором код list прокручивает make_adders до конца, оставляя a = 2
, и все функции выдают одинаковый результат.
Вывод мы должны сделать такой: yield не создает нового замыкания с отдельной переменной a и не освобождает нас от ответственности следить за переменными.
Еще кое-что.
adders = make_adders() for adder in adders: print(adder(2)) # 2 3 4 next(adders) # StopIteration
После исполнения цикла в коде выше, генератор adders будет исчерпан. В нем больше не останется значений, и если еще раз запустить цикл по adders, то он пройдет ровно 0 итераций.
Генератор – вещь одноразовая.
Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈