La compréhension de liste relie les noms même après l'étendue de la compréhension. Est-ce correct?

118

Les compréhensions ont des interactions inattendues avec la portée. Est-ce le comportement attendu?

J'ai une méthode:

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

Au risque de pleurnicher, c'est une source brutale d'erreurs. Au fur et à mesure que j'écris un nouveau code, je trouve occasionnellement des erreurs très étranges dues à la reliure - même maintenant que je sais que c'est un problème. J'ai besoin de faire une règle comme "toujours préface les variables temporaires dans les compréhensions de liste avec un trait de soulignement", mais même ce n'est pas infaillible.

Le fait qu'il y ait cette sorte d'attente aléatoire de bombe à retardement annule toute la "facilité d'utilisation" de la compréhension de liste.

Jabavu Adams
la source
7
-1: "source brutale d'erreurs"? À peine. Pourquoi choisir un terme aussi argumentatif? En général, les erreurs les plus coûteuses sont les malentendus des exigences et les erreurs logiques simples. Ce type d'erreur a été un problème standard dans de nombreux langages de programmation. Pourquoi l'appeler «brutal»?
S.Lott
44
Cela viole le principe de la moindre surprise. Ce n'est pas non plus mentionné dans la documentation python sur les compréhensions de liste qui mentionne cependant à plusieurs reprises à quel point elles sont faciles et pratiques. Essentiellement, c'est une mine terrestre qui existait en dehors de mon modèle de langage et qui était donc impossible pour moi de prévoir.
Jabavu Adams
33
+1 pour "source brutale d'erreurs". Le mot «brutal» est entièrement justifié.
Nathaniel
3
La seule chose «brutale» que je vois ici est votre convention de dénomination. Ce n'est plus les années 80, vous n'êtes plus limité à 3 noms de variables de caractères.
UloPe
5
Note: l'documention fait état de cette liste-compréhension sont équivalentes à l'explicite forconstruction -loop et for-loops des variables fuite . Ce n'était donc pas explicite, mais c'était implicitement déclaré.
Bakuriu

Réponses:

172

Les compréhensions de listes fuient la variable de contrôle de boucle dans Python 2 mais pas dans Python 3. Voici Guido van Rossum (créateur de Python) expliquant l'histoire derrière ceci:

Nous avons également apporté un autre changement dans Python 3, pour améliorer l'équivalence entre les compréhensions de liste et les expressions génératrices. En Python 2, la compréhension de la liste "fuit" la variable de contrôle de boucle dans la portée environnante:

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

C'était un artefact de l'implémentation originale des compréhensions de liste; c'était l'un des "sales petits secrets" de Python pendant des années. Cela a commencé comme un compromis intentionnel pour rendre la compréhension des listes incroyablement rapide, et bien que ce ne soit pas un écueil courant pour les débutants, cela piquait définitivement les gens de temps en temps. Pour les expressions génératrices, nous ne pouvions pas faire cela. Les expressions de générateur sont implémentées à l'aide de générateurs, dont l'exécution nécessite un cadre d'exécution distinct. Ainsi, les expressions génératrices (surtout si elles itèrent sur une courte séquence) étaient moins efficaces que les compréhensions de listes.

Cependant, dans Python 3, nous avons décidé de corriger le "sale petit secret" des compréhensions de liste en utilisant la même stratégie d'implémentation que pour les expressions génératrices. Ainsi, dans Python 3, l'exemple ci-dessus (après modification pour utiliser print (x) :-) affichera 'before', prouvant que le 'x' dans la compréhension de la liste fait temporairement de l'ombre mais ne remplace pas le 'x' dans l'environnement portée.

Steven Rumbalski
la source
14
J'ajouterai que bien que Guido l'appelle un "sale petit secret", beaucoup l'ont considéré comme une fonctionnalité, pas comme un bogue.
Steven Rumbalski
38
Notez également que maintenant dans 2.7, les compréhensions d'ensembles et de dictionnaires (et générateurs) ont des portées privées, mais pas encore les compréhensions de listes. Bien que cela ait du sens dans la mesure où les premiers ont tous été rétroportés à partir de Python 3, cela fait vraiment le contraste avec les compréhensions de liste discordantes.
Matt B.
7
Je sais que c'est une question incroyablement ancienne, mais pourquoi certains l'ont-ils considérée comme une caractéristique de la langue? Y a-t-il quelque chose en faveur de ce type de fuite variable?
Mathias Müller
2
pour: les boucles qui fuient ont de bonnes raisons, en particulier. pour accéder à la dernière valeur après le début break- mais sans rapport avec les comprehesions. Je me souviens de certaines discussions comp.lang.python où les gens voulaient attribuer des variables au milieu de l'expression. Le moyen le moins insensé trouvé était une valeur unique pour les clauses, par exemple. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], mais a juste besoin d'un var local de compréhension et fonctionne aussi bien en Python 3. Je pense que "la fuite" était le seul moyen de définir une variable visible en dehors d'une expression. Tout le monde a convenu que ces techniques sont horribles :-)
Beni Cherniavsky-Paskin
1
Le problème ici n'est pas d'avoir accès à la portée environnante des compréhensions de liste, mais la liaison dans la portée des compréhensions de liste affectant la portée environnante.
Felipe Gonçalves Marques
48

