Frage Warum schreiben List Comprehensions in die Loop-Variable, aber Generatoren nicht? [Duplikat]


Diese Frage hat hier bereits eine Antwort:

Wenn ich etwas mit List Comprehensions mache, schreibt es in eine lokale Variable:

i = 0
test = any([i == 2 for i in xrange(10)])
print i

Dies druckt "9". Wenn ich jedoch einen Generator verwende, schreibt er nicht in eine lokale Variable:

i = 0
test = any(i == 2 for i in xrange(10))
print i

Dies druckt "0".

Gibt es einen guten Grund für diesen Unterschied? Ist dies eine Design-Entscheidung oder nur ein zufälliges Nebenprodukt der Art, wie Generatoren und Listen-Comprehensions implementiert werden? Persönlich würde es mir besser erscheinen, wenn Listenkompressen nicht in lokale Variablen schreiben würden.


76
2017-11-07 22:32


Ursprung


Antworten:


Pythons Schöpfer, Guido van Rossum, erwähnt dies, als er darüber schrieb Generatorausdrücke das war einheitlich in Python 3 eingebaut: (Hervorhebung meins)

Wir haben auch eine weitere Änderung in Python 3 vorgenommen, um die Äquivalenz zwischen Listenkompressen und Generatorausdrücken zu verbessern. In Python 2 "leckt" das Listenverständnis die Schleifenkontrollvariable 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 List Comprehensions; es war jahrelang eines von Pythons "schmutzigen kleinen Geheimnissen". Es begann als ein vorsätzlicher Kompromiss, um Listen-Comprehensions blendend schnell zu machen, und obwohl es kein gewöhnlicher Fallstrick für Anfänger war, stach es definitiv Leute gelegentlich. Für Generatorausdrücke konnten wir dies nicht tun. Generatorausdrücke werden unter Verwendung von Generatoren implementiert, deren Ausführung einen separaten Ausführungsrahmen erfordert. Daher waren Generatorausdrücke (besonders wenn sie über eine kurze Sequenz iterieren) weniger effizient als Listenerfassungen.

In Python 3 haben wir uns jedoch entschlossen, das "schmutzige kleine Geheimnis" der Listenkompromittierung mit derselben Implementierungsstrategie wie für Generatorausdrücke zu beheben. In Python 3 wird also das obige Beispiel (nach der Änderung von print (x) :-) 'vorher' gedruckt, was beweist, dass das 'x' im Listenverständnis temporär Schatten wirft, aber das 'x' in der Umgebung nicht überschreibt Umfang.

In Python 3 wird das nicht mehr passieren.

Interessant, dict Verständnis in Python 2 mache das auch nicht; Dies liegt vor allem daran, dass dict-Comprehensions von Python 3 zurückportiert wurden und daher bereits über diese Korrektur verfügten.

Es gibt noch einige andere Fragen, die auch dieses Thema abdecken, aber ich bin mir sicher, dass Sie diese bereits bei der Suche nach dem Thema gesehen haben, oder? ;)


74
2017-11-07 22:38



Wie PEP 289 (Generator Expressions) erklärt:

Die Schleifenvariable (wenn es sich um eine einfache Variable oder ein Tupel von einfachen Variablen handelt) ist nicht der umgebenden Funktion ausgesetzt. Dies erleichtert die Implementierung und macht typische Anwendungsfälle zuverlässiger.

Dies scheint aus Implementierungsgründen geschehen zu sein.

Persönlich würde es mir besser erscheinen, wenn Listenkompressen nicht in lokale Variablen schreiben würden.

PEP 289 verdeutlicht dies auch:

Listenübersichten "lecken" auch ihre Schleifenvariable in den umgebenden Bereich. Dies wird sich auch in Python 3.0 ändern, so dass die semantische Definition eines Listenverständnisses in Python 3.0 der von list () entspricht.

Mit anderen Worten, das Verhalten, das Sie beschreiben, unterscheidet sich in Python 2, wurde aber in Python 3 behoben.


