La comprensione dell'elenco riassocia i nomi anche dopo l'ambito della comprensione. È giusto?


118

Le comprensioni stanno avendo alcune interazioni impreviste con l'ambito. È questo il comportamento previsto?

Ho un metodo:

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

A rischio di piagnucolare, questa è una brutale fonte di errori. Mentre scrivo nuovo codice, solo occasionalmente trovo errori molto strani dovuti al rebinding, anche ora che so che è un problema. Devo fare una regola come "anteponi sempre le variabili temporanee nelle comprensioni di elenchi con un trattino basso", ma anche questo non è infallibile.

Il fatto che ci sia questa bomba a orologeria casuale in attesa in qualche modo nega tutta la piacevole "facilità d'uso" della comprensione delle liste.


7
-1: "brutale fonte di errori"? Quasi. Perché scegliere un termine così polemico? Generalmente gli errori più costosi sono incomprensioni di requisiti e semplici errori logici. Questo tipo di errore è stato un problema standard in molti linguaggi di programmazione. Perché chiamarlo "brutale"?
S.Lott

44
Viola il principio della minima sorpresa. Inoltre non è menzionato nella documentazione di python sulle comprensioni di elenchi, che tuttavia menziona più volte quanto siano facili e convenienti. Essenzialmente è una miniera terrestre che esisteva al di fuori del mio modello linguistico, e quindi era impossibile per me prevedere.
Jabavu Adams

33
+1 per "brutale fonte di errori". La parola "brutale" è del tutto giustificata.
Nathaniel

3
L'unica cosa "brutale" che vedo qui è la tua convenzione sui nomi. Non sono più gli anni '80 non sei più limitato a nomi di variabili di 3 caratteri.
UloPe

5
Nota: il documention fa stato che l'elenco-di comprensione sono equivalenti alla esplicita forcostrutto -loop e for-loops variabili perdite . Quindi non era esplicito ma era implicitamente affermato.
Bakuriu

Risposte:


172

Le comprensioni dell'elenco fanno trapelare la variabile di controllo del ciclo in Python 2 ma non in Python 3. Ecco Guido van Rossum (creatore di Python) che spiega la storia dietro questo:

Abbiamo anche apportato un'altra modifica in Python 3, per migliorare l'equivalenza tra le comprensioni delle liste e le espressioni del generatore. In Python 2, la comprensione della lista "trapela" la variabile di controllo del ciclo nello scope circostante:

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

Questo era un artefatto dell'implementazione originale della comprensione delle liste; è stato per anni uno dei "piccoli sporchi segreti" di Python. È iniziato come un compromesso intenzionale per rendere le comprensioni delle liste incredibilmente veloci e, sebbene non fosse una trappola comune per i principianti, occasionalmente pungeva sicuramente le persone. Per le espressioni del generatore non abbiamo potuto farlo. Le espressioni del generatore vengono implementate utilizzando generatori, la cui esecuzione richiede un frame di esecuzione separato. Pertanto, le espressioni del generatore (specialmente se ripetono su una breve sequenza) erano meno efficienti delle comprensioni di lista.

Tuttavia, in Python 3, abbiamo deciso di correggere il "piccolo sporco segreto" della comprensione delle liste utilizzando la stessa strategia di implementazione delle espressioni del generatore. Quindi, in Python 3, l'esempio sopra (dopo la modifica per usare print (x) :-) stamperà 'prima', dimostrando che la 'x' nella comprensione dell'elenco si ombreggia temporaneamente ma non sovrascrive la 'x' circostante scopo.


14
Aggiungo che sebbene Guido lo definisca uno "sporco piccolo segreto", molti lo considerano una funzionalità, non un bug.
Steven Rumbalski

38
Si noti inoltre che ora in 2.7, le comprensioni di set e dizionario (e generatori) hanno ambiti privati, ma le comprensioni di elenco ancora no. Anche se questo ha un senso in quanto i primi sono stati tutti portati indietro da Python 3, rende davvero il contrasto con le comprensioni delle liste stridente.
Matt B.

7
So che questa è una domanda follemente vecchia, ma perché alcuni la considerano una caratteristica della lingua? C'è qualcosa a favore di questo tipo di perdite variabili?
Mathias Müller

