Das Listenverständnis bindet Namen auch nach dem Umfang des Verstehens neu. Ist das richtig?

117

Das Verständnis hat einige unerwartete Wechselwirkungen mit dem Umfang. Ist das das erwartete Verhalten?

Ich habe eine Methode:

def leave_room(self, uid):
  u = self.user_by_id(uid)
  r = self.rooms[u.rid]

  other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
  other_us = [self.user_by_id(uid) for uid in other_uids]

  r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above

  # Interestingly, it's rebound to the last uid in the list, so the error only shows
  # up when len > 1

Dies ist eine brutale Fehlerquelle. Während ich neuen Code schreibe, finde ich nur gelegentlich sehr seltsame Fehler aufgrund des erneuten Bindens - selbst jetzt, wo ich weiß, dass es ein Problem ist. Ich muss eine Regel wie "Temp-Vars in Listenverständnissen immer mit Unterstrich voranstellen" festlegen, aber selbst das ist nicht narrensicher.

Die Tatsache, dass diese zufällige Zeitbombe wartet, negiert die nette "Benutzerfreundlichkeit" des Listenverständnisses.

Jabavu Adams
quelle
7
-1: "brutale Fehlerquelle"? Kaum. Warum so einen argumentativen Begriff wählen? Im Allgemeinen sind Anforderungsmissverständnisse und einfache Logikfehler die teuersten Fehler. Diese Art von Fehler war in vielen Programmiersprachen ein Standardproblem. Warum es "brutal" nennen?
S.Lott
44
Es verstößt gegen das Prinzip der geringsten Überraschung. Es wird auch nicht in der Python-Dokumentation zum Listenverständnis erwähnt, in der jedoch mehrmals erwähnt wird, wie einfach und bequem sie sind. Im Wesentlichen handelt es sich um eine Landmine, die außerhalb meines Sprachmodells existierte und daher für mich nicht vorhersehbar war.
Jabavu Adams
33
+1 für "brutale Fehlerquelle". Das Wort "brutal" ist völlig gerechtfertigt.
Nathaniel
3
Das einzige "brutale", was ich hier sehe, ist Ihre Namenskonvention. Dies ist nicht mehr die 80er Jahre. Sie sind nicht mehr auf 3-stellige Variablennamen beschränkt.
UloPe
5
Hinweis: In der Dokumentation wird angegeben, dass das Listenverständnis dem expliziten forKonstrukt for-loop und den Leckvariablen -loops entspricht . Es war also nicht explizit, sondern wurde implizit angegeben.
Bakuriu

Antworten:

172

Listenverständnisse verlieren die Regelungsvariable in Python 2, aber nicht in Python 3. Hier ist Guido van Rossum (Schöpfer von Python) , der die Geschichte dahinter erklärt:

Wir haben auch eine weitere Änderung in Python 3 vorgenommen, um die Äquivalenz zwischen Listenverständnis und Generatorausdrücken zu verbessern. In Python 2 "verliert" das Listenverständnis die Regelungsvariable in den umgebenden Bereich:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

Dies war ein Artefakt der ursprünglichen Implementierung von Listenverständnissen; Es war jahrelang eines von Pythons "schmutzigen kleinen Geheimnissen". Es begann als absichtlicher Kompromiss, um Listenverständnisse unglaublich schnell zu machen, und obwohl es für Anfänger keine häufige Gefahr war, stach es definitiv gelegentlich Menschen. Für Generatorausdrücke konnten wir dies nicht tun. Generatorausdrücke werden mithilfe von Generatoren implementiert, deren Ausführung einen separaten Ausführungsrahmen erfordert. Daher waren Generatorausdrücke (insbesondere wenn sie über eine kurze Sequenz iterieren) weniger effizient als Listenverständnisse.

In Python 3 haben wir uns jedoch entschlossen, das "schmutzige kleine Geheimnis" des Listenverständnisses zu beheben, indem wir dieselbe Implementierungsstrategie wie für Generatorausdrücke verwendeten. In Python 3 wird das obige Beispiel (nach der Änderung zur Verwendung von print (x) :-) "vor" gedruckt, was beweist, dass das "x" im Listenverständnis vorübergehend das "x" in der Umgebung überschattet, aber nicht überschreibt Umfang.

Steven Rumbalski
quelle
14
Ich werde hinzufügen, dass, obwohl Guido es ein "schmutziges kleines Geheimnis" nennt, viele es als eine Funktion betrachteten, nicht als einen Fehler.
Steven Rumbalski
38
Beachten Sie auch, dass jetzt in 2.7 Set- und Wörterbuchverständnisse (und Generatoren) private Bereiche haben, Listenverständnisse jedoch immer noch nicht. Dies ist zwar insofern sinnvoll, als die ersteren alle von Python 3 zurückportiert wurden, macht aber den Kontrast zum Listenverständnis wirklich erschütternd.
Matt B.
7
Ich weiß, dass dies eine wahnsinnig alte Frage ist, aber warum haben einige sie als ein Merkmal der Sprache angesehen? Gibt es etwas für diese Art von variablem Leck?
Mathias Müller
2
für: undichte Schleifen hat gute Gründe, insb. nach frühem Wert auf den letzten Wert zugreifen break- aber für Verständnisse irrelevant. Ich erinnere mich an einige comp.lang.python-Diskussionen, bei denen Leute Variablen mitten im Ausdruck zuweisen wollten. Der weniger verrückte Weg war der Einzelwert für Klauseln, z. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], benötigt aber nur eine verständnislokale Variable und funktioniert genauso gut in Python 3. Ich denke, "undicht" war die einzige Möglichkeit, eine Variable außerhalb eines Ausdrucks sichtbar zu machen. Alle waren sich einig, dass diese Techniken schrecklich sind :-)
Beni Cherniavsky-Paskin
1
Das Problem besteht hier darin, keinen Zugriff auf den umgebenden Bereich des Listenverständnisses zu haben, sondern im Bereich des Listenverständnisses zu binden, der sich auf den umgebenden Bereich auswirkt.
Felipe Gonçalves Marques
48

