Nichtlokales Schlüsselwort in Python 2.x.

117

Ich versuche, einen Abschluss in Python 2.6 zu implementieren, und ich muss auf eine nicht lokale Variable zugreifen, aber es scheint, dass dieses Schlüsselwort in Python 2.x nicht verfügbar ist. Wie sollte man in Abschlüssen in diesen Python-Versionen auf nichtlokale Variablen in Closures zugreifen?

adinsa
quelle

Antworten:

125

Innere Funktionen können nichtlokale Variablen in 2.x lesen , aber nicht erneut binden . Das ist ärgerlich, aber Sie können es umgehen. Erstellen Sie einfach ein Wörterbuch und speichern Sie Ihre Daten als Elemente darin. Innere Funktionen dürfen die Objekte, auf die sich nichtlokale Variablen beziehen, nicht mutieren .

So verwenden Sie das Beispiel aus Wikipedia:

def outer():
    d = {'y' : 0}
    def inner():
        d['y'] += 1
        return d['y']
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3
Chris B.
quelle
4
Warum ist es möglich, den Wert aus dem Wörterbuch zu ändern?
Coelhudo
9
@coelhudo Da Sie können nicht - lokale Variablen ändern. Sie können jedoch keine Zuordnung zu nichtlokalen Variablen vornehmen. Dies löst beispielsweise UnboundLocalError aus : def inner(): print d; d = {'y': 1}. Hier print dliest Outer dund erzeugt so eine nichtlokale Variable dim inneren Bereich.
Suzanshakya
21
Danke für diese Antwort. Ich denke, Sie könnten die Terminologie verbessern: Anstelle von "kann lesen, kann nicht ändern", vielleicht "kann referenzieren, kann nicht zuweisen". Sie können den Inhalt eines Objekts in einem nicht lokalen Bereich ändern, aber Sie können nicht ändern, auf welches Objekt verwiesen wird.
Metamatt
3
Alternativ können Sie anstelle des Wörterbuchs eine beliebige Klasse verwenden und ein Objekt instanziieren. Ich finde es viel eleganter und lesbarer, da es den Code nicht mit Literalen (Wörterbuchschlüsseln) überflutet und Sie Methoden verwenden können.
Alois Mahdal
6
Für Python ist es meiner Meinung nach am besten, das Verb "bind" oder "rebind" zu verwenden und das Substantiv "name" anstelle von "variable" zu verwenden. In Python 2.x können Sie ein Objekt schließen, aber den Namen im ursprünglichen Bereich nicht erneut binden. In einer Sprache wie C reserviert das Deklarieren einer Variablen etwas Speicher (entweder statischen Speicher oder temporär auf dem Stapel). In Python X = 1bindet der Ausdruck den Namen einfach an Xein bestimmtes Objekt (und an intden Wert 1). X = 1; Y = Xbindet zwei Namen an genau dasselbe Objekt. Auf jeden Fall sind einige Objekte veränderbar und Sie können ihre Werte ändern.
Steveha
37

Die folgende Lösung ist von der Antwort von Elias Zamaria inspiriert , behandelt jedoch im Gegensatz zu dieser Antwort mehrere Aufrufe der äußeren Funktion korrekt. Die "Variable" inner.yist lokal für den aktuellen Aufruf von outer. Nur ist es keine Variable, da dies verboten ist, sondern ein Objektattribut (das Objekt ist die Funktion innerselbst). Dies ist sehr hässlich (beachten Sie, dass das Attribut erst erstellt werden kann, nachdem die innerFunktion definiert wurde), scheint jedoch effektiv zu sein.

def outer():
    def inner():
        inner.y += 1
        return inner.y
    inner.y = 0
    return inner

f = outer()
g = outer()
print(f(), f(), g(), f(), g()) #prints (1, 2, 1, 3, 2)
Marc van Leeuwen
quelle
Es muss nicht hässlich sein. Verwenden Sie anstelle von inner.y Outer.y. Sie können Outer.y = 0 vor dem Def Inner definieren.
jgomo3
1
Ok, ich sehe, mein Kommentar ist falsch. Elias Zamaria hat auch die äußere Lösung implementiert. Aber wie Nathaniel [1] kommentiert, sollten Sie aufpassen. Ich denke, diese Antwort sollte als Lösung beworben werden, aber beachten Sie die äußere Lösung und die Kavetten. [1] stackoverflow.com/questions/3190706/…
jgomo3
Dies wird hässlich, wenn Sie möchten, dass mehr als eine innere Funktion den nichtlokalen veränderlichen Zustand teilt. Sagen Sie a inc()und a, die dec()von außen zurückgegeben werden, um einen gemeinsam genutzten Zähler zu erhöhen und zu verringern. Dann müssen Sie entscheiden, an welche Funktion der aktuelle Zählerwert angehängt werden soll, und diese Funktion von den anderen referenzieren. Was etwas seltsam und asymmetrisch aussieht. ZB in dec()einer Zeile wie inc.value -= 1.
BlackJack
33

