Почему вложенные в Python функции не называются замыканиями?


249

Я видел и использовал вложенные функции в Python, и они соответствуют определению замыкания. Так почему их называют nested functionsвместо closures?

Разве вложенные функции не являются замыканиями, потому что они не используются внешним миром?

ОБНОВЛЕНИЕ: я читал о замыканиях, и это заставило меня задуматься об этой концепции относительно Python. Я искал и нашел статью, упомянутую кем-то в комментарии ниже, но я не мог полностью понять объяснение в этой статье, поэтому я задаю этот вопрос.


8
Интересно, что некоторые поисковики нашли мне это, датированное декабрем 2006 года: effbot.org/zone/closure.htm . Я не уверен - на SO так осуждают "внешние дубликаты"?
HBW

1
PEP 227 - Статически Вложенные Области для получения дополнительной информации.
Честный Абэ

Ответы:


394

Закрытие происходит, когда функция имеет доступ к локальной переменной из охватывающей области, которая завершила свое выполнение.

def make_printer(msg):
    def printer():
        print msg
    return printer

printer = make_printer('Foo!')
printer()

Когда make_printerвызывается, новый стек помещается в стек с скомпилированным кодом для printerфункции в качестве константы и значением msgкак локальным. Затем он создает и возвращает функцию. Поскольку функция printerссылается на msgпеременную, она сохраняется после ее make_printerвозвращения.

Итак, если ваши вложенные функции не

  1. доступ к переменным, которые являются локальными для окружающих областей,
  2. делать это, когда они выполняются за пределами этой области,

тогда они не замыкания.

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

def make_printer(msg):
    def printer(msg=msg):
        print msg
    return printer

printer = make_printer("Foo!")
printer()  #Output: Foo!

Здесь мы привязываем значение к значению параметра по умолчанию. Это происходит при создании функции printer, поэтому после возврата не требуется сохранять ссылку на значение msgвнешнего значения . это просто нормальная локальная переменная функции в этом контексте.printermake_printermsgprinter


2
Вы отвечаете намного лучше, чем мой, вы делаете хорошее замечание, но если мы собираемся следовать самым строгим определениям функционального программирования, будут ли ваши примеры даже функциями? Это было давно, и я не могу вспомнить, если строгое функциональное программирование допускает функции, которые не возвращают значения. Суть спорная, если вы считаете, что возвращаемое значение равно None, но это совсем другая тема.
Микероби

6
@mikerobi, я не уверен, что нам нужно принимать во внимание функциональное программирование, так как python на самом деле не является функциональным языком, хотя он, безусловно, может использоваться как таковой. Но нет, внутренние функции не являются функциями в этом смысле, поскольку их цель - создавать побочные эффекты.
Впрочем

31
@mikerobi: Является ли капля кода замыканием, зависит от того, закрывается он или нет в окружающей среде, а не от того, как вы его называете. Это может быть процедура, функция, процедура, метод, блок, подпрограмма, что угодно. В Ruby методы не могут быть замыканиями, могут только блоки. В Java методы не могут быть замыканиями, а классы могут. Это не делает их менее замкнутыми. (Хотя тот факт, что они закрываются только по некоторым переменным и не могут их изменять, делает их почти бесполезными.) Можно утверждать, что метод - это просто закрытая процедура self. (В JavaScript / Python это почти так.)
Jörg W Mittag

3
@ JörgWMittag Пожалуйста, определите «закрывается».
Евгений Сергеев

