122 votes

La compréhension de liste lie à nouveau les noms même après la portée de la compréhension. Est-ce bien le cas ?

Les compréhensions ont des interactions inattendues avec le scoping. 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. Lorsque j'écris du nouveau code, je trouve de temps en temps des erreurs très bizarres dues au rebinding -- même maintenant que je sais que c'est un problème. J'ai besoin de faire une règle comme "toujours faire précéder les variables temp dans les compréhensions de liste avec un soulignement", mais même cela n'est pas infaillible.

Le fait qu'il y ait cette bombe à retardement aléatoire annule en quelque sorte toute la "facilité d'utilisation" des compréhensions de listes.

7 votes

-1 : "source brutale d'erreurs" ? Pas du tout. Pourquoi choisir un tel terme argumentatif ? En général, les erreurs les plus coûteuses sont les malentendus sur les exigences et les simples erreurs de logique. Ce type d'erreur a été un problème standard dans de nombreux langages de programmation. Pourquoi l'appeler "brutale" ?

48 votes

Il viole le principe de la moindre surprise. Il n'est pas non plus mentionné dans la documentation python sur les compréhensions de listes, qui mentionne pourtant plusieurs fois combien elles sont faciles et pratiques. En fait, il s'agit d'une mine qui existait en dehors de mon modèle de langage, et qu'il m'était donc impossible de prévoir.

37 votes

+1 pour "source brutale d'erreurs". Le mot "brutal" est entièrement justifiée.

182voto

Steven Rumbalski Points 16838

Les compréhensions de listes font fuir 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 tout ça :

Nous avons également apporté un autre changement dans Python 3, afin d'améliorer l'équivalence entre les et les générateurs et les expressions de générateurs. Dans Python 2, la compréhension de liste fait "fuir" 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 de l'implémentation originale des compréhensions de listes ; c'était un des "sales petits secrets" de Python secrets" de Python pendant des années. Au départ, il s'agissait un compromis intentionnel pour rendre les compréhensions de listes pour rendre les compréhensions de listes incroyablement rapides, et alors que ce n'était pas un piège commun pour débutants, il a définitivement piqué les gens à l'occasion. Pour les générateurs nous ne pouvions pas faire cela. Les expressions de générateur sont implémentées en utilisant des générateurs, dont l'exécution nécessite un cadre d'exécution séparé. Ainsi, les expressions de générateur (surtout si elles itèrent sur une séquence courte) sont moins efficaces que les compréhensions de listes.

Cependant, dans Python 3, nous avons décidé de réparer le "sale petit secret" des compréhensions en utilisant le 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) :-) sera imprimera 'before', ce qui prouve que le 'x' de la compréhension de la liste dans la compréhension de la liste fait temporairement fait de l'ombre mais ne remplace pas le 'x' dans la portée environnante. dans la portée environnante.

16 votes

J'ajouterai que, bien que Guido appelle cela un "sale petit secret", beaucoup considèrent qu'il s'agit d'une fonctionnalité, et non d'un bug.

38 votes

Notez également que maintenant, dans la version 2.7, les compréhensions d'ensembles et de dictionnaires (et les générateurs) ont des scopes privés, mais pas les compréhensions de listes. Bien que cela ait un certain sens dans la mesure où les premiers ont tous été rétroportés de Python 3, cela rend le contraste avec les compréhensions de listes vraiment frappant.

9 votes

Je sais que c'est une question incroyablement vieille, mais pourquoi Certains l'ont-ils considéré comme une caractéristique du langage ? Y a-t-il des arguments en faveur de ce type de fuite de variables ?

49voto

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

Rétrospectivement, on s'est rendu compte que c'était une erreur, et on l'a évitée avec des expressions de générateur. EDIT : Comme Matt B. note elle a également été évitée lorsque les syntaxes de compréhension des ensembles et des dictionnaires ont été rétroportées depuis 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 qu'en tout :

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 site 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 fonction x à la portée environnante.


Mise à jour pour Python 3.8( ?) : PEP 572 introduira := opérateur d'affectation qui fuites délibérées à partir de compréhensions et d'expressions génératrices ! Il est motivé par essentiellement 2 cas d'utilisation : capturer un "témoin" à partir de fonctions à terminaison précoce telles que any() y all() :

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

et la mise à jour de l'état mutable :

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

Ver Annexe B pour un scoping exact. La variable est assignée dans l'environnement le plus proche def o lambda à moins que cette fonction ne le déclare nonlocal o global .

8voto

JAL Points 11164

Oui, l'affectation se produit ici, tout comme dans le cas d'une for boucle. Aucune nouvelle portée n'est créée.

C'est bien 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, il semble assez facile de l'éviter : n'utilisez pas les noms existants pour les variables dans les compréhensions.

2voto

Chris Travers Points 7808

Il est intéressant de noter que cela n'affecte pas les compréhensions de dictionnaires ou d'ensembles.

>>> [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, ce problème a été corrigé dans la version 3, comme indiqué ci-dessus.

1voto

Marek Slebodnik Points 11

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X