Dlaczego funkcje zagnieżdżone w Pythonie nie są nazywane zamknięciami?


249

Widziałem i użyłem funkcji zagnieżdżonych w Pythonie i pasują one do definicji zamknięcia. Dlaczego więc są nazywane nested functionszamiast closures?

Czy funkcje zagnieżdżone nie są zamknięciami, ponieważ nie są używane przez świat zewnętrzny?

AKTUALIZACJA: Czytałem o zamknięciach i przyszło mi do głowy myśleć o tej koncepcji w odniesieniu do Pythona. Przeszukałem i znalazłem artykuł wymieniony przez kogoś w komentarzu poniżej, ale nie mogłem całkowicie zrozumieć wyjaśnienia w tym artykule, dlatego zadaję to pytanie.


8
Co ciekawe, niektórzy google znaleźli mnie, datowany na grudzień 2006: effbot.org/zone/closure.htm . Nie jestem pewien - czy „SO duplikaty zewnętrzne” są źle postrzegane w SO?
hbw

1
PEP 227 - Zakresy zagnieżdżone statycznie, aby uzyskać więcej informacji.
Uczciwy Abe

Odpowiedzi:


393

Zamknięcie występuje, gdy funkcja ma dostęp do zmiennej lokalnej z zakresu obejmującego, który zakończył wykonywanie.

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

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

Po make_printerwywołaniu na stos jest umieszczana nowa ramka ze skompilowanym kodem printerfunkcji jako stałej i wartością msgjako lokalną. Następnie tworzy i zwraca funkcję. Ponieważ funkcja printerodwołuje się do msgzmiennej, jest utrzymywana przy życiu po jej make_printerzwróceniu.

Więc jeśli twoje funkcje zagnieżdżone nie

  1. dostęp do zmiennych, które są lokalne dla obejmujących zakresy,
  2. rób to, gdy są wykonywane poza tym zakresem,

to nie są zamknięcia.

Oto przykład zagnieżdżonej funkcji, która nie jest zakończeniem.

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

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

Tutaj wiążymy wartość z wartością domyślną parametru. Dzieje się tak, gdy funkcja printerjest tworzona, więc po powrocie nie trzeba utrzymywać odniesienia do wartości msgzewnętrznego do jest po prostu normalną zmienną lokalną funkcji w tym kontekście.printermake_printermsgprinter


2
Twoja odpowiedź jest znacznie lepsza niż moja, masz rację, ale jeśli mamy zamiar zastosować najściślejsze definicje programowania funkcjonalnego, czy twoje przykłady są nawet funkcjami? Minęło trochę czasu i nie pamiętam, czy ścisłe programowanie funkcjonalne dopuszcza funkcje, które nie zwracają wartości. Chodzi o kwestię sporną, jeśli uważasz, że zwracana wartość to None, ale jest to zupełnie inny temat.
mikerobi

6
@ Mikeerobi, nie jestem pewien, czy musimy wziąć pod uwagę programowanie funkcjonalne, ponieważ Python nie jest tak naprawdę językiem funkcjonalnym, chociaż z pewnością można go używać jako takiego. Ale nie, funkcje wewnętrzne nie są funkcjami w tym sensie, ponieważ ich celem jest tworzenie efektów ubocznych. Łatwo jest jednak stworzyć funkcję, która równie dobrze ilustruje punkty,
aaronasterling

31
@mikerobi: To, czy plama kodu jest zamknięciem, zależy od tego, czy zamyka się w swoim środowisku, a nie jak to nazywasz. Może to być procedura, funkcja, procedura, metoda, blok, podprogram, cokolwiek. W Rubim metody nie mogą być zamknięciami, tylko bloki mogą. W Javie metody nie mogą być zamknięciami, ale klasy mogą. To nie czyni ich mniejszym zamknięciem. (Chociaż fakt, że zamykają tylko niektóre zmienne i nie mogą ich modyfikować, czyni je prawie bezużytecznymi.) Można argumentować, że metoda jest tylko zamkniętą procedurą self. (W JavaScript / Python to prawie prawda.)
Jörg W Mittag

