Статья

Замыкания в Python

В Python замыкание (closure) — это функция, которая находится внутри другой функции и ссылается на переменные объявленные в теле внешней функции. Это бывает полезно, когда нужно избежать написания небольших классов, содержащих только одну функцию. Замыкания способствуют сокрытию данных и предлагают альтернативу глобальным переменным.
В этой статье мы сначала опишем замыкания, а затем покажем их применение на различных примерах.

Вложенные функции

Прежде чем перейти к замыканиям, давайте поговорим о вложенных функциях: в Питоне функции могут быть определены вложенными, то есть находящимися одна внутри другой:
def outer_func(x):
    def inner_func():
        print(f"value: {x}")
    print(f"value: {x}")
    inner_func()

outer_func(0)
В этом примере outer_func является внешней функцией, а внутри нее мы определили другую функцию с именем inner_func — вложенную, или внутреннюю функцию. inner_func может быть вызвана изнутри outer_func. Мы можем видеть, что inner_func имеет доступ к переменным outer_func.
Х в примере выше называется свободной переменной — переменной, которая используется в блоке кода, но не определена там.

Замыкания

В Python, мы можем возвращать функции из других функций без их вызова, что и приводит нас к замыканиям:
def outer_func(x):
    def inner_func():
        print(f"inner: {x}")
    print(f"outer: {x}")
    return inner_func

my_closure = outer_func(0)
my_closure()
Теперь outer_func возвращает ссылку на функцию, а через my_closure = outer_func(0) мы определяем замыкание — и получаем объект, в котором переменная x (в данном случае 0) “привязана” к возвращаемому объекту — даже если позже x больше не будет доступен. Давайте еще раз обратимся к определению замыкания: замыкание – это внутренняя функция, которая обращается к переменным из своей области видимости (свободные переменные) и возвращается другой функцией.

Конечно, код выше пока не имеет особого смысла, поэтому давайте приведем пример получше:
def greeter(name):
    def greet():
        print(f"Привет, {name}!")
    return greet

ivan_greeter = greeter("Ivan")
helen_greeter = greeter("Helen")

ivan_greeter()
helen_greeter()

'''
результат:
Привет, Ivan!
Привет, Helen!
'''
Выше мы определили функцию, которую можно использовать для генерации личных приветствий.

Изменение переменных внутри замыканий

Теперь мы расширим наш пример выше, чтобы также подсчитать, как часто приветствуют конкретного человека. Для этого введем переменную счетчика. Однако наивная версия не запустится — мы должны использовать ключевое слово nonlocal, чтобы разрешить присваивать значения переменным из внешних областей:
def greeter(name):
    count = 0
    def greet():
        nonlocal count
        count += 1
        print(f"Привет, {name}! Я здоровался с Вами {count} раз")
    return greet

ivan_greeter = greeter("Ivan")
helen_greeter = greeter("Helen")

ivan_greeter()
helen_greeter()
ivan_greeter()
ivan_greeter()

'''
результат: 
Привет, Ivan! Я здоровался с Вами 1 раз
Привет, Helen! Я здоровался с Вами 1 раз
Привет, Ivan! Я здоровался с Вами 2 раз
Привет, Ivan! Я здоровался с Вами 3 раз
'''
Причина использования nonlocal в том, что count – это int и, следовательно, неизменяемый тип. Для изменяемых типов, таких как списки или словари, nonlocal не требуется:
def greeter(name):
    summary_dict = {"name": name, "count": 0}
    def greet():
        summary_dict["count"] += 1
        print(f"Привет, {summary_dict['name']}! Я здоровался с Вами {summary_dict['count']} раз.")
    return greet

ivan_greeter = greeter("Ivan")
helen_greeter = greeter("Helen")

ivan_greeter()
helen_greeter()
ivan_greeter()
ivan_greeter()

'''
результат: 
Привет, Ivan! Я здоровался с Вами 1 раз
Привет, Helen! Я здоровался с Вами 1 раз
Привет, Ivan! Я здоровался с Вами 2 раз
Привет, Ivan! Я здоровался с Вами 3 раз
'''

Зачем нужны замыкания?

В целом, замыкания поддерживают идею сокрытия данных и уменьшают потребность в глобальных переменных.
Давайте закончим статью некоторыми более реалистичными примерами. В первом случае мы хотим подсчитать, сколько раз вызывалась функция, обернув ее внутри замыкания. Обратите внимание, что это тесно связано с декораторами Python, и, в зависимости от варианта использования, они, вероятно, предпочтительнее здесь.
def call_counter(func):
    call_count = 0
    def call(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        print(f"Вызов {func.__name__}. Номер вызова: {call_count}.")
        func(*args, **kwargs)
    return call

def printer(msg):
    print(msg)
    
    
call_printer = call_counter(printer)
call_printer("вызов 1")
call_printer("вызов 2")

'''
результат: 
Вызов printer. Номер вызова: 1.
вызов 1
Вызов printer. Номер вызова: 2.
вызов 2
'''
В следующем примере мы определяем небольшой logger:
def make_logger():
    log_content = "Начало логгирования.\n"
    def log(msg):
        nonlocal log_content
        log_content += msg + "\n"
        return log_content
    return log

logger = make_logger()
log_content = logger("Log X")
log_content = logger("Log Y")
print(log_content)

'''
результат: 
Начало логгирования.
Log X
Log Y
'''
python