Ja, Listenverständnisse "lecken" ihre Variable in Python 2.x, genau wie bei Schleifen.

Rückblickend wurde dies als Fehler erkannt und mit Generatorausdrücken vermieden. BEARBEITEN: Wie Matt B. bemerkt , wurde dies auch vermieden, wenn Syntaxen für das Festlegen und das Wörterbuchverständnis von Python 3 zurückportiert wurden.

Das Verhalten des Listenverständnisses musste wie in Python 2 belassen werden, ist jedoch in Python 3 vollständig behoben.

Dies bedeutet, dass in allen:

list(x for x in a if x>32)
set(x//4 for x in a if x>32)         # just another generator exp.
dict((x, x//16) for x in a if x>32)  # yet another generator exp.
{x//4 for x in a if x>32}            # 2.7+ syntax
{x: x//16 for x in a if x>32}        # 2.7+ syntax

Das xist immer lokal für den Ausdruck, während diese:

[x for x in a if x>32]
set([x//4 for x in a if x>32])         # just another list comp.
dict([(x, x//16) for x in a if x>32])  # yet another list comp.

In Python 2.x verlieren alle die xVariable in den umgebenden Bereich.


UPDATE für Python 3.8 (?) : PEP 572 führt einen :=Zuweisungsoperator ein, der absichtlich aus dem Verständnis und den Generatorausdrücken herausläuft! Es ist im Wesentlichen durch zwei Anwendungsfälle motiviert: Erfassen eines "Zeugen" aus früh beendeten Funktionen wie any()und all():

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

und Aktualisieren des veränderlichen Zustands:

total = 0
partial_sums = [total := total + v for v in values]

Siehe Anhang B für den genauen Umfang. Die Variable wird in der nächsten Umgebung defoder zugewiesen lambda, es sei denn, diese Funktion deklariert sie nonlocaloder global.

Beni Cherniavsky-Paskin
quelle
7

Ja, die Zuweisung erfolgt dort wie in einer forSchleife. Es wird kein neuer Bereich erstellt.

Dies ist definitiv das erwartete Verhalten: In jedem Zyklus ist der Wert an den von Ihnen angegebenen Namen gebunden. Zum Beispiel,

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234

Sobald dies erkannt wurde, scheint es leicht zu vermeiden zu sein: Verwenden Sie keine vorhandenen Namen für die Variablen innerhalb des Verständnisses.

JAL
quelle
2

Interessanterweise hat dies keinen Einfluss auf das Wörterbuch oder das Verständnis.

>>> [x for x in range(1, 10)]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x
9
>>> {x for x in range(1, 5)}
set([1, 2, 3, 4])
>>> x
9
>>> {x:x for x in range(1, 100)}
{1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}
>>> x
9

Es wurde jedoch wie oben erwähnt in 3 behoben.

Chris Travers
quelle
Diese Syntax funktioniert in Python 2.6 überhaupt nicht. Sprechen Sie über Python 2.7?
Paul Hollingsworth
Python 2.6 hat nur Listenverständnisse, ebenso wie Python 3.0. 3.1 fügte Satz- und Wörterbuchverständnisse hinzu und diese wurden auf 2.7 portiert. Entschuldigung, wenn das nicht klar war. Es sollte eine Einschränkung auf eine andere Antwort festgestellt werden, und welche Versionen es betrifft, ist nicht ganz einfach.
Chris Travers
Ich kann mir zwar vorstellen, dass es Fälle gibt, in denen die Verwendung von Python 2.7 für neuen Code sinnvoll ist, aber ich kann nicht dasselbe für Python 2.6 sagen ... Auch wenn 2.6 mit Ihrem Betriebssystem geliefert wurde, bleiben Sie nicht hängen es. Erwägen Sie die Installation von virtualenv und die Verwendung von 3.6 für neuen Code!
Alex L
Der Punkt zu Python 2.6 könnte jedoch bei der Wartung vorhandener Legacy-Systeme auftauchen. Als historische Anmerkung ist es also nicht völlig irrelevant. Gleiches gilt für 3.0 (ick)
Chris Travers
Tut mir leid, wenn ich unhöflich klinge, aber das beantwortet die Frage in keiner Weise. Es ist besser als Kommentar geeignet.
0xc0de
1

Einige Problemumgehungen für Python 2.6, wenn dieses Verhalten nicht erwünscht ist

# python
Python 2.6.6 (r266:84292, Aug  9 2016, 06:11:56)
Type "help", "copyright", "credits" or "license" for more information.
>>> x=0
>>> a=list(x for x in xrange(9))
>>> x
0
>>> a=[x for x in xrange(9)]
>>> x
8
Marek Slebodnik
quelle
-1

In Python3 wird die Variable während des Listenverständnisses nicht geändert, nachdem der Gültigkeitsbereich überschritten wurde. Wenn wir jedoch eine einfache for-Schleife verwenden, wird die Variable außerhalb des Gültigkeitsbereichs neu zugewiesen.

i = 1 print (i) print ([i im Bereich (5)]) print (i) Der Wert von i bleibt nur 1.

Verwenden Sie jetzt einfach die for-Schleife. Der Wert von i wird neu zugewiesen.

ASHOK KUMAR
quelle