3
@ JörgWMittag Zdefiniuj „zamyka się”.
Jewgienij Siergiejew

4
@EvgeniSergeev „zamyka się”, tzn. Odnosi się do „zmiennej lokalnej [powiedzmy i] z zakresu obejmującego”. odnosi się, tj. może sprawdzać (lub zmieniać) iwartość, nawet jeśli / kiedy zakres ten „zakończył wykonywanie”, tj. wykonanie programu przeszło do innych części kodu. Blok, w którym ijest zdefiniowany, już nie istnieje, ale funkcje odnoszące się do niego inadal mogą to zrobić. Jest to powszechnie opisywane jako „zamykanie zmiennej i”. Aby nie zajmować się określonymi zmiennymi, można je zaimplementować jako zamykanie całej ramki środowiska, w której ta zmienna jest zdefiniowana.
Czy Ness

103

Odpowiedź na to pytanie została już udzielona przez Aaronasterling

Jednak ktoś może być zainteresowany tym, jak zmienne są przechowywane pod maską.

Zanim przejdziesz do fragmentu:

Zamknięcia to funkcje, które dziedziczą zmienne z otaczającego środowiska. Gdy przekażesz funkcję zwrotną jako argument do innej funkcji, która wykona operacje we / wy, ta funkcja zwrotna zostanie wywołana później, a ta funkcja - niemal magicznie - zapamięta kontekst, w którym została zadeklarowana, wraz ze wszystkimi dostępnymi zmiennymi w tym kontekście.

  • Jeśli funkcja nie używa wolnych zmiennych, nie tworzy zamknięcia.

  • Jeśli istnieje inny poziom wewnętrzny, który używa wolnych zmiennych - wszystkie poprzednie poziomy zapisują środowisko leksykalne (przykład na końcu)

  • atrybuty funkcji func_closurew pythonie <3.X lub __closure__w pythonie> 3.X zapisz wolne zmienne.

  • Każda funkcja w pythonie ma takie atrybuty zamknięcia, ale nie zapisuje żadnej zawartości, jeśli nie ma wolnych zmiennych.

przykład: atrybutów zamknięcia, ale brak zawartości w środku, ponieważ nie ma wolnej zmiennej.

>>> def foo():
...     def fii():
...         pass
...     return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>

NB: DARMOWA ZMIENNA MUSI BYĆ TWORZENIE ZAMKNIĘCIA.

Wyjaśnię, używając tego samego fragmentu kodu jak powyżej:

>>> def make_printer(msg):
...     def printer():
...         print msg
...     return printer
...
>>> printer = make_printer('Foo!')
>>> printer()  #Output: Foo!

Wszystkie funkcje Pythona mają atrybut zamknięcia, więc zbadajmy otaczające zmienne powiązane z funkcją zamknięcia.

Oto atrybut func_closurefunkcjiprinter

>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>

closureAtrybut zwraca parę obiektów komórkowych, które zawierają dane o zmiennych zdefiniowanych w zakresie okalającego.

Pierwszy element w func_closure, którym może być None lub krotka komórek zawierających powiązania dla wolnych zmiennych funkcji i jest tylko do odczytu.

>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
 '__setattr__',  '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>

Tutaj w powyższym wyjściu możesz zobaczyć cell_contents, zobaczmy, co przechowuje:

>>> printer.func_closure[0].cell_contents
'Foo!'    
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>

Tak więc, kiedy wywołaliśmy funkcję printer(), uzyskuje dostęp do wartości przechowywanej wewnątrz cell_contents. W ten sposób otrzymaliśmy wynik jako „Foo!”