Anstelle eines Wörterbuchs gibt es weniger Unordnung in einer nichtlokalen Klasse . Beispiel von @ ChrisB ändern :

def outer():
    class context:
        y = 0
    def inner():
        context.y += 1
        return context.y
    return inner

Dann

f = outer()
assert f() == 1
assert f() == 2
assert f() == 3
assert f() == 4

Jeder äußere () Aufruf ruft eine neue und eindeutige Klasse namens Kontext auf (nicht nur eine neue Instanz). So wird vermieden, dass @ Nathaniel sich vor dem gemeinsamen Kontext hütet .

g = outer()
assert g() == 1
assert g() == 2

assert f() == 5
Bob Stein
quelle
Nett! Dies ist in der Tat eleganter und lesbarer als das Wörterbuch.
Vincenzo
Ich würde empfehlen, hier generell Slots zu verwenden. Es schützt Sie vor Tippfehlern.
DerWeh
@DerWeh interessant, davon habe ich noch nie gehört . Könnten Sie das gleiche von jeder Klasse sagen? Ich hasse es, von Tippfehlern durcheinander gebracht zu werden.
Bob Stein
@ BobStein Sorry, ich verstehe nicht wirklich was du meinst. Aber durch das Hinzufügen __slots__ = ()und Erstellen eines Objekts stattdessen die Klasse, zB context.z = 3wäre ein erhöhen AttributeError. Es ist für alle Klassen möglich, es sei denn, sie erben von einer Klasse, die keine Slots definiert.
DerWeh
@DerWeh Ich habe noch nie davon gehört, Slots zum Abfangen von Tippfehlern zu verwenden. Sie haben Recht, es würde nur in Klasseninstanzen helfen, nicht in Klassenvariablen. Vielleicht möchten Sie ein funktionierendes Beispiel für Pyfiddle erstellen. Es würde mindestens zwei zusätzliche Zeilen erfordern. Ich kann mir nicht vorstellen, wie Sie es ohne mehr Unordnung machen würden.
Bob Stein
14

Ich denke, der Schlüssel hier ist, was Sie unter "Zugang" verstehen. Es sollte kein Problem beim Lesen einer Variablen außerhalb des Schließungsbereichs geben, z.

x = 3
def outer():
    def inner():
        print x
    inner()
outer()

sollte wie erwartet funktionieren (Drucken 3). Das Überschreiben des Werts von x funktioniert jedoch nicht, z.

x = 3
def outer():
    def inner():
        x = 5
    inner()
outer()
print x

wird weiterhin gedruckt 3. Nach meinem Verständnis von PEP-3104 ist dies das, was das nichtlokale Schlüsselwort abdecken soll. Wie im PEP erwähnt, können Sie eine Klasse verwenden, um dasselbe zu erreichen (etwas chaotisch):

class Namespace(object): pass
ns = Namespace()
ns.x = 3
def outer():
    def inner():
        ns.x = 5
    inner()
outer()
print ns.x
ig0774
quelle
Anstatt eine Klasse zu erstellen und zu instanziieren, kann man einfach eine Funktion erstellen: def ns(): passgefolgt von ns.x = 3. Es ist nicht schön, aber für mein Auge etwas weniger hässlich.
Davidchambers
2
Gibt es einen besonderen Grund für die Ablehnung? Ich gebe zu, meine ist nicht die eleganteste Lösung, aber sie funktioniert ...
ig0774
2
Was ist mit class Namespace: x = 3?
Feuermurmel
3
Ich denke nicht, dass dieser Weg nicht lokal simuliert, da er eine globale Referenz anstelle einer Referenz in einem Abschluss erstellt.
CarmeloS
1
Ich stimme @CarmeloS zu, Ihr letzter Codeblock, der nicht das tut, was PEP-3104 vorschlägt, um etwas Ähnliches in Python 2 zu erreichen, nsist ein globales Objekt, weshalb Sie ns.xin der printAnweisung ganz am Ende auf Modulebene verweisen können .
Martineau
13

Es gibt eine andere Möglichkeit, nichtlokale Variablen in Python 2 zu implementieren, falls eine der Antworten hier aus irgendeinem Grund unerwünscht ist:

def outer():
    outer.y = 0
    def inner():
        outer.y += 1
        return outer.y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Es ist überflüssig, den Namen der Funktion in der Zuweisungsanweisung der Variablen zu verwenden, aber es sieht für mich einfacher und sauberer aus, als die Variable in ein Wörterbuch aufzunehmen. Der Wert wird von einem Anruf zum anderen gespeichert, genau wie in der Antwort von Chris B.

Elias Zamaria
quelle
19
Bitte beachten Sie: Wenn Sie dies auf diese Weise implementieren f = outer()und später tun g = outer(), wird fder Zähler zurückgesetzt. Dies liegt daran, dass beide eine einzelne outer.y Variable gemeinsam nutzen und nicht jede ihre eigene unabhängige Variable hat. Obwohl dieser Code ästhetisch ansprechender aussieht als die Antwort von Chris B, scheint sein Weg der einzige Weg zu sein, das lexikalische Scoping zu emulieren, wenn Sie outermehr als einmal anrufen möchten .
Nathaniel
@ Nathaniel: Lass mich sehen, ob ich das richtig verstehe. Die Zuweisung an outer.ybeinhaltet nichts Lokales für den Funktionsaufruf (Instanz) outer(), sondern weist ein Attribut des Funktionsobjekts zu, das an den Namen outerin seinem umschließenden Bereich gebunden ist . Und deshalb könnte man genauso gut verwendet hat, schriftlich outer.y, andere Namen statt outer, sofern es bekannt ist , in diesem Umfang gebunden zu sein. Ist das richtig?
Marc van Leeuwen
1
Korrektur, hätte ich nach "in diesem Bereich gebunden" sagen sollen: an ein Objekt, dessen Typ das Setzen von Attributen erlaubt (wie eine Funktion oder eine beliebige Klasseninstanz). Da dieser Bereich tatsächlich weiter entfernt ist als der von uns gewünschte, würde dies nicht die folgende äußerst hässliche Lösung vorschlagen: outer.yVerwenden Sie stattdessen den Namen inner.y(da er innerinnerhalb des Aufrufs gebunden ist outer(), was genau der gewünschte Bereich ist), sondern setzen die Initialisierung inner.y = 0 nach der Definition von inner (da das Objekt existieren muss, wenn sein Attribut erstellt wird), aber natürlich vorher return inner?
Marc van Leeuwen
@MarcvanLeeuwen Sieht so aus, als hätte Ihr Kommentar die Lösung mit inner.y von Elias inspiriert. Gute Gedanken von dir.
Ankur Agarwal
Der Zugriff auf und die Aktualisierung globaler Variablen ist wahrscheinlich nicht das, was die meisten Benutzer versuchen, wenn sie die Captures eines Abschlusses mutieren.
Binki
12

Hier ist etwas, das von einem Vorschlag inspiriert wurde, den Alois Mahdal in einem Kommentar zu einer anderen Antwort gemacht hat :

