Вложенные функции, замыкания и nonlocal
- Определять функцию внутри функции и объяснять, что она видит из объемлющей области (Enclosing)
- Объяснить, что такое замыкание (Closure) — функция, которая помнит переменные даже после завершения внешней функции
- Понимать, почему изменение внешней переменной вызывает UnboundLocalError, и исправлять это через nonlocal
- Различать global и nonlocal
Что мы изучим?
- Вложенную функцию (Nested Function) — функцию, определённую внутри функции.
- Enclosing — букву E в LEGB, вблизи.
- Замыкание (Closure) — функцию, которая «помнит» переменные.
nonlocal— как изменить переменную объемлющей функции.
Зачем это нужно?
- Приватная вспомогательная функция: иногда функции нужна маленькая вспомогательная функция, которая принадлежит только ей. Нет причин делать её глобальной и смешивать с остальным кодом.
- Функции, которые помнят состояние: в предыдущем уроке мы видели, что
make_multiplier(2)возвращает функцию, которая «помнит» число 2. Сегодня поймём, как это работает, — и как построить счётчик, накопитель или кэш безglobal.
Объяснение с основ
Функция внутри функции
Можно определить def внутри def. Внутренняя функция:
- Существует только внутри внешней функции — снаружи её нельзя вызвать.
- Видит переменные и параметры внешней функции.
Enclosing — буква E в LEGB
В уроке об области видимости мы изучили порядок поиска L → E → G → B. Когда внутренняя функция использует имя, Python ищет его:
L Local → inside the inner function
E Enclosing → inside the outer function
G Global → top level of the file
B Built-in → print, len, ...Замыкание — функция, которая помнит
Когда внешняя функция возвращает внутреннюю, происходит интересное: внешняя функция уже завершилась, а внутренняя всё ещё помнит её переменные. Это сочетание — функция + переменные, которые она помнит, — называется замыканием (Closure).
Внешняя функция пишет письмо (внутреннюю функцию) и кладёт его в конверт вместе с запиской с данными (переменными). Она отправляет конверт и исчезает, — но тот, кто откроет конверт, получит и письмо, и записку. В каждом конверте своя записка.
nonlocal
Читать переменную из объемлющей области можно всегда. Изменить её (присвоить новое значение) без объявления нельзя — точно как с глобальной. Объявление — nonlocal name: «эта переменная принадлежит объемлющей функции».
| Объявление | Переменная принадлежит… |
|---|---|
global x | Уровню файла |
nonlocal x | Ближайшей объемлющей функции |
Синтаксис
def outer():
count = 0 # enclosing variable
def inner():
nonlocal count # assignments go to outer's count
count += 1
return count
return inner # return the function itself (no parentheses)Первый пример кода
Приватная вспомогательная функция. week-2/receipt.py:
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Вывод:
Milk 6
Bread 9Разбор кода построчно
| Строка | Объяснение |
|---|---|
def line(name, price): | Внутренняя функция — существует только внутри format_receipt. |
name.ljust(10) | Дополняет имя пробелами до 10 символов, чтобы цены стояли в столбец. |
lines.append(line(name, price)) | Внешняя функция использует свою вспомогательную функцию. |
"\n".join(lines) | Соединяет все строки в один текст с переводом строки между ними. |
Ещё один пример
Замыкание — функция, которая помнит. week-2/greeter.py:
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Вывод:
Hello, Dana
Shalom, Avimake_greeter уже завершилась на строке hello = ..., но hello всё ещё помнит, что greeting — это "Hello". У каждой созданной функции своя копия переменной — shalom помнит "Shalom".
Счётчик с nonlocal. week-2/counter.py:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter(), counter())
other = make_counter()
print(other())Вывод:
1 2 3
1Каждый счётчик помнит свой count: other начинает с нуля, независимо от counter. Это приватное состояние — без глобальной переменной, которую может менять весь код.
Типичная ошибка
Ошибка: изменить переменную объемлющей функции без nonlocal.
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueПочему? Из-за присваивания count += 1 Python решает, что count — локальная переменная increment, — и затем пытается прочитать локальную, у которой ещё нет значения. Это ровно та же ошибка, что мы видели с глобальными переменными в уроке об области видимости.
Исправление: nonlocal count в начале increment.
Практика
Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.
Упражнение 1: Внутренняя вспомогательная функция
Напишите функцию format_receipt(items), которая получает список пар (название, цена) и возвращает текст чека. Внутри неё определите внутреннюю функцию line(name, price), которая дополняет название до 10 символов (ljust).
print(format_receipt([("Milk", 6), ("Bread", 9)]))Ожидаемый вывод:
Milk 6
Bread 9Показать решение
def format_receipt(items):
def line(name, price):
return name.ljust(10) + str(price)
lines = []
for name, price in items:
lines.append(line(name, price))
return "\n".join(lines)
print(format_receipt([("Milk", 6), ("Bread", 9)]))Milk 6
Bread 9line существует только внутри format_receipt — это приватная вспомогательная функция, и она не смешивается с остальным кодом.
Упражнение 2: Замыкание
Напишите make_greeter(greeting), которая возвращает функцию, получающую имя и возвращающую приветствие.
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Ожидаемый вывод:
Hello, Dana
Shalom, AviПоказать решение
def make_greeter(greeting):
def greet(name):
return greeting + ", " + name
return greet
hello = make_greeter("Hello")
shalom = make_greeter("Shalom")
print(hello("Dana"))
print(shalom("Avi"))Hello, Dana
Shalom, Avimake_greeter уже завершилась, но каждая возвращённая ею функция помнит свой greeting. Это и есть замыкание (Closure).
Упражнение 3: Предскажите вывод
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()Показать решение
def outer():
message = "outer"
def show():
print("show:", message)
def change():
nonlocal message
message = "changed"
show()
change()
show()
outer()show: outer
show: changedchange использует nonlocal, поэтому присваивание меняет message функции outer, а не создаёт новую локальную переменную.
Упражнение 4: Счётчик с шагом
Напишите make_counter(step) — каждый вызов счётчика увеличивает его на step и возвращает новое значение. Создайте два счётчика и проверьте, что они не влияют друг на друга.
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())Ожидаемый вывод:
2 4
10
6Показать решение
def make_counter(step):
count = 0
def increment():
nonlocal count
count += step
return count
return increment
by_two = make_counter(2)
by_ten = make_counter(10)
print(by_two(), by_two())
print(by_ten())
print(by_two())2 4
10
6stepвнутриincrementтолько читается, поэтому объявление не нужно.countменяется, поэтому нуженnonlocal count.
Упражнение 5: Без nonlocal
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
counter()- Какую ошибку вы получите и почему?
- В чём разница между
globalиnonlocal?
Показать решение
UnboundLocalError. Из-за присваиванияcount += 1Python решает, чтоcount— локальная переменнаяincrement, — и у неё нет значения, когда вы пытаетесь её увеличить. Исправление:nonlocal countв началеincrement.global— переменная принадлежит уровню файла.nonlocal— переменная принадлежит ближайшей объемлющей функции. Замыкание сnonlocalдаёт каждому счётчику приватное состояние без глобальной переменной, которую может менять весь код.
Итоги
- Вложенная функция — функция внутри функции. Существует только внутри и видит переменные объемлющей (Enclosing).
- Замыкание (Closure) — внутренняя функция, которую вернули и которая помнит переменные создавшей её функции. У каждой — своя копия.
- Читать переменную из объемлющей области — можно. Изменять — только с
nonlocal, иначеUnboundLocalError. global— переменная на уровне файла.nonlocal— переменная в объемлющей функции.- Замыкание даёт приватное состояние без глобальной переменной. И всё же, когда возможно, параметр на входе и
returnна выходе — лучше.