Почему функция f возвращает [...] 3,14? Я думал, что после выполнения функции все локальное пространство имен должно быть уничтожено is'nt?
Да и нет.
В этом случае необходимые переменные из локального пространства имен сохраняются как так называемое «замыкание» для локально определенной функции. В этом случае переменная pi
остается доступной для этой функции до тех пор, пока она не понадобится.
Давайте немного уточним:
def foo():
pi = 3.14
def f():
return pi
return f
Это внешняя функция.
В CLI мы можем немного поиграться.
>>> foo
<function foo at 0x000001B4E5282E18>
>>> dir(foo)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
А, есть что-то вроде __closure__
, слово, которое я использовал раньше. Что это?
>>> foo.__closure__
>>>
А?
>>> foo.__closure__ is None
True
А
>>> f = foo() # get the inner function
>>> f
<function foo.<locals>.f at 0x000001B4E5C6C158>
>>> f()
3.14
Хорошо. Посмотрим, что у него внутри:
>>> f.__closure__
(<cell at 0x000001B4E5C34A08: float object at 0x000001B4E5231648>,)
Что это теперь?
>>> c = f.__closure__[0]
>>> c
<cell at 0x000001B4E5C34A08: float object at 0x000001B4E5231648>
Ячейка?
>>> dir(c)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>> c.cell_contents
3.14
Ах. Итак, f.__closure__[0]
- это ячейка, что-то вроде контейнера, для значения, взятого из локального пространства имен выше.
В качестве бонуса мы могли бы изучить разборку функций:
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (3.14)
2 STORE_DEREF 0 (pi)
3 4 LOAD_CLOSURE 0 (pi)
6 BUILD_TUPLE 1
8 LOAD_CONST 2 (<code object f at 0x000001B4E5C418A0, file "<stdin>", line 3>)
10 LOAD_CONST 3 ('foo.<locals>.f')
12 MAKE_FUNCTION 8
14 STORE_FAST 0 (f)
5 16 LOAD_FAST 0 (f)
18 RETURN_VALUE
>>> dis.dis(f)
4 0 LOAD_DEREF 0 (pi)
2 RETURN_VALUE
Здесь мы видим, как строится f
:
3 4 LOAD_CLOSURE 0 (pi)
Загрузить переменную pi
как закрытие (ячейку)
6 BUILD_TUPLE 1
Строить кортеж только с этой ячейкой
8 LOAD_CONST 2 (<code object f at 0x000001B4E5C418A0, file "<stdin>", line 3>)
10 LOAD_CONST 3 ('foo.<locals>.f')
12 MAKE_FUNCTION 8
Создать функцию с заданным именем, кодом и закрытием
14 STORE_FAST 0 (f)
Сохранить.
В функции доступ к закрывающим элементам осуществляется с помощью LOAD_DEREF
.
Если мы немного расширим функцию, например,
def foo():
pi = 3.14
two = 2
three = 3
def f():
return pi - three
return f
, мы увидим, как обрабатываются и обрабатываются эти переменные:
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (3.14)
2 STORE_DEREF 0 (pi)
3 4 LOAD_CONST 2 (2)
6 STORE_FAST 0 (two)
4 8 LOAD_CONST 3 (3)
10 STORE_DEREF 1 (three)
5 12 LOAD_CLOSURE 0 (pi)
14 LOAD_CLOSURE 1 (three)
16 BUILD_TUPLE 2
18 LOAD_CONST 4 (<code object f at 0x000001B4E5CA36F0, file "<stdin>", line 5>)
20 LOAD_CONST 5 ('foo.<locals>.f')
22 MAKE_FUNCTION 8
24 STORE_FAST 1 (f)
7 26 LOAD_FAST 1 (f)
28 RETURN_VALUE
См. чем переменные pi
и three
отличаются от two
: two
сохраняется с STORE_FAST
, остальные используют STORE_DEREF
, чтобы их можно было передать функции.
>>> foo().__closure__
(<cell at 0x000001B4E5BC41F8: float object at 0x000001B4E5231528>, <cell at 0x000001B4E5C348B8: int object at 0x0000000050816120>)
Теперь у него два элемента:
>>> foo().__closure__[0].cell_contents
3.14
>>> foo().__closure__[1].cell_contents
3
И вот как он используется:
>>> dis.dis(f)
6 0 LOAD_DEREF 0 (pi)
2 LOAD_DEREF 1 (three)
4 BINARY_SUBTRACT
6 RETURN_VALUE
Вычитание действительно происходит внутри внутреннего f unction, поскольку переменные даже могут изменяться:
import time
import threading
def foo():
c = 0
def run():
nonlocal c
while c < 50:
c += 1
time.sleep(1.0)
t = threading.Thread(target=run)
t.start()
def f(): return c
return f
Здесь поток увеличивает значение переменной каждую секунду. Если мы теперь выполним f = foo()
, мы получим эту внутреннюю функцию, которая возвращает разные значения, если вызывается несколько раз с некоторым временем между вызовами.