class Nonlocal(object):
    """ Helper to implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)


def outer():
    nl = Nonlocal(y=0)
    def inner():
        nl.y += 1
        return nl.y
    return inner

f = outer()
print(f(), f(), f()) # -> (1 2 3)

Aktualisieren

Nachdem ich kürzlich darauf zurückgeblickt hatte, war ich beeindruckt, wie dekoratorisch es war - als mir klar wurde, dass die Implementierung als solche es allgemeiner und nützlicher machen würde (obwohl dies die Lesbarkeit in gewissem Maße beeinträchtigt).

# Implemented as a decorator.

class Nonlocal(object):
    """ Decorator class to help implement nonlocal names in Python 2.x """
    def __init__(self, **kwargs):
        self._vars = kwargs

    def __call__(self, func):
        for k, v in self._vars.items():
            setattr(func, k, v)
        return func


@Nonlocal(y=0)
def outer():
    def inner():
        outer.y += 1
        return outer.y
    return inner


f = outer()
print(f(), f(), f()) # -> (1 2 3)

Beachten Sie, dass beide Versionen sowohl in Python 2 als auch in Python 3 funktionieren.

Martineau
quelle
Dieser scheint der beste in Bezug auf Lesbarkeit und Beibehaltung des lexikalischen Umfangs zu sein.
Aaron S. Kurland
3

Die Scoping-Regeln von Python enthalten eine Warze. Durch die Zuweisung wird eine Variable lokal für den unmittelbar umschließenden Funktionsumfang. Für eine globale Variable würden Sie dies mit dem globalSchlüsselwort lösen .

Die Lösung besteht darin, ein Objekt einzuführen, das von den beiden Bereichen gemeinsam genutzt wird und veränderbare Variablen enthält, auf das jedoch selbst über eine nicht zugewiesene Variable verwiesen wird.

def outer(v):
    def inner(container = [v]):
        container[0] += 1
        return container[0]
    return inner

Eine Alternative ist einige Bereiche Hackery:

def outer(v):
    def inner(varname = 'v', scope = locals()):
        scope[varname] += 1
        return scope[varname]
    return inner

Möglicherweise können Sie einige Tricks herausfinden, um den Namen des Parameters zu ermitteln outer, und ihn dann als Variablennamen übergeben, ohne sich jedoch auf den Namen verlassen zu outermüssen, müssen Sie einen Y-Kombinator verwenden.

Marcin
quelle
Beide Versionen funktionieren in diesem speziellen Fall, sind jedoch kein Ersatz für nonlocal. locals()Erstellt ein Wörterbuch von outer()s Einheimischen zu dem Zeitpunkt inner()definiert ist, aber das Ändern dieses Wörterbuchs ändert nicht das vin outer(). Dies funktioniert nicht mehr, wenn Sie mehr innere Funktionen haben, die eine geschlossene Variable gemeinsam nutzen möchten. Sagen Sie a inc()und dec()erhöhen und verringern Sie einen gemeinsam genutzten Zähler.
BlackJack
@BlackJack nonlocalist eine Python 3-Funktion.
Marcin
Ja, ich weiß. Die Frage war, wie der Effekt von Python 3 nonlocalin Python 2 im Allgemeinen erzielt werden kann . Ihre Ideen decken nicht den allgemeinen Fall ab, sondern nur den mit einer inneren Funktion. Schauen Sie sich dieses Wesentliche als Beispiel an. Beide inneren Funktionen haben einen eigenen Container. Sie benötigen ein veränderbares Objekt im Rahmen der äußeren Funktion, wie bereits in anderen Antworten vorgeschlagen.
BlackJack
@BlackJack Das ist nicht die Frage, aber danke für deine Eingabe.
Marcin
Ja das ist die frage. Schauen Sie sich den oberen Rand der Seite an. adinsa hat Python 2.6 und benötigt Zugriff auf nichtlokale Variablen in einem Abschluss ohne das nonlocalin Python 3 eingeführte Schlüsselwort.
BlackJack
3

Ein anderer Weg, dies zu tun (obwohl es zu ausführlich ist):

import ctypes

def outer():
    y = 0
    def inner():
        ctypes.pythonapi.PyCell_Set(id(inner.func_closure[0]), id(y + 1))
        return y
    return inner

x = outer()
x()
>> 1
x()
>> 2
y = outer()
y()
>> 1
x()
>> 3
Ezer Fernandes
quelle
1
Eigentlich bevorzuge ich die Lösung mit einem Wörterbuch, aber dieses ist cool :)
Ezer Fernandes
0

Die Erweiterung der eleganten Lösung von Martineau oben auf einen praktischen und etwas weniger eleganten Anwendungsfall, den ich bekomme:

class nonlocals(object):
""" Helper to implement nonlocal names in Python 2.x.
Usage example:
def outer():
     nl = nonlocals( n=0, m=1 )
     def inner():
         nl.n += 1
     inner() # will increment nl.n

or...
    sums = nonlocals( { k:v for k,v in locals().iteritems() if k.startswith('tot_') } )
"""
def __init__(self, **kwargs):
    self.__dict__.update(kwargs)

def __init__(self, a_dict):
    self.__dict__.update(a_dict)
Amnon Harel
quelle
-3

Verwenden Sie eine globale Variable

def outer():
    global y # import1
    y = 0
    def inner():
        global y # import2 - requires import1
        y += 1
        return y
    return inner

f = outer()
print(f(), f(), f()) #prints 1 2 3

Persönlich mag ich die globalen Variablen nicht. Mein Vorschlag basiert jedoch auf der Antwort https://stackoverflow.com/a/19877437/1083704

def report():
        class Rank: 
            def __init__(self):
                report.ranks += 1
        rank = Rank()
report.ranks = 0
report()

Hier muss der Benutzer ranksjedes Mal eine globale Variable deklarieren , wenn Sie die aufrufen müssen report. Durch meine Verbesserung muss der Benutzer die Funktionsvariablen nicht mehr initialisieren.

Val
quelle
Es ist viel besser, ein Wörterbuch zu verwenden. Sie können auf die Instanz in verweisen inner, sie jedoch nicht zuweisen, aber Sie können ihre Schlüssel und Werte ändern. Dies vermeidet die Verwendung globaler Variablen.
Johannesestaas