Ponownie wyjaśnię użycie powyższego fragmentu z pewnymi zmianami:

 >>> def make_printer(msg):
 ...     def printer():
 ...         pass
 ...     return printer
 ...
 >>> printer = make_printer('Foo!')
 >>> printer.func_closure
 >>>

W powyższym fragmencie nie wypisuję msg wewnątrz funkcji drukarki, więc nie tworzy żadnej wolnej zmiennej. Ponieważ nie ma wolnej zmiennej, wewnątrz zamknięcia nie będzie zawartości. Dokładnie to widzimy powyżej.

Teraz wyjaśnię inny fragment kodu, aby wyczyścić wszystko za Free Variablepomocą 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')

Widzimy więc, że func_closurewłaściwość jest krotką komórek zamykających , możemy odesłać je i ich zawartość jawnie - komórka ma właściwość „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
>>>

Tutaj, kiedy zadzwoniliśmy inn, odniesie wszystkie zmienne wolne od zapisu, więc otrzymamyI am free variable

>>> inn('variable')
'I am free variable'
>>>

9
W Pythonie 3 func_closurejest teraz wywoływany __closure__, podobnie jak różne inne func_*atrybuty.
lvc

3
__closure_Jest również dostępny w Python 2.6+ dla kompatybilności z Python 3.
Pierre

Zamknięcie odnosi się do rekordu, w którym przechowywane są zmienne zamknięte, dołączone do obiektu funkcji. To nie jest sama funkcja. W Pythonie to __closure__obiekt jest zamknięciem.
Martijn Pieters

Dzięki @MartijnPieters za wyjaśnienia.
James Sapam

71

Python ma słabą obsługę zamykania. Aby zobaczyć, co mam na myśli, weźmy następujący przykład licznika używającego zamknięcia z 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

Zamknięcie jest dość eleganckie, ponieważ daje tak napisanym funkcjom możliwość posiadania „pamięci wewnętrznej”. Od wersji Python 2.7 nie jest to możliwe. Jeśli spróbujesz

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

Pojawi się błąd informujący, że x nie jest zdefiniowane. Ale jak to możliwe, jeśli inni pokazali, że można go wydrukować? Wynika to z tego, jak Python zarządza zmiennym zakresem funkcji. Chociaż funkcja wewnętrzna może odczytać zmienne funkcji zewnętrznej, nie może ich zapisać .

To naprawdę szkoda. Ale po zamknięciu tylko do odczytu można przynajmniej zaimplementować wzorzec dekoratora funkcji, dla którego Python oferuje cukier syntaktyczny.

Aktualizacja

Jak już wspomniano, istnieją sposoby radzenia sobie z ograniczeniami zakresu Pythona, a niektóre z nich ujawnię.

1. Użyj globalsłowa kluczowego (generalnie niezalecane).

2. W Pythonie 3.x użyj nonlocalsłowa kluczowego (sugerowanego przez @unutbu i @leewz)

3. Zdefiniuj prostą modyfikowalną klasęObject

class Object(object):
    pass

i utwórz Object scopewewnątrz, initCounteraby przechowywać zmienne

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

Ponieważ scopetak naprawdę jest tylko odniesieniem, działania podejmowane z jego polami tak naprawdę nie modyfikują scopesię, więc nie powstaje błąd.

4. Alternatywnym sposobem, jak wskazał @unutbu, byłoby zdefiniowanie każdej zmiennej jako tablicy ( x = [0]) i zmodyfikowanie jej pierwszego elementu ( x[0] += 1). Ponownie nie powstaje błąd, ponieważ xsam nie jest modyfikowany.

5. Jak sugeruje @raxacoricofallapatorius, możesz zrobić xwłasnośćcounter

def initCounter ():

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

    counter.x = 0
    return counter

27
Są na to sposoby. W Python2 można tworzyć x = [0]w zakresie zewnętrznym i używać x[0] += 1w zakresie wewnętrznym. W Python3 możesz zachować swój obecny kod i użyć nielokalnego słowa kluczowego .
unutbu

