Почему вложенные в 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 функция вернулась.

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

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

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

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

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, см., например, обсуждение кортежей

Наша способность использовать кортежи в качестве элементов других кортежей предоставляет новые средства комбинирования в нашем языке программирования. Мы называем возможность кортежей вкладываться подобным образом свойством замыкания типа данных кортежа. В общем случае метод объединения значений данных удовлетворяет свойству замыкания, если сам результат объединения может быть объединен с использованием того же метода.

Другие вопросы по тегам