16
2017-11-07 22:35



Persönlich würde es mir besser erscheinen, wenn Listenkompressen nicht in lokale Variablen schreiben würden.

Du hast Recht. Dies ist in Python 3.x behoben. Das Verhalten ist in 2.x unverändert, so dass es keinen Einfluss auf vorhandenen Code hat, der dieses Loch (ab) verwendet.


9
2017-11-07 22:35



Weil weil.

Nein, wirklich, das ist es. Quirk der Umsetzung. Und wohl ein Bug, da er in Python 3 behoben ist.


4
2017-11-07 22:35



Als ein Nebenprodukt des Wanderns, wie List-Comprehensions tatsächlich implementiert werden, fand ich eine gute Antwort auf Ihre Frage.

Sehen Sie sich in Python 2 den für ein einfaches Listenverständnis generierten Byte-Code an:

>>> s = compile('[i for i in [1, 2, 3]]', '', 'exec')
>>> dis(s)
  1           0 BUILD_LIST               0
              3 LOAD_CONST               0 (1)
              6 LOAD_CONST               1 (2)
              9 LOAD_CONST               2 (3)
             12 BUILD_LIST               3
             15 GET_ITER            
        >>   16 FOR_ITER                12 (to 31)
             19 STORE_NAME               0 (i)
             22 LOAD_NAME                0 (i)
             25 LIST_APPEND              2
             28 JUMP_ABSOLUTE           16
        >>   31 POP_TOP             
             32 LOAD_CONST               3 (None)
             35 RETURN_VALUE  

es übersetzt sich im Wesentlichen zu einem einfachen for-loopDas ist der syntaktische Zucker dafür. Als Ergebnis die gleiche Semantik wie für for-loops sich bewerben:

a = []
for i in [1, 2, 3]
    a.append(i)
print(i) # 3 leaky

Im Fall des Listenverständnisses verwendet (C) Python einen "versteckten Listennamen" und eine spezielle Anweisung LIST_APPEND mit der Kreation umzugehen, tut aber wirklich nichts mehr.

Ihre Frage sollte also verallgemeinern, warum Python in die For-Schleife schreibt for-loops; das ist schön beantwortet durch einen Blogbeitrag von Eli Bendersky.

Python 3, wie bereits erwähnt, und andere haben die Semantik des Listenverständnisses geändert, um besser der von Generatoren zu entsprechen (indem ein separates Codeobjekt für das Verständnis geschaffen wird) und ist im Wesentlichen syntaktischer Zucker für Folgendes:

a = [i for i in [1, 2, 3]]

# equivalent to
def __f(it):
    _ = []
    for i in it
        _.append(i)
    return _
a = __f([1, 2, 3])

Dies wird nicht geleakt, da es nicht im obersten Bereich ausgeführt wird, wie dies bei Python 2 der Fall ist. Das i ist durchgesickert, nur in __f und dann als lokale Variable für diese Funktion zerstört.

Wenn Sie möchten, sehen Sie sich den für Python 3 generierten Byte-Code an Laufen dis('a = [i for i in [1, 2, 3]]'). Sie sehen, wie ein "verstecktes" Code-Objekt geladen wird und am Ende ein Funktionsaufruf erfolgt.


1
2018-02-26 02:26



Eine der subtilen Folgen des schmutzigen Geheimnisses, das oben beschrieben wurde, ist das list(...) und [...] hat in Python 2 nicht die gleichen Nebenwirkungen:

In [1]: a = 'Before'
In [2]: list(a for a in range(5))
In [3]: a
Out[3]: 'Before'

Also kein Nebeneffekt für den Generator-Ausdruck innerhalb des Listen-Konstruktors, aber der Nebeneffekt ist da in einem direkten Listen-Verständnis:

In [4]: [a for a in range(5)]
In [5]: a
Out[5]: 4

0
2017-10-28 10:51