Warum werden in Python verschachtelte Funktionen nicht als Closures bezeichnet?


249

Ich habe verschachtelte Funktionen in Python gesehen und verwendet, die der Definition eines Abschlusses entsprechen. Warum heißen sie nested functionsstatt closures?

Sind verschachtelte Funktionen keine Abschlüsse, weil sie nicht von der Außenwelt verwendet werden?

UPDATE: Ich habe über Schließungen gelesen und über dieses Konzept in Bezug auf Python nachgedacht. Ich habe den Artikel gesucht und gefunden, der von jemandem in einem Kommentar unten erwähnt wurde, aber ich konnte die Erklärung in diesem Artikel nicht vollständig verstehen, deshalb stelle ich diese Frage.


8
Interessanterweise haben mich einige Googler vom Dezember 2006 gefunden: effbot.org/zone/closure.htm . Ich bin mir nicht sicher - sind "externe Duplikate" auf SO verpönt?
Hbw

1
Weitere Informationen finden Sie in PEP 227 - Statisch verschachtelte Bereiche .
Ehrliche Abe

Antworten:


394

Ein Abschluss tritt auf, wenn eine Funktion von einem umschließenden Bereich, der ihre Ausführung beendet hat, Zugriff auf eine lokale Variable hat.

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

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

Beim make_printerAufruf wird ein neuer Frame mit dem kompilierten Code für die printerFunktion als Konstante und dem Wert msgals lokal auf den Stapel gelegt . Anschließend wird die Funktion erstellt und zurückgegeben. Da die Funktion printerauf die msgVariable verweist , bleibt sie nach der make_printerRückkehr der Funktion am Leben .

Also, wenn Ihre verschachtelten Funktionen dies nicht tun

  1. Zugriffsvariablen, die für einschließende Bereiche lokal sind,
  2. Tun Sie dies, wenn sie außerhalb dieses Bereichs ausgeführt werden.

dann sind sie keine Verschlüsse.

Hier ist ein Beispiel für eine verschachtelte Funktion, die kein Abschluss ist.

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

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

Hier binden wir den Wert an den Standardwert eines Parameters. Dies tritt auf, wenn die Funktion printererstellt wird und daher nach der Rückgabe kein Verweis auf den Wert von msgexternal to printerbeibehalten werden muss make_printer. msgist printerin diesem Zusammenhang nur eine normale lokale Variable der Funktion .


2
Ihre Antwort ist viel besser als meine, Sie machen einen guten Punkt, aber wenn wir uns an die strengsten Definitionen der funktionalen Programmierung halten, sind Ihre Beispiele überhaupt Funktionen? Es ist eine Weile her und ich kann mich nicht erinnern, ob eine strikte funktionale Programmierung Funktionen zulässt, die keine Werte zurückgeben. Der Punkt ist strittig, wenn Sie den Rückgabewert als Keine betrachten, aber das ist ein ganz anderes Thema.
Mikerobi

6
@mikerobi, ich bin mir nicht sicher, ob wir die funktionale Programmierung berücksichtigen müssen, da Python keine funktionale Sprache ist, obwohl es durchaus als solche verwendet werden kann. Aber nein, die inneren Funktionen sind keine Funktionen in diesem Sinne, da es darum geht, Nebenwirkungen zu erzeugen. Es ist einfach, eine Funktion zu erstellen, die die Punkte genauso gut illustriert
aaronasterling

31
@mikerobi: Ob ein Code-Blob ein Abschluss ist oder nicht, hängt davon ab, ob er über seiner Umgebung geschlossen wird oder nicht, nicht wie Sie ihn nennen. Es kann eine Routine, eine Funktion, eine Prozedur, eine Methode, ein Block, eine Unterroutine sein, was auch immer. In Ruby können Methoden keine Abschlüsse sein, sondern nur Blöcke. In Java können Methoden keine Abschlüsse sein, Klassen jedoch. Das macht sie nicht weniger zu einer Schließung. (Obwohl die Tatsache, dass sie nur einige Variablen schließen und sie nicht ändern können, sie nahezu unbrauchbar macht.) Sie könnten argumentieren, dass eine Methode nur eine geschlossene Prozedur ist self. (In JavaScript / Python ist das fast wahr.)
Jörg W Mittag

3
@ JörgWMittag Bitte definieren Sie "schließt über".
Evgeni Sergeev

4
@EvgeniSergeev "schließt über" dh bezieht sich "auf eine lokale Variable [sagen wir i] aus einem umschließenden Bereich". bezieht sich, dh kann den iWert überprüfen (oder ändern) , selbst wenn / wenn dieser Bereich "seine Ausführung beendet hat", dh die Ausführung eines Programms auf andere Teile des Codes übergegangen ist. Der Block, in dem idefiniert ist, ist nicht mehr vorhanden, Funktionen, auf die inoch Bezug genommen wird, können dies jedoch tun. Dies wird allgemein als "Schließen über der Variablen i" beschrieben. Um die spezifischen Variablen nicht zu behandeln, kann sie als Abschluss über den gesamten Umgebungsrahmen implementiert werden, in dem diese Variable definiert ist.
Will Ness

