Чи можете ви пояснити закриття (як це стосується Python)?


83

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

Відповіді:


95

Закриття на закриття

Об'єкти - це дані із доданими методами, закриття - це функції із доданими даними.

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, а не лише значення, яке var мав на той час, тому при виклику bar він друкує 5, а не 3.

Також має бути зрозуміло, що Python 2.x має обмежене закриття: я ніяк не можу змінити 'x' всередині 'bar', оскільки написання 'x = bla' оголосило б локальний 'x' у bar, а не присвоїв 'x' foo . Це побічний ефект призначення Python = оголошення. Щоб обійти це, 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, тим не менше, для пояснення слова "закриття".
Bruno Le Floch

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

Для мене "закриття" - це функції, здатні запам'ятати середовище, яке вони створили. Ця функціональність дозволяє використовувати змінні або методи всередині закриття, яке, в іншому випадку, ви не зможете використовувати, оскільки вони вже не існують, або недоступні через область дії. Давайте подивимось на цей код в ruby:

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"  

це працює навіть тоді, коли обидва, метод «множення» та змінна «х», вже не існують. Все тому, що можливість закриття запам'ятати.


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. Для того, щоб забезпечити роботу функціонального коду, слід мати доступ до фрейму стека давно зниклої функції мейкера, що резервно копіюється в закритті, яке ми можемо знайти разом із об'єктом функції 'message'.


-2

Найкраще пояснення, яке я коли-небудь бачив про закриття, було пояснення механізму. Це пройшло приблизно так:

Уявіть, що ваш стек програм є виродженим деревом, де кожен вузол має лише одну дочірню організацію, а один листовий вузол є контекстом поточної процедури, що виконується.

Тепер розслабте обмеження, що кожен вузол може мати лише одну дитину.

Якщо ви зробите це, ви можете створити конструкцію ('yield'), яка може повернутися з процедури, не відкидаючи локальний контекст (тобто вона не вискакує з стека, коли ви повертаєтеся). Під час наступного виклику процедури виклик піднімає старий стек (дерево) і продовжує виконувати те місце, де він зупинився.


Це НЕ пояснення закриття.
Жуль

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