web-dev-qa-db-fra.com

La compréhension de liste lie les noms même après la portée de la compréhension. Est-ce correct?

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 brutale source d'erreurs. Lorsque j'écris du nouveau code, je trouve occasionnellement des erreurs très étranges en raison de 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éfacer les variables temporaires dans les listes avec soulignement", mais même ce n'est pas infaillible.

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

113
Jabavu Adams

Les compréhensions de liste 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'historique derrière cela:

Nous avons également effectué un autre changement dans Python 3, pour améliorer l'équivalence entre les compréhensions de liste et les expressions de générateur. Dans 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'

Il s'agissait d'un artefact de l'implémentation d'origine 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 les listes de compréhension incroyablement rapides, et même si ce n'était pas un piège courant pour les débutants, cela a définitivement piqué 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 une trame d'exécution distincte. Ainsi, les expressions génératrices (surtout si elles itèrent sur une courte séquence) étaient moins efficaces que les compréhensions de liste.

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 de générateur. Ainsi, dans Python 3, l'exemple ci-dessus (après modification pour utiliser print (x) :-) affichera "avant", prouvant que le "x" dans la compréhension de la liste apparaît temporairement mais ne remplace pas le "x 'dans la portée environnante.

164
Steven Rumbalski

Oui, les compréhensions de liste "fuient" leur variable dans Python 2.x, tout comme pour les boucles.

Rétrospectivement, cela a été reconnu comme une erreur, et cela a été évité avec les expressions de générateur. EDIT: Comme Matt B. note cela a également été évité lorsque les syntaxes de compréhension de l'ensemble et du dictionnaire ont été rétroportées à partir de Python 3.

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

Cela signifie que dans tous:

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 x est toujours local à l'expression alors 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.

dans Python 2.x, toutes les fuites de la variable x dans la portée environnante.


MISE À JOUR pour Python 3.8 (?) : PEP 572 présentera := Opérateur d'affectation qui fuit délibérément hors des compréhensions et des expressions du générateur! Il est motivé par essentiellement 2 cas d'utilisation: capturer un "témoin" à partir de fonctions à terminaison précoce comme 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 Annexe B pour la portée exacte. La variable est affectée dans def ou lambda le plus proche, sauf si cette fonction la déclare nonlocal ou global.

47

Oui, l'affectation se produit là, tout comme dans une boucle for. 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 cela reconnu, il semble assez facile à éviter: n'utilisez pas de noms existants pour les variables dans les compréhensions.

7
JAL

Fait intéressant, cela n'affecte pas les dictionnaires ou les ensembles de définitions.

>>> [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é en 3 comme indiqué ci-dessus.

2
Chris Travers

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
1
Marek Slebodnik