103

Die Frage wurde bereits von aaronasterling beantwortet

Es könnte jedoch jemand interessiert sein, wie die Variablen unter der Haube gespeichert werden.

Bevor Sie zum Snippet kommen:

Closures sind Funktionen, die Variablen von ihrer umgebenden Umgebung erben. Wenn Sie einen Funktionsrückruf als Argument an eine andere Funktion übergeben, die E / A ausführt, wird diese Rückruffunktion später aufgerufen, und diese Funktion merkt sich - fast auf magische Weise - den Kontext, in dem sie deklariert wurde, sowie alle verfügbaren Variablen in diesem Zusammenhang.

  • Wenn eine Funktion keine freien Variablen verwendet, bildet sie keinen Abschluss.

  • Wenn es eine andere innere Ebene gibt, die freie Variablen verwendet , speichern alle vorherigen Ebenen die lexikalische Umgebung (Beispiel am Ende).

  • Funktionsattribute func_closurein Python <3.X oder __closure__in Python> 3.X speichern die freien Variablen.

  • Jede Funktion in Python verfügt über diese Abschlussattribute, speichert jedoch keinen Inhalt, wenn keine freien Variablen vorhanden sind.

Beispiel: von Abschlussattributen, aber ohne Inhalt, da keine freie Variable vorhanden ist.

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

NB: KOSTENLOSE VARIABLE MÜSSEN EINEN SCHLUSS SCHAFFEN.

Ich werde das gleiche Snippet wie oben erklären:

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

Und alle Python-Funktionen haben ein Closure-Attribut. Untersuchen wir also die einschließenden Variablen, die einer Closure-Funktion zugeordnet sind.

Hier ist das Attribut func_closurefür die Funktionprinter

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

Das closureAttribut gibt ein Tupel von Zellenobjekten zurück, die Details der im umschließenden Bereich definierten Variablen enthalten.

Das erste Element in func_closure, das None oder ein Tupel von Zellen sein kann, die Bindungen für die freien Variablen der Funktion enthalten, ist schreibgeschützt.

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

Hier in der obigen Ausgabe sehen Sie cell_contents, was darin gespeichert ist:

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

Wenn wir die Funktion printer()aufrufen, greift sie auf den Wert zu, der in der Funktion gespeichert ist cell_contents. So haben wir die Ausgabe als 'Foo!'

Wieder werde ich die Verwendung des obigen Snippets mit einigen Änderungen erklären:

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

Im obigen Snippet drucke ich keine Nachricht innerhalb der Druckerfunktion, sodass keine freie Variable erstellt wird. Da es keine freie Variable gibt, enthält der Abschluss keinen Inhalt. Genau das sehen wir oben.

Jetzt werde ich ein anderes Snippet erklären, um alles zu klären Free Variablemit 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')

Wir sehen also, dass eine func_closureEigenschaft ein Tupel von Abschlusszellen ist . Wir können sie und ihren Inhalt explizit referenzieren - eine Zelle hat die Eigenschaft "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
>>>

Wenn wir hier aufrufen inn, werden alle speicherfreien Variablen referenziert, damit wir sie erhaltenI am free variable

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

9
In Python 3 func_closurewird jetzt __closure__ähnlich wie bei den verschiedenen anderen func_*Attributen aufgerufen .
lvc

3
Auch __closure_ist in Python 2.6+ für die Kompatibilität mit Python 3.
Pierre

Closure bezieht sich auf den Datensatz, in dem die geschlossenen Variablen gespeichert sind, die an das Funktionsobjekt angehängt sind. Es ist nicht die Funktion selbst. In Python ist es das __closure__Objekt, das geschlossen wird.
Martijn Pieters

Vielen Dank an @MartijnPieters für Ihre Klarstellung.
James Sapam

71

Python hat eine schwache Unterstützung für das Schließen. Um zu sehen, was ich meine, nehmen Sie das folgende Beispiel eines Zählers, der mit JavaScript geschlossen wird:

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

Das Schließen ist ziemlich elegant, da es Funktionen, die so geschrieben sind, die Möglichkeit gibt, "internen Speicher" zu haben. Ab Python 2.7 ist dies nicht möglich. Wenn du es versuchst

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

count = initCounter();

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

Sie erhalten eine Fehlermeldung, dass x nicht definiert ist. Aber wie kann das sein, wenn andere gezeigt haben, dass Sie es drucken können? Dies liegt daran, wie Python den Funktionsvariablenbereich verwaltet. Während die innere Funktion die Variablen der äußeren Funktion lesen kann, kann sie sie nicht schreiben .

Das ist wirklich eine Schande. Mit nur schreibgeschütztem Abschluss können Sie jedoch zumindest das Funktionsdekorationsmuster implementieren , für das Python syntaktischen Zucker anbietet.

Aktualisieren

