Почему вложенные в Python функции не называются замыканиями?
Я видел и использовал вложенные функции в Python, и они соответствуют определению замыкания. Так почему они называются nested functions
вместо closures
?
Разве вложенные функции не являются замыканиями, потому что они не используются внешним миром?
ОБНОВЛЕНИЕ: я читал о замыканиях, и это заставило меня задуматься об этой концепции относительно Python. Я искал и нашел статью, упомянутую кем-то в комментарии ниже, но я не мог полностью понять объяснение в этой статье, поэтому я задаю этот вопрос.
10 ответов
Закрытие происходит, когда функция имеет доступ к локальной переменной из включающей области, которая завершила свое выполнение.
def make_printer(msg):
def printer():
print msg
return printer
printer = make_printer('Foo!')
printer()
когда make_printer
вызывается новый кадр помещается в стек с скомпилированным кодом для printer
функционировать как константа и значение msg
как местный. Затем он создает и возвращает функцию. Потому что функция printer
ссылается на msg
переменная, она сохраняется после make_printer
функция вернулась.
Итак, если ваши вложенные функции не
- доступ к переменным, которые являются локальными для окружающих областей,
- делать это, когда они выполняются за пределами этой области,
тогда они не замыкания.
Вот пример вложенной функции, которая не является замыканием.
def make_printer(msg):
def printer(msg=msg):
print msg
return printer
printer = make_printer("Foo!")
printer() #Output: Foo!
Здесь мы привязываем значение к значению параметра по умолчанию. Это происходит, когда функция printer
создан и поэтому нет ссылки на значение msg
внешний по отношению к printer
необходимо поддерживать после make_printer
возвращается. msg
это просто нормальная локальная переменная функции printer
в данном контексте.
aaronasterling уже ответил на aaronasterling
Тем не менее, кто-то может быть заинтересован в том, как переменные хранятся под капотом.
Прежде чем перейти к фрагменту:
Замыкания - это функции, которые наследуют переменные из окружающей их среды. Когда вы передаете функцию обратного вызова в качестве аргумента другой функции, которая будет выполнять ввод / вывод, эта функция обратного вызова будет вызвана позже, и эта функция - почти волшебным образом - запомнит контекст, в котором она была объявлена, вместе со всеми доступными переменными в этом контексте.
Если функция не использует свободные переменные, она не образует замыкание.
Если есть другой внутренний уровень, который использует свободные переменные - все предыдущие уровни сохраняют лексическую среду (пример в конце)
атрибуты функции
func_closure
в питоне < 3.X или__closure__
в Python > 3.X сохранить свободные переменные.Каждая функция в python имеет атрибуты замыкания, но не сохраняет никакого содержимого, если нет свободных переменных.
пример: атрибутов замыкания, но нет содержимого внутри, так как нет свободной переменной.
>>> def foo():
... def fii():
... pass
... return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>
NB: БЕСПЛАТНАЯ ПЕРЕМЕННАЯ ДОЛЖНА СОЗДАТЬ ЗАКРЫТИЕ.
Я объясню, используя тот же фрагмент, что и выше:
>>> def make_printer(msg):
... def printer():
... print msg
... return printer
...
>>> printer = make_printer('Foo!')
>>> printer() #Output: Foo!
И все функции Python имеют атрибут замыкания, поэтому давайте рассмотрим переменные, связанные с функцией замыкания.
Вот атрибут func_closure
для функции printer
>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>
closure
Атрибут возвращает кортеж из ячеистых объектов, которые содержат сведения о переменных, определенных во вложенной области видимости.
Первый элемент в func_closure, который может быть None или набором ячеек, которые содержат привязки для свободных переменных функции и доступны только для чтения.
>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
'__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>
Здесь в приведенном выше выводе вы можете увидеть cell_contents
посмотрим что там хранится:
>>> printer.func_closure[0].cell_contents
'Foo!'
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>
Итак, когда мы вызвали функцию printer()
, он получает доступ к значению, хранящемуся внутри cell_contents
, Вот как мы получили вывод "Foo!"
Я снова объясню использование приведенного выше фрагмента с некоторыми изменениями:
>>> def make_printer(msg):
... def printer():
... pass
... return printer
...
>>> printer = make_printer('Foo!')
>>> printer.func_closure
>>>
В приведенном выше фрагменте я не печатаю msg внутри функции принтера, поэтому она не создает свободной переменной. Поскольку свободной переменной нет, внутри замыкания не будет содержимого. Это именно то, что мы видим выше.
Теперь я объясню другой фрагмент, чтобы очистить все Free Variable
с Closure
:
>>> def outer(x):
... def intermediate(y):
... free = 'free'
... def inner(z):
... return '%s %s %s %s' % (x, y, free, z)
... return inner
... return intermediate
...
>>> outer('I')('am')('variable')
'I am free variable'
>>>
>>> inter = outer('I')
>>> inter.func_closure
(<cell at 0x10c989130: str object at 0x10c831b98>,)
>>> inter.func_closure[0].cell_contents
'I'
>>> inn = inter('am')
Итак, мы видим, что func_closure
Свойство - это кортеж ячеек замыкания, мы можем явно ссылаться на них и их содержимое - ячейка имеет свойство "cell_contents"
>>> inn.func_closure
(<cell at 0x10c9807c0: str object at 0x10c9b0990>,
<cell at 0x10c980f68: str object at 0x10c9eaf30>,
<cell at 0x10c989130: str object at 0x10c831b98>)
>>> for i in inn.func_closure:
... print i.cell_contents
...
free
am
I
>>>
Вот когда мы позвонили inn
, он будет ссылаться на все переменные сохранения, поэтому мы получаем I am free variable
>>> inn('variable')
'I am free variable'
>>>
Python имеет слабую поддержку для закрытия. Чтобы понять, что я имею в виду, возьмем следующий пример счетчика, использующего замыкание с JavaScript:
function initCounter(){
var x = 0;
function counter () {
x += 1;
console.log(x);
};
return counter;
}
count = initCounter();
count(); //Prints 1
count(); //Prints 2
count(); //Prints 3
Закрытие довольно элегантно, поскольку дает функциям, написанным таким образом, возможность иметь "внутреннюю память". Начиная с Python 2.7 это невозможно. Если вы попытаетесь
def initCounter():
x = 0;
def counter ():
x += 1 ##Error, x not defined
print x
return counter
count = initCounter();
count(); ##Error
count();
count();
Вы получите сообщение о том, что x не определен. Но как это может быть, если другим было показано, что вы можете напечатать это? Это связано с тем, как Python управляет областью видимости переменных функций. Хотя внутренняя функция может читать переменные внешней функции, она не может их записывать.
Это действительно позор. Но только с закрытием только для чтения вы можете по крайней мере реализовать шаблон функции-декоратора, для которого Python предлагает синтаксический сахар.
Обновить
Как уже указывалось, есть способы справиться с ограничениями области действия Python, и я расскажу о некоторых.
1. Используйте global
ключевое слово (как правило, не рекомендуется).
2. Определите простой модифицируемый класс Object
class Object(object):
pass
и создать Object scope
в initCounter
хранить переменные
def initCounter ():
scope = Object()
scope.x = 0
def counter():
scope.x += 1
print scope.x
return counter
поскольку scope
на самом деле это просто ссылка, действия, предпринятые с его полями, не изменяют scope
само по себе, поэтому никакой ошибки не возникает.
3. Как указал @unutbu, альтернативный способ - определить каждую переменную как массив (x = [0]
) и измените его первый элемент (x[0] += 1
). Опять ошибка не возникает, потому что x
Сам не изменен.
4. Как предложено @raxacoricofallapatorius, вы можете сделать x
свойство counter
def initCounter ():
def counter():
counter.x += 1
print counter.x
counter.x = 0
return counter
В Python 2 не было замыканий - у него были обходные пути, которые напоминали замыкания.
В ответах уже есть множество примеров: копирование переменных во внутреннюю функцию, изменение объекта во внутренней функции и т. Д.
В Python 3 поддержка более четкая и краткая:
def closure():
count = 0
def inner():
nonlocal count
count += 1
print(count)
return inner
Использование:
start = closure()
start() # prints 1
start() # prints 2
start() # prints 3
nonlocal
Ключевое слово связывает внутреннюю функцию с явно упомянутой внешней переменной, по сути, заключая ее в себе. Следовательно, более явно "закрытие".
У меня была ситуация, когда мне нужно было отдельное, но постоянное пространство имен. Я использовал классы. Я не иначе. Отдельные, но постоянные имена являются замыканиями.
>>> class f2:
... def __init__(self):
... self.a = 0
... def __call__(self, arg):
... self.a += arg
... return(self.a)
...
>>> f=f2()
>>> f(2)
2
>>> f(2)
4
>>> f(4)
8
>>> f(8)
16
# **OR**
>>> f=f2() # **re-initialize**
>>> f(f(f(f(2)))) # **nested**
16
# handy in list comprehensions to accumulate values
>>> [f(i) for f in [f2()] for i in [2,2,4,8]][-1]
16
def nested1(num1):
print "nested1 has",num1
def nested2(num2):
print "nested2 has",num2,"and it can reach to",num1
return num1+num2 #num1 referenced for reading here
return nested2
дает:
In [17]: my_func=nested1(8)
nested1 has 8
In [21]: my_func(5)
nested2 has 5 and it can reach to 8
Out[21]: 13
Это пример того, что такое замыкание и как его можно использовать.
Люди не понимают, что такое закрытие. Замыкание - это не внутренняя функция. значение закрытия - это акт закрытия. Таким образом, внутренняя функция закрывается над нелокальной переменной, которая называется свободной переменной.
def counter_in(initial_value=0):
# initial_value is the free variable
def inc(increment=1):
nonlocal initial_value
initial_value += increment
return print(initial_value)
return inc
когда ты звонишь
counter_in()
это вернется
inc
функция со свободной переменной
initial_value
. Итак, мы создали ЗАКРЫТИЕ. люди звонят
inc
как функция закрытия, и я думаю, что это сбивает людей с толку, люди думают, что "нормально, внутренние функции - это замыкания". в действительности
inc
не является закрытием, так как это часть закрытия, чтобы облегчить жизнь, они называют это функцией закрытия.
myClosingOverFunc=counter_in(2)
это возвращается
inc
функция, закрывающая свободную переменную
initial_value
. когда вы вызываете
myClosingOverFunc
myClosingOverFunc()
он напечатает 2.
когда python видит, что система закрытия существует, он создает новый объект с именем CELL. это сохранит только имя свободной переменной, которая
initial_value
в этом случае. Этот объект Cell будет указывать на другой объект, в котором хранится значение
initial_value
.
в нашем примере
initial_value
во внешней функции и внутренней функции будут указывать на этот объект ячейки, и этот объект ячейки будет указывать на значение
initial_value
.
variable initial_value =====>> CELL ==========>> value of initial_value
Итак, когда вы звоните
counter_in
его объем исчез, но это не имеет значения. потому что переменная
initial_value
напрямую ссылается на CELL Obj. и косвенно ссылается на значение
initial_value
. Вот почему, даже если объем внешней функции исчез, внутренняя функция все равно будет иметь доступ к свободной переменной.
скажем, я хочу написать функцию, которая принимает функцию как аргумент и возвращает, сколько раз эта функция вызывается.
def counter(fn):
# since cnt is a free var, python will create a cell and this cell will point to the value of cnt
# every time cnt changes, cell will be pointing to the new value
cnt = 0
def inner(*args, **kwargs):
# we cannot modidy cnt with out nonlocal
nonlocal cnt
cnt += 1
print(f'{fn.__name__} has been called {cnt} times')
# we are calling fn indirectly via the closue inner
return fn(*args, **kwargs)
return inner
в этом примере
cnt
это наша бесплатная переменная и
inner
+
cnt
создать ЗАКРЫТИЕ. когда python увидит это, он создаст CELL Obj и
cnt
всегда будет напрямую ссылаться на этот объект ячейки, а CELL будет ссылаться на другой объект в памяти, в котором хранится значение
cnt
. изначально cnt=0.
cnt ======>>>> CELL =============> 0
когда вы вызываете внутреннюю функцию с передачей параметра
counter(myFunc)()
это увеличит cnt на 1. Таким образом, наша схема ссылок изменится следующим образом:
cnt ======>>>> CELL =============> 1 #first counter(myFunc)()
cnt ======>>>> CELL =============> 2 #second counter(myFunc)()
cnt ======>>>> CELL =============> 3 #third counter(myFunc)()
это только один случай закрытия. Вы можете создать несколько экземпляров закрытия, передав другую функцию
counter(differentFunc)()
это создаст другой объект CELL из приведенного выше. Мы только что создали еще один экземпляр закрытия.
cnt ======>> difCELL ========> 1 #first counter(differentFunc)()
cnt ======>> difCELL ========> 2 #secon counter(differentFunc)()
cnt ======>> difCELL ========> 3 #third counter(differentFunc)()
Я хотел бы предложить еще одно простое сравнение между примером Python и JS, если это поможет прояснить ситуацию.
JS:
function make () {
var cl = 1;
function gett () {
console.log(cl);
}
function sett (val) {
cl = val;
}
return [gett, sett]
}
и выполнение:
a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3
Python:
def make ():
cl = 1
def gett ():
print(cl);
def sett (val):
cl = val
return gett, sett
и выполнение:
g, s = make()
g() #1
s(2); g() #1
s(3); g() #1
Причина: как и многие другие говорили выше, в python, если во внутренней области видимости есть присвоение переменной с тем же именем, создается новая ссылка во внутренней области видимости. Не так с JS, если вы явно не объявите один с var
ключевое слово.
Здесь представлен способ определить, является ли функция замыканием или нет через объекты.
Как уже упоминалось в других ответах, не каждая вложенная функция является закрытием. Для составной функции (которая представляет общее действие) ее промежуточные состояния могут быть либо замыканием, либо вложенной функцией. Замыкание — это своего рода функция, которая «параметризована» своей (непустой) охватывающей областью видимости, пространством свободных переменных. Обратите внимание, что составная функция может быть создана обоими типами.
Внутренний тип (Python)объект представляет скомпилированное тело функции. Его атрибут и может использоваться для «обзора» закрытия/области функции. Как упоминалось в документе
-
co_freevars
: кортеж имен свободных переменных (на которые ссылается замыкание функции) -
co_cellvars
: кортеж имен переменных ячеек (на которые ссылаются содержащие области).
Как только функция прочитана, при выполнении рекурсивных вызовов частичная функция возвращается со своим собственным(следовательноcell_contents
) и список свободных переменных из его clousre и в его области.
Введем некоторые вспомогательные функции
# the "lookarounds"
def free_vars_from_closure_of(f):
print(f.__name__, 'free vars from its closure', f.__code__.co_cellvars)
def free_vars_in_scopes_of(f):
print(f.__name__, 'free vars in its scope ', f.__code__.co_freevars)
# read cells values
def cell_content(f):
if f.__closure__ is not None:
if len(f.__closure__) == 1: # otherwise problem with join
c = f.__closure__[0].cell_contents
else:
c = ','.join(str(c.cell_contents) for c in f.__closure__)
else:
c = None
print(f'cells of {f.__name__}: {c}')
Вот пример из другого ответа, переписанного более систематически.
def f1(x1):
def f2(x2):
a = 'free' # <- better choose different identifier to avoid confusion
def f3(x3):
return '%s %s %s %s' % (x1, x2, a, x3)
return f3
return f2
# partial functions
p1 = f1('I')
p2 = p1('am')
# lookaround
for p in (f1, p1, p2):
free_vars_in_scopes_of(p)
free_vars_from_closure_of(p)
cell_content(p)
Выход
f1 free vars in its scope () # <- because it's the most outer function
f1 free vars from its closure ('x1',)
cells of f1: None
f2 free vars in its scope ('x1',)
f2 free vars from its closure ('a', 'x2')
cells of f2: I
f3 free vars in its scope ('a', 'x1', 'x2')
f3 free vars from its closure () # <- because it's the most inner function
cells of f3: free, I, am
The lambda
аналог:
def g1(x1):
return lambda x2, a='free': lambda x3: '%s %s %s %s' % (x1, x2, a, x3)
С точки зрения свободных переменных/области видимости эквивалентны. Единственными незначительными отличиями являются некоторые значения некоторых атрибутовcode
объект:co_varnames
,co_consts
,co_code
,co_lnotab
,co_stacksize
... и, естественно,__name__
атрибут.
Смешанный пример, замыкания и не сразу:
# example: counter
def h1(): # <- not a closure
c = 0
def h2(c=c): # <- not a closure
def h3(x): # <- closure
def h4(): # <- closure
nonlocal c
c += 1
print(c)
return h4
return h3
return h2
# partial functions
p1 = h1()
p2 = p1()
p3 = p2('X')
p1() # do nothing
p2('X') # do nothing
p2('X') # do nothing
p3() # +=1
p3() # +=1
p3() # +=1
# lookaround
for p in (h1, p1, p2, p3):
free_vars_in_scopes_of(p)
#free_vars_from_closure_of(p)
cell_content(p)
Выход
1 X
2 X
3 X
h1 free vars in its scope ()
cells of h1: None
h2 free vars in its scope ()
cells of h2: None
h3 free vars in its scope ('c',)
cells of h3: 3
h4 free vars in its scope ('c', 'x')
cells of h4: 3,X
h1
иh2
оба не являются замыканиями , поскольку в их области видимости нет ячеек и свободных переменных. иh3
являются замыканиями и совместно используют (в данном случае) одну и ту же ячейку и свободную переменную дляc
.h4
имеет дополнительную свободную переменнуюx
со своей ячейкой.
Заключительные соображения:
- в
__closure__
атрибут и__code__.co_freevars
может использоваться для проверки значений и имен (идентификаторов) свободных переменных - антианалогии (в широком смысле) между и :
nonlocal
действует на внешнюю функцию,__code__.co_cellvars
вместо этого к внутренней функции
Для читателей «Структура и интерпретация компьютерных программ» (SICP): есть 2 несвязанных значения замыкания (CS VS Math), последнее/менее распространенное см. в Википедии:
Сассман и Абельсон также использовали термин « замыкание » в 1980-х годах во втором, несвязанном значении: свойство оператора, добавляющего данные в структуру данных , также иметь возможность добавлять вложенные структуры данных. Такое использование термина происходит от использования математики, а не от предшествующего использования в информатике. Авторы считают такое совпадение терминологии «неудачным».
Второе (математическое) значение также используется в SICP в Python, см., например, обсуждение кортежей
Наша способность использовать кортежи в качестве элементов других кортежей предоставляет новые средства комбинирования в нашем языке программирования. Мы называем возможность кортежей вкладываться подобным образом свойством замыкания типа данных кортежа. В общем случае метод объединения значений данных удовлетворяет свойству замыкания, если сам результат объединения может быть объединен с использованием того же метода.