4
@EvgeniSergeev «закрывается», т. Е. Ссылается на «локальную переменную [скажем, iиз окружающего объема» ». ссылается, т.е. может проверять (или изменять) iзначение, даже если / когда эта область «закончила свое выполнение», то есть выполнение программы перешло к другим частям кода. Блок, в котором iопределено, больше не существует, но функция (и), ссылающаяся на него, iвсе еще может это сделать. Обычно это называется «закрытием по переменной i». Чтобы не иметь дело с конкретными переменными, это может быть реализовано как закрытие по всему фрейму среды, где определена эта переменная.
Уилл Несс

103

Ааронастерлинг уже ответил на вопрос

Тем не менее, кто-то может быть заинтересован в том, как переменные хранятся под капотом.

Прежде чем перейти к фрагменту:

Замыкания - это функции, которые наследуют переменные из окружающей их среды. Когда вы передаете функцию обратного вызова в качестве аргумента другой функции, которая будет выполнять ввод / вывод, эта функция обратного вызова будет вызвана позже, и эта функция - почти волшебным образом - запомнит контекст, в котором она была объявлена, вместе со всеми доступными переменными в этом контексте.

  • Если функция не использует свободные переменные, она не образует замыкание.

  • Если есть другой внутренний уровень, который использует свободные переменные - все предыдущие уровни сохраняют лексическую среду (пример в конце)

  • Атрибуты функций func_closureв python <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'
>>>

9
В Python 3 func_closureтеперь вызывается __closure__, как и другие func_*атрибуты.
LVC

3
Также __closure_доступно в Python 2.6+ для совместимости с Python 3.
Pierre

Закрытие относится к записи, в которой хранятся закрытые переменные, прикрепленные к объекту функции. Это не сама функция. В Python это __closure__объект, который является замыканием.
Мартин Питерс

Спасибо @MartijnPieters за разъяснения.
Джеймс Сапам

71

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. В Python 3.x используйте nonlocalключевое слово (предложенное @unutbu и @leewz)

3. Определите простой модифицируемый класс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себя, поэтому ошибки не возникает.

4. Альтернативный способ, как указал @unutbu, - определить каждую переменную как массив ( x = [0]) и изменить ее первый элемент ( x[0] += 1). Снова ошибка не возникает, потому что xсам не изменен.

5. Как предлагает @raxacoricofallapatorius, вы можете сделать xсвойствоcounter

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

27
Есть способы обойти это. В Python2 вы можете сделать x = [0]во внешней области и использовать x[0] += 1во внутренней области. В Python3 вы можете оставить свой код как есть и использовать нелокальное ключевое слово .
unutbu

«Хотя внутренняя функция может читать переменные внешней функции, она не может их записывать». - Это неточно согласно комментарию unutbu. Проблема в том, что когда Python встречает что-то вроде x = ..., x интерпретируется как локальная переменная, которая, конечно, еще не определена в этой точке. OTOH, если x является изменяемым объектом с изменяемым методом, его можно легко изменить, например, если x является объектом, который поддерживает метод inc (), который изменяет сам себя, x.inc () будет работать без заминки.
Thanh DK

@ThanhDK Разве это не значит, что вы не можете писать в переменную? Когда вы используете вызов метода из изменяемого объекта, вы просто указываете ему изменить себя, вы фактически не изменяете переменную (которая просто содержит ссылку на объект). Другими словами, ссылка, на которую xуказывает переменная, остается точно такой же, даже если вы вызываете inc()или что-то еще, и вы не выполняли эффективную запись в переменную.
user193130

4
Есть другой вариант, строго лучше, чем у # 2, сделать xсвойствоcounter .
ором

9
В Python 3 есть nonlocalключевое слово, которое похоже globalна переменные внешней функции. Это позволит внутренней функции перепривязать имя из ее внешней функции. Я думаю, что «привязать к имени» является более точным, чем «изменить переменную».
leewz

16

В 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Ключевое слово связывает внутреннюю функцию к внешней переменной явно упомянутый, по сути , заключив его. Отсюда более явно «закрытие».


1
Интересно, для справки: docs.python.org/3/reference/… . Я не знаю, почему нелегко найти больше информации о замыканиях (и как вы можете ожидать, что они будут вести себя, исходя из JS) в документации по python3?
user3773048

9

У меня была ситуация, когда мне нужно было отдельное, но постоянное пространство имен. Я использовал классы. Я не иначе. Сегрегированные, но постоянные имена являются замыканиями.

>>> 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

6
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

Это пример того, что такое замыкание и как его можно использовать.


0

Я хотел бы предложить еще одно простое сравнение между примером 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ключевым словом.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.