Можете ли вы объяснить замыкания (поскольку они относятся к Python)?


85

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

Ответы:


97

Закрытие на закрытии

Объекты - это данные с прикрепленными методами, замыкания - это функции с прикрепленными данными.

def make_counter():
    i = 0
    def counter(): # counter() is a closure
        nonlocal i
        i += 1
        return i
    return counter

c1 = make_counter()
c2 = make_counter()

print (c1(), c1(), c2(), c2())
# -> 1 2 1 2

6
Обратите внимание, что nonlocalбыл добавлен в python 3, в python 2.x не было полноценных закрытий для чтения-записи (то есть вы могли читать закрытые переменные, но не изменять их значения)
Джеймс Портер

6
@JamesPorter: примечание: вы можете эмулировать nonlocalключевое слово в Python 2, используя изменяемый объект, например, L = [0] \n def counter(): L[0] += 1; return L[0]то есть вы не можете изменить имя (привязать его к другому объекту) в этом случае, но вы можете изменить сам изменяемый объект, на который ссылается имя к. Список необходим, потому что целые числа неизменяемы в Python.
jfs

1
@JFSebastian: верно. это всегда кажется грязным, грязным взломом :)
Джеймс Портер

46

Это просто: функция, которая ссылается на переменные из содержащей области видимости, потенциально после того, как поток управления покинул эту область. Последний бит очень полезен:

>>> def makeConstantAdder(x):
...     constant = x
...     def adder(y):
...         return y + constant
...     return adder
... 
>>> f = makeConstantAdder(12)
>>> f(3)
15
>>> g = makeConstantAdder(4)
>>> g(3)
7

Обратите внимание, что 12 и 4 «исчезли» внутри f и g, соответственно, эта особенность делает f и g правильными замыканиями.


В этом нет необходимости constant = x; вы можете просто сделать это return y + xво вложенной функции (или получить аргумент с именем constant), и все будет работать нормально; аргументы захватываются замыканием не иначе, как локальные переменные без аргументов.
ShadowRanger

15

Мне нравится это грубое, емкое определение :

Функция, которая может относиться к средам, которые больше не активны.

Я бы добавил

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

Декораторы, которые принимают параметры, обычно используются для замыканий. Замыкания - это обычный механизм реализации для такого рода «фабрики функций». Я часто использую замыкания в паттерне стратегии. когда стратегия изменяется данными во время выполнения.

В языке, который позволяет определять анонимные блоки - например, Ruby, C # - замыкания могут использоваться для реализации (до какой степени) новых структур управления. Отсутствие анонимных блоков является одним из ограничений закрытий в Python .


15

Честно говоря, я прекрасно понимаю замыкания, за исключением того, что я никогда не понимал, что именно является «закрытием» и что в нем такого «замыкания». Я рекомендую вам отказаться от поиска какой-либо логики в выборе термина.

Во всяком случае, вот мое объяснение:

def foo():
   x = 3
   def bar():
      print x
   x = 5
   return bar

bar = foo()
bar()   # print 5

Ключевой идеей здесь является то, что объект функции, возвращаемый из foo, сохраняет привязку к локальной переменной 'x', даже если 'x' вышел за пределы области видимости и должен быть более не функционирующим. Этот хук относится к самой переменной var, а не только к ее значению в то время, поэтому, когда вызывается bar, он печатает 5, а не 3.

Также имейте в виду, что Python 2.x имеет ограниченное закрытие: я не могу изменить 'x' внутри 'bar', потому что запись 'x = bla' объявляет локальный 'x' в bar, а не назначает 'x' из foo . Это побочный эффект объявления Python assignment =. Чтобы обойти это, Python 3.0 вводит нелокальное ключевое слово:

def foo():
   x = 3
   def bar():
      print x
   def ack():
      nonlocal x
      x = 7
   x = 5
   return (bar, ack)

bar, ack = foo()
ack()   # modify x of the call to foo
bar()   # print 7

7

Я никогда не слышал, чтобы транзакции использовались в том же контексте, что и объяснение того, что такое закрытие, и здесь действительно нет никакой семантики транзакции.

Это называется закрытием, потому что оно «закрывает» внешнюю переменную (константу), то есть это не просто функция, а оболочка среды, в которой функция была создана.

В следующем примере вызов закрытия g после изменения x также изменит значение x внутри g, поскольку g закрывается по x:

x = 0

def f():
    def g(): 
        return x * 2
    return g


closure = f()
print(closure()) # 0
x = 2
print(closure()) # 4

Кроме того, в его нынешнем виде g()вычисляет, x * 2но ничего не возвращает. Так и должно быть return x * 2. +1 все же за объяснение слова «закрытие».
Бруно Ле Флок

3

Вот типичный вариант использования замыканий - обратные вызовы для элементов графического интерфейса (это было бы альтернативой подклассу класса кнопки). Например, вы можете создать функцию, которая будет вызываться в ответ на нажатие кнопки и «закрывать» соответствующие переменные в родительской области, которые необходимы для обработки щелчка. Таким образом, вы можете связать довольно сложные интерфейсы с одной и той же функцией инициализации, встроив все зависимости в замыкание.


2

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

Фактически, модель данных объясняет это в своем описании __closure__атрибута функций :

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

Чтобы продемонстрировать это:

def enclosure(foo):
    def closure(bar):
        print(foo, bar)
    return closure

closure_instance = enclosure('foo')

Очевидно, мы знаем, что теперь у нас есть функция, на которую указывает имя переменной closure_instance. Якобы, если мы вызываем его с помощью объекта, barон должен напечатать строку, 'foo'и что бы там ни было строковое представление bar.

