Czy możesz wyjaśnić zamknięcia (w odniesieniu do Pythona)?

84

Dużo czytałem o zamknięciach i myślę, że je rozumiem, ale bez zaciemniania obrazu sobie i innym mam nadzieję, że ktoś może wyjaśnić zamknięcia tak zwięźle i jasno, jak to tylko możliwe. Szukam prostego wyjaśnienia, które pomoże mi zrozumieć, gdzie i dlaczego chciałbym ich używać.

znany obywatel
źródło

Odpowiedzi:

96

Zamknięcie na zamknięciach

Obiekty to dane z dołączonymi metodami, domknięcia to funkcje z dołączonymi danymi.

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
jfs
źródło
6
Zauważ, że nonlocalzostał dodany w pythonie 3, python 2.x nie miał zamkniętych zamknięć do odczytu i zapisu (tj. Można było czytać zamknięte zmienne, ale nie zmieniać ich wartości)
James Porter
6
@JamesPorter: uwaga: możesz emulować nonlocalsłowo kluczowe w Pythonie 2 używając zmiennego obiektu, L = [0] \n def counter(): L[0] += 1; return L[0]np. Nie możesz zmienić nazwy (powiązać ją z innym obiektem) w tym przypadku, ale możesz zmienić sam zmienny obiekt, do którego odnosi się nazwa do. Lista jest wymagana, ponieważ liczby całkowite są niezmienne w Pythonie.
jfs
1
@JFSebastian: racja. to zawsze wygląda jak brudny, brudny hack :)
James Porter
46

To proste: funkcja, która odwołuje się do zmiennych z zakresu zawierającego, potencjalnie po tym, jak przepływ kontroli opuści ten zakres. Ten ostatni fragment jest bardzo przydatny:

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

Zauważ, że 12 i 4 „zniknęły” odpowiednio wewnątrz f i g, ta cecha sprawia, że ​​f i g są właściwymi zamknięciami.

Anders Eurenius
źródło
Nie ma takiej potrzeby constant = x; możesz po prostu zrobić to return y + xw funkcji zagnieżdżonej (lub odebrać argument o nazwie constant) i będzie działać dobrze; argumenty są przechwytywane przez zamknięcie nie inaczej niż nieargumentowe wartości lokalne.
ShadowRanger
15

Podoba mi się ta szorstka, zwięzła definicja :

Funkcja, która może odnosić się do środowisk, które nie są już aktywne.

Dodałbym

Zamknięcie umożliwia powiązanie zmiennych z funkcją bez przekazywania ich jako parametrów .

Powszechnym zastosowaniem dla zamknięć są dekoratory akceptujące parametry. Zamknięcia są powszechnym mechanizmem implementacji dla tego rodzaju „fabryki funkcji”. Często wybieram domknięcia we wzorcu strategii, gdy strategia jest modyfikowana przez dane w czasie wykonywania.

W języku, który pozwala na anonimowe definiowanie bloków - np. Ruby, C # - domknięcia mogą być używane do implementacji (do jakiego stopnia) nowych, nowych struktur kontrolnych. Brak anonimowych bloków jest jednym z ograniczeń zamknięć w Pythonie .

ESV
źródło
15

Szczerze mówiąc, doskonale rozumiem zamknięcia, z wyjątkiem tego, że nigdy nie byłem pewien, co dokładnie jest tym, co jest „zamknięciem” i co w nim jest „zamknięciem”. Radzę zrezygnować z szukania logiki stojącej za wyborem terminu.

W każdym razie, oto moje wyjaśnienie:

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

bar = foo()
bar()   # print 5

Kluczową ideą jest tutaj to, że obiekt funkcji zwrócony z foo zachowuje punkt zaczepienia do lokalnej zmiennej „x”, mimo że „x” wyszedł poza zakres i powinien być zlikwidowany. Ten punkt zaczepienia jest związany z samą zmienną, a nie tylko wartością, która miała zmienna w tym czasie, więc po wywołaniu bar wypisuje 5, a nie 3.

Wyjaśnij również, że Python 2.x ma ograniczone zamknięcie: nie ma sposobu, abym mógł zmodyfikować „x” wewnątrz „bar”, ponieważ wpisanie „x = bla” zadeklarowałoby lokalny „x” w pasku, a nie przypisany do „x” w foo . Jest to efekt uboczny deklaracji przypisania = w Pythonie. Aby obejść ten problem, Python 3.0 wprowadza nielokalne słowo kluczowe:

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
Jegschemesch
źródło
7

Nigdy nie słyszałem, aby transakcje były używane w tym samym kontekście, co wyjaśnianie, czym jest zamknięcie i tak naprawdę nie ma tu żadnej semantyki transakcji.

Nazywa się to zamknięciem, ponieważ „zamyka” zewnętrzną zmienną (stałą) - tj. Nie jest tylko funkcją, ale obudową środowiska, w którym została utworzona.

W poniższym przykładzie wywołanie zamknięcia g po zmianie x zmieni również wartość x w obrębie g, ponieważ g zamyka się nad x:

x = 0

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


closure = f()
print(closure()) # 0
x = 2
print(closure()) # 4
Mark Cidade
źródło
Ponadto w obecnym stanie g()oblicza, x * 2ale nic nie zwraca. Tak powinno być return x * 2. +1 mimo wszystko za wyjaśnienie słowa „zamknięcie”.
Bruno Le Floch
3

