Замыкание (Closure)#

Замыкание (closure) — функция, которая находится внутри другой функции и ссылается на переменные объявленные в теле внешней функции (свободные переменные).

Внутренняя функция создается каждый раз во время выполнения внешней. Каждый раз при вызове внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.

Ссылки на переменные внешней функции действительны внутри вложенной функции до тех пор, пока работает вложенная функция, даже если внешняя функция закончила работу, и переменные вышли из области видимости.

Пример замыкания:

def multiply(num1):
    var = 10
    def inner(num2):
        return num1 * num2
    return inner

Тут замыканием является функция inner. Функция inner использует внутри себя переменную num1 - параметр функции multiply, поэтому переменная num1 будет запомнена, а вот переменная var не используется и запоминатся не будет.

Использование созданной функции выглядит так:

Сначала делается вызов функции multiply с передачей одного аргумента, значение которого запишется в переменную num1:

In [2]: mult_by_9 = multiply(9)

Переменная mult_by_9 ссылается на внутреннюю функцию inner и при этом внутренняя функция помнит значение num1 = 9 и поэтому все числа будут умножаться на 9:

In [3]: mult_by_9
Out[3]: <function __main__.multiply.<locals>.inner(num2)>

In [4]: mult_by_9.__closure__
Out[4]: (<cell at 0xb0bd5f2c: int object at 0x836bf60>,)

In [5]: mult_by_9.__closure__[0].cell_contents
Out[5]: 9

In [8]: mult_by_9(10)
Out[8]: 90

In [9]: mult_by_9(2)
Out[9]: 18

Еще один пример замыкания с несколькими свободными переменными:

def func1():
    a = 1
    b = 'line'
    c = [1, 2, 3]

    def func2():
        return a, b, c

    return func2

In [11]: call_func = func1()

In [12]: call_func
Out[12]: <function __main__.func1.<locals>.func2()>

In [13]: call_func.__closure__
Out[13]:
(<cell at 0xb12170bc: int object at 0x836bee0>,
 <cell at 0xb12172e4: str object at 0xb732d720>,
 <cell at 0xb12177f4: list object at 0xb4e6d66c>)

In [14]: for item in call_func.__closure__:
    ...:     print(item, item.cell_contents)
    ...:
<cell at 0xb12170bc: int object at 0x836bee0> 1
<cell at 0xb12172e4: str object at 0xb732d720> line
<cell at 0xb12177f4: list object at 0xb4e6d66c> [1, 2, 3]

Изменение свободных переменных#

Для получения значения свободной переменной достаточно обратиться к ней, однако, при изменении значений есть нюансы. Если переменная ссылается на изменяемый объект, например, список, изменение содержимого делается стандартным образом без каких-либо проблем. Однако если необходимо, к примеру, добавить 1 к числу, мы получим ошибку:

def func1():
    a = 1
    b = 'line'
    c = [1, 2, 3]

    def func2():
        c.append(4)
        a = a + 1
        return a, b, c

    return func2


In [32]: call_func = func1()

In [33]: call_func()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-33-9288e4e0f32f> in <module>
----> 1 call_func()

<ipython-input-31-56414e2c364b> in func2()
      6     def func2():
      7         c.append(4)
----> 8         a += 1
      9         return a, b, c
     10

UnboundLocalError: local variable 'a' referenced before assignment

In [34]: for item in call_func.__closure__:
    ...:     print(item, item.cell_contents)
    ...:
<cell at 0xb12174c4: str object at 0xb732d720> line
<cell at 0xb1217af4: list object at 0xb11e5dac> [1, 2, 3, 4]

Если необходимо присвоить свободной переменной другое значение, необходимо явно объявить ее как nonlocal:

def func1():
    a = 1
    b = 'line'
    c = [1, 2, 3]

    def func2():
        nonlocal a
        c.append(4)
        a += 1
        return a, b, c

    return func2


In [41]: call_func = func1()

In [42]: call_func()
Out[42]: (2, 'line', [1, 2, 3, 4])

In [43]: for item in call_func.__closure__:
    ...:     print(item, item.cell_contents)
    ...:
<cell at 0xb11fc6bc: int object at 0x836bef0> 2
<cell at 0xb11fcdac: str object at 0xb732d720> line
<cell at 0xb11fc56c: list object at 0xb117fe2c> [1, 2, 3, 4]

Использование nonlocal нужно только если необходимо менять свободную переменную сохраняя измененное значение между вызовами внутренней функции. Для обычного переприсваивания значения ничего делать не нужно.

Пример использования nonlocal с повторным вызовом внутренней функции:

def countdown(n):
    def step():
        nonlocal n
        r = n
        n -= 1
        return r
    return step

In [49]: do_step = countdown(10)

In [50]: do_step()
Out[50]: 10

In [51]: do_step()
Out[51]: 9

In [52]: do_step()
Out[52]: 8

In [53]: do_step()
Out[53]: 7

Примеры использования замыкания#

Пример с подключением SSH:

from netmiko import ConnectHandler

device_params = {
    'device_type': 'cisco_ios',
    'ip': '192.168.100.1',
    'username': 'cisco',
    'password': 'cisco',
    'secret': 'cisco'
}

def netmiko_ssh(**params_dict):
        ssh = ConnectHandler(**params_dict)
        ssh.enable()
        def send_show_command(command):
            return ssh.send_command(command)
        netmiko_ssh.send_show_command = send_show_command
        return send_show_command


In [25]: r1 = netmiko_ssh(**device_params)

In [26]: r1('sh clock')
Out[26]: '*15:14:13.240 UTC Wed Oct 2 2019'