„Chociaż funkcja wewnętrzna może odczytywać zmienne funkcji zewnętrznej, nie może ich zapisać”. - Jest to niedokładne jak na komentarz unutbu. Problem polega na tym, że gdy Python napotyka coś takiego jak x = ..., x jest interpretowany jako zmienna lokalna, która oczywiście nie jest jeszcze zdefiniowana w tym momencie. OTOH, jeśli x jest obiektem podlegającym mutacji metodą mutable, można go dokładnie zmodyfikować, np. Jeśli x jest obiektem obsługującym metodę inc (), która się mutuje, x.inc () będzie działał bez problemów.
Thanh DK

@ThanhDK Czy to nie znaczy, że nie możesz pisać do zmiennej? Kiedy używasz wywołania metody z obiektu zmiennego, po prostu mówisz jej, żeby się zmodyfikowała, tak naprawdę nie modyfikujesz zmiennej (która jedynie zawiera odniesienie do obiektu). Innymi słowy, odniesienie, na które xwskazuje zmienna, pozostaje dokładnie takie samo, nawet jeśli wywołujesz inc()lub cokolwiek, i nie zapisałeś skutecznie do zmiennej.
user193130 15.01.2015

4
Jest jeszcze jedna opcja, zdecydowanie lepsza niż # 2, imv, tworzenia xwłasnościcounter .
orome

9
Python 3 ma nonlocalsłowo kluczowe, które przypomina globalzmienne funkcji zewnętrznej. Umożliwi to funkcji wewnętrznej ponowne powiązanie nazwy z jej zewnętrznymi funkcjami. Myślę, że „powiązanie z nazwą” jest dokładniejsze niż „modyfikacja zmiennej”.
leewz

16

Python 2 nie miał zamknięć - miał obejścia, które przypominały zamknięcia.

W podanych już odpowiedziach jest wiele przykładów - kopiowanie zmiennych do funkcji wewnętrznej, modyfikowanie obiektu w funkcji wewnętrznej itp.

W Pythonie 3 obsługa jest bardziej wyraźna i zwięzła:

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

Stosowanie:

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

nonlocalKluczowe wiąże funkcji wewnętrznej do zewnętrznej zmiennej wyraźnie wspomniano, w efekcie zamykając go. Stąd bardziej precyzyjnie „zamknięcie”.


1
Ciekawe, dla odniesienia: docs.python.org/3/reference/… . Nie wiem, dlaczego nie jest łatwo znaleźć więcej informacji o zamknięciach (i jak można się spodziewać, że będą się zachowywać, pochodzące z JS) w dokumentacji python3?
user3773048,

9

Miałem sytuację, w której potrzebowałem osobnej, ale trwałej przestrzeni nazw. Korzystałem z zajęć. Ja inaczej nie. Segregowane, ale trwałe nazwy są zamknięciami.

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

Daje:

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

To jest przykład tego, czym jest zamknięcie i jak można go użyć.


0

Chciałbym zaoferować inne proste porównanie między pythonem a przykładem JS, jeśli to pomoże wyjaśnić sprawę.

JS:

function make () {
  var cl = 1;
  function gett () {
    console.log(cl);
  }
  function sett (val) {
    cl = val;
  }
  return [gett, sett]
}

i wykonywanie:

a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3

Pyton:

def make (): 
  cl = 1
  def gett ():
    print(cl);
  def sett (val):
    cl = val
  return gett, sett

i wykonywanie:

g, s = make()
g() #1
s(2); g() #1
s(3); g() #1

Powód: Jak wielu innych powiedziało powyżej, w pythonie, jeśli w wewnętrznym zakresie jest przypisane do zmiennej o tej samej nazwie, tworzone jest nowe odniesienie w wewnętrznym zakresie. Nie dotyczy to JS, chyba że jednoznacznie zadeklarujesz go varsłowem kluczowym.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.