Oto typowy przypadek użycia zamknięć - wywołania zwrotne elementów GUI (byłoby to alternatywa dla podklasy klasy przycisku). Na przykład można skonstruować funkcję, która będzie wywoływana w odpowiedzi na naciśnięcie przycisku i „zamyka” odpowiednie zmienne w zakresie nadrzędnym, które są niezbędne do przetworzenia kliknięcia. W ten sposób możesz połączyć dość skomplikowane interfejsy z tej samej funkcji inicjalizacyjnej, budując wszystkie zależności w zamknięciu.


źródło
2

W Pythonie zamknięcie jest instancją funkcji, do której są niezmiennie przypisane zmienne.

W rzeczywistości model danych wyjaśnia to w swoim opisie __closure__atrybutu funkcji :

Brak lub krotka komórek, które zawierają powiązania dla wolnych zmiennych funkcji. Tylko czytać

Aby to zademonstrować:

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

closure_instance = enclosure('foo')

Oczywiście wiemy, że mamy teraz wskazaną funkcję z nazwy zmiennej closure_instance. Pozornie, jeśli wywołasz go za pomocą obiektu, barpowinien wypisać łańcuch 'foo'i niezależnie od jego reprezentacji bar.

W rzeczywistości ciąg „foo” jest powiązany z wystąpieniem funkcji i możemy go bezpośrednio przeczytać tutaj, uzyskując dostęp do cell_contentsatrybutu pierwszej (i jedynej) komórki w krotce __closure__atrybutu:

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

Na marginesie, obiekty komórek są opisane w dokumentacji C API:

Obiekty „komórki” służą do implementowania zmiennych, do których istnieją odwołania w wielu zakresach

Możemy zademonstrować użycie naszego zamknięcia, zauważając, że 'foo'utknęło w funkcji i się nie zmienia:

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

I nic nie może tego zmienić:

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

Funkcje częściowe

Podany przykład wykorzystuje zamknięcie jako funkcję częściową, ale jeśli jest to nasz jedyny cel, ten sam cel można osiągnąć za pomocą 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

Istnieją również bardziej skomplikowane domknięcia, które nie pasowałyby do przykładu funkcji częściowej, i pokażę je dalej, gdy pozwoli na to czas.

Aaron Hall
źródło
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

Kryteria, które muszą spełnić Zamknięcia to:

  1. Musimy mieć funkcję zagnieżdżoną.
  2. Funkcja zagnieżdżona musi odnosić się do wartości zdefiniowanej w funkcji otaczającej.
  3. Funkcja zamykająca musi zwracać funkcję zagnieżdżoną.

# 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
Dinesh Sonachalam
źródło
1

Oto przykład zamknięć w Pythonie3

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
thiagoh
źródło
0

Dla mnie „domknięcia” to funkcje, które są w stanie zapamiętać środowisko, w którym zostały stworzone. Ta funkcjonalność pozwala na użycie zmiennych lub metod w domknięciu, których w inny sposób nie byłbyś w stanie użyć, ponieważ już nie istnieją lub są poza zasięgiem ze względu na zakres. Spójrzmy na ten kod w Rubim:

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"  

działa nawet wtedy, gdy zarówno metoda „multiply”, jak i zmienna „x” już nie istnieją. Wszystko ze względu na zapięcie do zapamiętania.

Ricardo Avila
źródło
0

wszyscy używaliśmy dekoratorów w Pythonie. Są fajnymi przykładami pokazującymi, czym są funkcje zamykające w Pythonie.

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)

tutaj końcowa wartość to 12

Tutaj funkcja opakowująca ma dostęp do obiektu func, ponieważ opakowanie jest „zamknięciem leksykalnym”, ma dostęp do jego atrybutów nadrzędnych. Dlatego jest w stanie uzyskać dostęp do obiektu funkcyjnego.

Nitish Chauhan
źródło
0

Chciałbym podzielić się moim przykładem i wyjaśnieniem na temat zamknięć. Zrobiłem przykład w Pythonie i dwie figury, aby zademonstrować stany stosu.

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!')

Wynik tego kodu wyglądałby następująco:

*****      hello      #####

      good bye!    ♥♥♥

Oto dwie figury przedstawiające stosy i zamknięcie dołączone do obiektu funkcyjnego.

gdy funkcja jest zwracana przez producenta

kiedy funkcja zostanie wywołana później

Gdy funkcja jest wywoływana za pomocą parametru lub zmiennej nielokalnej, kod wymaga lokalnych powiązań zmiennych, takich jak margin_top, dopełnienie, a także a, b, n. Aby kod funkcji działał, ramka stosu funkcji kreatora, która zniknęła dawno temu, powinna być dostępna, a kopia zapasowa znajduje się w domknięciu, które możemy znaleźć wraz z obiektem funkcji wiadomości.

Eunjung Lee
źródło
-2

Najlepszym wyjaśnieniem zamknięcia, jakie kiedykolwiek widziałem, było wyjaśnienie mechanizmu. To wyglądało mniej więcej tak:

Wyobraź sobie stos programu jako zdegenerowane drzewo, w którym każdy węzeł ma tylko jedno dziecko, a węzeł z jednym liściem jest kontekstem aktualnie wykonywanej procedury.

Teraz zwolnij ograniczenie, że każdy węzeł może mieć tylko jedno dziecko.

Jeśli to zrobisz, możesz mieć konstrukcję ('yield'), która może powrócić z procedury bez odrzucania lokalnego kontekstu (tj. Nie zdejmuje jej ze stosu po powrocie). Następnym razem, gdy procedura jest wywoływana, wywołanie podnosi starą ramkę stosu (drzewa) i kontynuuje wykonywanie od miejsca, w którym zostało przerwane.

ConcernedOfTunbridgeWells
źródło
To NIE jest wyjaśnienie zamknięć.
Jules
Opisujesz kontynuacje, a nie zamknięcia.
Matthew Olenik