2
per: i loop che perdono hanno buone ragioni, esp. per accedere all'ultimo valore dopo l'inizio break- ma irrilevante per le comprensioni. Ricordo alcune discussioni di comp.lang.python in cui le persone volevano assegnare variabili nel mezzo dell'espressione. Il modo meno folle trovato è stato il valore singolo per le clausole es. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], ma ha solo bisogno di una var locale di comprensione e funziona altrettanto bene in Python 3. Penso che "leaking" fosse l'unico modo per impostare la variabile visibile al di fuori di un'espressione. Tutti hanno convenuto che queste tecniche sono orribili :-)
Beni Cherniavsky-Paskin

1
Il problema qui non è avere accesso all'ambito circostante delle comprensioni dell'elenco, ma vincolare l'ambito delle comprensioni dell'elenco che influisce sull'ambito circostante.
Felipe Gonçalves Marques,

48

Sì, le comprensioni delle liste "perdono" la loro variabile in Python 2.x, proprio come i cicli for.

In retrospettiva, questo è stato riconosciuto come un errore ed è stato evitato con espressioni generatrici. EDIT: Come osserva Matt B. è stato anche evitato quando le sintassi di comprensione di set e dizionari venivano backported da Python 3.

Il comportamento delle liste di comprensione doveva essere lasciato com'è in Python 2, ma è stato completamente risolto in Python 3.

Ciò significa che in tutto:

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

l' xè sempre locale per l'espressione mentre questi:

[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 tutte le xvariabili perdono nello scope circostante.


AGGIORNAMENTO per Python 3.8 (?) : PEP 572 introdurrà un :=operatore di assegnazione che fuoriesce deliberatamente dalle comprensioni e dalle espressioni del generatore! È motivato essenzialmente da 2 casi d'uso: acquisire un "testimone" da funzioni di terminazione anticipata come any()e all():

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

e aggiornamento dello stato mutevole:

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

Vedere l' Appendice B per l'esatto ambito. La variabile è assegnata nell'ambiente più vicino defo lambda, a meno che la funzione non la dichiari nonlocalo global.


7

Sì, l'assegnazione avviene lì, proprio come accadrebbe in un forciclo. Non viene creato alcun nuovo ambito.

Questo è sicuramente il comportamento previsto: ad ogni ciclo, il valore è vincolato al nome specificato. Per esempio,

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

Una volta riconosciuto, sembra abbastanza facile da evitare: non utilizzare nomi esistenti per le variabili all'interno delle comprensioni.


2

È interessante notare che questo non influisce sul dizionario o sulla comprensione dell'insieme.

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

Tuttavia è stato risolto in 3 come indicato sopra.


Questa sintassi non funziona affatto in Python 2.6. Stai parlando di Python 2.7?
Paul Hollingsworth,

Python 2.6 ha solo le liste di comprensione così come Python 3.0. 3.1 ha aggiunto la comprensione di set e dizionari e questi sono stati portati a 2.7. Scusa se non è stato chiaro. Aveva lo scopo di notare una limitazione a un'altra risposta e le versioni a cui si applica non sono del tutto semplici.
Chris Travers

Anche se posso immaginare di sostenere che ci sono casi in cui l'uso di python 2.7 per il nuovo codice ha senso, non posso dire lo stesso per python 2.6 ... Anche se 2.6 è quello che è stato fornito con il tuo sistema operativo, non sei bloccato con esso. Considera l'installazione di virtualenv e l'utilizzo di 3.6 per il nuovo codice!
Alex L

Il punto su Python 2.6 potrebbe emergere però nel mantenere i sistemi legacy esistenti. Quindi come nota storica non è del tutto irrilevante. Lo stesso con 3.0 (ick)
Chris Travers

Scusa se sembro scortese, ma questo non risponde in alcun modo alla domanda. È più adatto come commento.
0xc0de

1

qualche soluzione alternativa, per python 2.6, quando questo comportamento non è desiderabile

# 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

-1

In python3, mentre si trova nella comprensione delle liste, la variabile non viene modificata una volta terminato l'ambito, ma quando si usa il semplice ciclo for la variabile viene riassegnata fuori dall'ambito.

i = 1 print (i) print ([i nell'intervallo (5)]) print (i) Il valore di i rimarrà solo 1.

Ora usa semplicemente for loop il valore di i verrà riassegnato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.