Wie bereits erwähnt, gibt es Möglichkeiten, mit den Einschränkungen des Python-Bereichs umzugehen, und ich werde einige davon offenlegen.

1. Verwenden Sie das globalSchlüsselwort (im Allgemeinen nicht empfohlen).

2. Verwenden Sie in Python 3.x das nonlocalSchlüsselwort (vorgeschlagen von @unutbu und @leewz).

3. Definieren Sie eine einfache modifizierbare KlasseObject

class Object(object):
    pass

und erstellen Sie ein Object scopeInneres initCounter, um die Variablen zu speichern

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

    return counter

Da scopees sich eigentlich nur um eine Referenz handelt, ändern scopesich die mit den Feldern ausgeführten Aktionen nicht wirklich selbst, sodass kein Fehler auftritt.

4. Eine alternative Möglichkeit wäre, wie @unutbu hervorhob, jede Variable als Array ( x = [0]) zu definieren und ihr erstes Element ( x[0] += 1) zu ändern . Auch hier tritt kein Fehler auf, da xselbst nicht geändert wird.

5. Wie von @raxacoricofallapatorius vorgeschlagen, können Sie xeine Eigenschaft von erstellencounter

def initCounter ():

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

    counter.x = 0
    return counter

27
Es gibt Möglichkeiten, dies zu umgehen. In Python2 können Sie x = [0]im äußeren Bereich erstellen und x[0] += 1im inneren Bereich verwenden. In Python3 können Sie Ihren Code unverändert lassen und das nichtlokale Schlüsselwort verwenden .
Unutbu

"Während die innere Funktion die Variablen der äußeren Funktion lesen kann, kann sie sie nicht schreiben." - Dies ist gemäß dem Kommentar von unutbu ungenau. Das Problem ist, dass wenn Python auf so etwas wie x = ... stößt, x als lokale Variable interpretiert wird, die zu diesem Zeitpunkt natürlich noch nicht definiert ist. OTOH, wenn x ein veränderbares Objekt mit einer veränderlichen Methode ist, kann es problemlos geändert werden. Wenn x beispielsweise ein Objekt ist, das die Methode inc () unterstützt, die sich selbst mutiert, funktioniert x.inc () problemlos.
Thanh DK

@ThanhDK Bedeutet das nicht, dass Sie nicht in die Variable schreiben können? Wenn Sie eine Methode von einem veränderlichen Objekt aufrufen, weisen Sie es lediglich an, sich selbst zu ändern. Sie ändern die Variable nicht tatsächlich (die lediglich einen Verweis auf das Objekt enthält). Mit anderen Worten, die Referenz, auf die die Variable xzeigt, bleibt genau gleich, selbst wenn Sie aufrufen inc()oder was auch immer, und Sie haben nicht effektiv in die Variable geschrieben.
user193130

4
Es gibt eine andere Option, streng besser als # 2, IMV, der Herstellung xeine Eigenschaftcounter .
Orome

9
Python 3 hat das nonlocalSchlüsselwort, das globalnur für die Variablen einer äußeren Funktion gilt. Dadurch kann eine innere Funktion einen Namen von ihren äußeren Funktionen neu binden. Ich denke, "an den Namen binden" ist genauer als "die Variable ändern".
Leez

16

Python 2 hatte keine Abschlüsse - es gab Problemumgehungen, die Abschlüssen ähnelten .

Es gibt viele Beispiele für bereits gegebene Antworten - Kopieren von Variablen in die innere Funktion, Ändern eines Objekts in der inneren Funktion usw.

In Python 3 ist die Unterstützung expliziter - und prägnant:

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

Verwendung:

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

Das nonlocalSchlüsselwort bindet die innere Funktion an die explizit erwähnte äußere Variable und schließt sie tatsächlich ein. Daher expliziter eine "Schließung".


1
Interessant als Referenz: docs.python.org/3/reference/… . Ich weiß nicht, warum es nicht einfach ist, in der Python3-Dokumentation weitere Informationen zu Schließungen zu finden (und wie Sie erwarten könnten, dass sie sich von JS verhalten).
user3773048

9

Ich hatte eine Situation, in der ich einen separaten, aber dauerhaften Namensraum brauchte. Ich habe Klassen benutzt. Ich nicht anders. Getrennte, aber dauerhafte Namen sind Schließungen.

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

Gibt:

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

Dies ist ein Beispiel dafür, was ein Verschluss ist und wie er verwendet werden kann.


0

Ich möchte einen weiteren einfachen Vergleich zwischen Python und JS-Beispiel anbieten, wenn dies dazu beiträgt, die Dinge klarer zu machen.

JS:

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

und Ausführen:

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

und Ausführen:

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

Grund: Wie viele andere oben bereits erwähnt, wird in Python eine neue Referenz im inneren Bereich erstellt, wenn im inneren Bereich eine Zuordnung zu einer Variablen mit demselben Namen erfolgt. Nicht so bei JS, es sei denn, Sie deklarieren ausdrücklich eins mit dem varSchlüsselwort.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.