Oui, les list comprehensions "fuient" leur variable dans Python 2.x, tout comme pour les boucles for.

Rétrospectivement, cela a été reconnu comme une erreur, et cela a été évité avec des expressions génératrices. EDIT: Comme le note Matt B., cela a également été évité lorsque les syntaxes de définition et de compréhension de dictionnaire étaient rétroportées à partir de Python 3.

Le comportement des compréhensions de listes a dû être laissé tel quel dans Python 2, mais il est entièrement corrigé dans Python 3.

Cela signifie que dans l'ensemble de:

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

le xest toujours local à l'expression tandis que ceux-ci:

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

en Python 2.x, tous la xvariable fuit vers la portée environnante.


MISE À JOUR pour Python 3.8 (?) : PEP 572 introduira un :=opérateur d'assignation qui fuit délibérément les compréhensions et les expressions du générateur! Il est motivé par essentiellement 2 cas d'utilisation: capturer un «témoin» à partir de fonctions de terminaison anticipée telles que any()et all():

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

et mise à jour de l'état mutable:

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

Voir l' annexe B pour la portée exacte. La variable est affectée dans l'entourage le plus proche defou lambda, à moins que cette fonction ne le déclare nonlocalou global.

Beni Cherniavsky-Paskin
la source
7

Oui, l'affectation se produit là, comme elle le ferait en forboucle. Aucune nouvelle étendue n'est en cours de création.

C'est certainement le comportement attendu: à chaque cycle, la valeur est liée au nom que vous spécifiez. Par exemple,

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

Une fois que cela est reconnu, cela semble assez facile à éviter: n'utilisez pas de noms existants pour les variables dans les compréhensions.

JAL
la source
2

Fait intéressant, cela n'affecte pas le dictionnaire ou la compréhension d'ensemble.

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

Cependant, il a été corrigé dans 3 comme indiqué ci-dessus.

Chris Travers
la source
Cette syntaxe ne fonctionne pas du tout dans Python 2.6. Parlez-vous de Python 2.7?
Paul Hollingsworth
Python 2.6 a une compréhension de liste uniquement, tout comme Python 3.0. 3.1 ajouta des compréhensions d'ensemble et de dictionnaire et celles-ci furent portées vers 2.7. Désolé si ce n'était pas clair. Il visait à noter une limitation à une autre réponse, et à quelles versions elle s'applique n'est pas tout à fait simple.
Chris Travers
Bien que je puisse imaginer argumenter qu'il y a des cas où l'utilisation de python 2.7 pour un nouveau code a du sens, je ne peux pas en dire autant pour python 2.6 ... Même si la version 2.6 est fournie avec votre système d'exploitation, vous n'êtes pas coincé avec il. Envisagez d'installer virtualenv et d'utiliser la version 3.6 pour le nouveau code!
Alex L
Le point sur Python 2.6 pourrait cependant surgir dans la maintenance des systèmes hérités existants. Donc, en tant que note historique, ce n'est pas totalement hors de propos. Idem avec 3.0 (ick)
Chris Travers
Désolé si j'ai l'air impoli, mais cela ne répond en aucun cas à la question. C'est mieux adapté comme commentaire.
0xc0de
1

une solution de contournement, pour python 2.6, lorsque ce comportement n'est pas souhaitable

# 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
la source
-1

En python3, pendant la compréhension de liste, la variable ne reçoit pas de changement une fois sa portée terminée, mais lorsque nous utilisons une simple boucle for, la variable est réaffectée hors de portée.

i = 1 print (i) print ([i in range (5)]) print (i) La valeur de i restera 1 seulement.

Maintenant, utilisez simplement pour la boucle, la valeur de i sera réaffectée.

ASHOK KUMAR
la source