На самом деле, строка «Foo» будет связан с экземпляром функции, и мы можем сразу прочитать его здесь, путем доступа к cell_contentsатрибуту (только и) первой ячейки в кортеже __closure__атрибута:

>>> closure_instance.__closure__[0].cell_contents
'foo'

Кроме того, объекты ячеек описаны в документации C API:

Объекты "Ячейка" используются для реализации переменных, на которые ссылаются несколько областей видимости.

И мы можем продемонстрировать использование нашего закрытия, отметив, что 'foo'оно застряло в функции и не меняется:

>>> closure_instance('bar')
foo bar
>>> closure_instance('baz')
foo baz
>>> closure_instance('quux')
foo quux

И ничего не может изменить:

>>> closure_instance.__closure__ = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: readonly attribute

Частичные функции

В данном примере замыкание используется как частичная функция, но если это наша единственная цель, ту же цель можно достичь с помощью functools.partial

>>> from __future__ import print_function # use this if you're in Python 2.
>>> partial_function = functools.partial(print, 'foo')
>>> partial_function('bar')
foo bar
>>> partial_function('baz')
foo baz
>>> partial_function('quux')
foo quux

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


2
# A Closure is a function object that remembers values in enclosing scopes even if they are not present in memory.

# Defining a closure

# This is an outer function.
def outer_function(message):
    # This is an inner nested function.
    def inner_function():
        print(message)
    return inner_function

# Now lets call the outer function and return value bound to name 'temp'
temp = outer_function("Hello")
# On calling temp, 'message' will be still be remembered although we had finished executing outer_function()
temp()
# Technique by which some data('message') that remembers values in enclosing scopes 
# even if they are not present in memory is called closures

# Output: Hello

Критерии закрытия закрытий:

  1. У нас должна быть вложенная функция.
  2. Вложенная функция должна ссылаться на значение, определенное во включающей функции.
  3. Функция включения должна возвращать вложенную функцию.

# Example 2
def make_multiplier_of(n): # Outer function
    def multiplier(x): # Inner nested function
        return x * n
    return multiplier
# Multiplier of 3
times3 = make_multiplier_of(3)
# Multiplier of 5
times5 = make_multiplier_of(5)
print(times5(3)) # 15
print(times3(2)) #  6

1

Вот пример закрытия Python3

def closure(x):
    def counter():
        nonlocal x
        x += 1
        return x
    return counter;

counter1 = closure(100);
counter2 = closure(200);

print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 2 " + str(counter2()))
print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 2 " + str(counter2()))

# result

i from closure 1 101
i from closure 1 102
i from closure 2 201
i from closure 1 103
i from closure 1 104
i from closure 1 105
i from closure 2 202

0

Для меня «замыкания» - это функции, которые способны запоминать среду, в которой они были созданы. Эта функциональность позволяет вам использовать переменные или методы внутри замыкания, которые иначе вы не сможете использовать, потому что они больше не существуют или недоступны из-за области действия. Посмотрим на этот код на рубине:

def makefunction (x)
  def multiply (a,b)
    puts a*b
  end
  return lambda {|n| multiply(n,x)} # => returning a closure
end

func = makefunction(2) # => we capture the closure
func.call(6)    # => Result equal "12"  

он работает даже тогда, когда и метод «умножения», и переменная «x» больше не существуют. Все из-за возможности запоминания закрытия.


0

все мы использовали декораторы в Python. Это хорошие примеры, показывающие, что такое закрывающие функции в Python.

class Test():
    def decorator(func):
        def wrapper(*args):
            b = args[1] + 5
            return func(b)
        return wrapper

@decorator
def foo(val):
    print val + 2

obj = Test()
obj.foo(5)

здесь окончательное значение 12

Здесь функция-оболочка может получить доступ к объекту func, поскольку оболочка является «лексическим замыканием», она может получить доступ к его родительским атрибутам. Вот почему он может получить доступ к объекту func.


0

Я хотел бы поделиться своим примером и пояснением о закрытии. Я сделал пример Python и два рисунка для демонстрации состояний стека.

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n’ * margin_top, a * n, 
            ' ‘ * padding, msg, ' ‘ * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)
…
f('hello')
g(‘good bye!')

Результат этого кода будет следующим:

*****      hello      #####

      good bye!    ♥♥♥

Вот два рисунка, показывающие стеки и замыкание, прикрепленное к объекту функции.

когда функция возвращается от производителя

когда функция вызывается позже

Когда функция вызывается через параметр или нелокальную переменную, коду требуются привязки локальных переменных, такие как margin_top, padding, а также a, b, n. Чтобы код функции работал, должен быть доступен стековый фрейм функции-создателя, которая давно ушла, резервная копия которой содержится в замыкании, которое мы можем найти вместе с объектом функции сообщения.


-2

Лучшее объяснение замыкания, которое я когда-либо видел, - это объяснение механизма. Это было примерно так:

Представьте свой программный стек как вырожденное дерево, в котором каждый узел имеет только одного дочернего элемента, а единственный листовой узел является контекстом вашей выполняемой в настоящее время процедуры.

Теперь ослабьте ограничение, согласно которому каждый узел может иметь только одного дочернего элемента.

Если вы сделаете это, у вас может быть конструкция ('yield'), которая может возвращаться из процедуры, не отбрасывая локальный контекст (т.е. она не выталкивает его из стека при возврате). В следующий раз, когда процедура вызывается, вызов берет старый стек (дерево) фрейм и продолжает выполнение с того места, где он остановился.


Это НЕ объяснение закрытий.
Жюль

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