Comment accédez-vous à d'autres variables de classe à partir d'une compréhension de liste dans la définition de classe? Ce qui suit fonctionne dans Python 2 mais échoue dans Python 3:
class Foo:
x = 5
y = [x for i in range(1)]
Python 3.2 donne l'erreur:
NameError: global name 'x' is not defined
Essayer Foo.x
Ne fonctionne pas non plus. Des idées sur la façon de le faire dans Python 3?
Un exemple de motivation un peu plus compliqué:
from collections import namedtuple
class StateDatabase:
State = namedtuple('State', ['name', 'capital'])
db = [State(*args) for args in [
['Alabama', 'Montgomery'],
['Alaska', 'Juneau'],
# ...
]]
Dans cet exemple, apply()
aurait été une solution de contournement décente, mais elle est malheureusement supprimée de Python 3.
La portée et la liste des classes, les compréhensions d'ensemble ou de dictionnaire, ainsi que les expressions de générateur ne se mélangent pas.
Dans Python 3, les compréhensions de liste ont reçu leur propre portée (espace de noms local), pour éviter que leurs variables locales ne se propagent dans la portée environnante (voir La compréhension de la liste de Python redéfinit les noms) même après la portée de la compréhension. Est-ce vrai? ) C'est génial lorsque vous utilisez une telle compréhension de liste dans un module ou dans une fonction, mais dans les classes, la portée est un peu, euh, étrange .
Ceci est documenté dans pep 227 :
Les noms dans la portée de classe ne sont pas accessibles. Les noms sont résolus dans l'étendue de fonction englobante la plus interne. Si une définition de classe se produit dans une chaîne d'étendues imbriquées, le processus de résolution ignore les définitions de classe.
et dans la documentation de l'instruction composée class
:
La suite de la classe est ensuite exécutée dans un nouveau cadre d'exécution (voir la section Nommage et liaison ), en utilisant un espace de noms local nouvellement créé et l'espace de noms global d'origine. (Habituellement, la suite ne contient que des définitions de fonctions.) Lorsque la suite de la classe termine l'exécution, son cadre d'exécution est ignoré mais son espace de noms local est enregistré . [4] Un objet classe est ensuite créé en utilisant la liste d'héritage pour les classes de base et l'espace de noms local enregistré pour le dictionnaire d'attributs.
Soulignez le mien; le cadre d'exécution est la portée temporaire.
Étant donné que la portée est réutilisée en tant qu'attributs sur un objet de classe, le fait de pouvoir l'utiliser également comme portée non locale conduit à un comportement non défini; que se passerait-il si une méthode de classe faisait référence à x
comme variable de portée imbriquée, puis manipulait également Foo.x
, par exemple? Plus important encore, qu'est-ce que cela signifierait pour les sous-classes de Foo
? Python a pour traiter une portée de classe différemment car elle est très différente d'une portée de fonction.
Dernier point, mais non le moindre, la section liée dénomination et liaison dans la documentation du modèle d'exécution mentionne explicitement les étendues de classe:
La portée des noms définis dans un bloc de classe est limitée au bloc de classe; il ne s'étend pas aux blocs de code des méthodes - cela inclut les compréhensions et les expressions de générateur car elles sont implémentées à l'aide d'une étendue de fonction. Cela signifie que les éléments suivants échoueront:
class A: a = 42 b = list(a + i for i in range(10))
Donc, pour résumer: vous ne pouvez pas accéder à la portée de la classe à partir des fonctions, des listes de compréhension ou des expressions de générateur incluses dans cette portée; ils agissent comme si cette portée n'existait pas. Dans Python 2, les compréhensions de liste ont été implémentées à l'aide d'un raccourci, mais dans Python 3, elles ont obtenu leur propre étendue de fonction (comme elles auraient dû l'avoir depuis le début) et ainsi votre exemple casse. Les autres types de compréhension ont leur propre portée indépendamment de Python version, donc un exemple similaire avec une compréhension set ou dict se casserait Python 2 .
# Same error, in Python 2 or 3
y = {x: x for i in range(1)}
Il existe une partie d'une expression de compréhension ou de générateur qui s'exécute dans la portée environnante, quelle que soit la version Python. Ce serait l'expression de l'itération la plus externe. Dans votre exemple, c'est la fonction range(1)
:
y = [x for i in range(1)]
# ^^^^^^^^
Ainsi, l'utilisation de x
dans cette expression ne générerait pas d'erreur:
# Runs fine
y = [i for i in range(x)]
Cela ne s'applique qu'aux éléments les plus itérables; si une compréhension a plusieurs clauses for
, les itérables des clauses internes for
sont évaluées dans la portée de la compréhension:
# NameError
y = [i for i in range(1) for j in range(x)]
Cette décision de conception a été prise afin de générer une erreur au moment de la création de genexp au lieu du temps d'itération lorsque la création de l'itérable le plus à l'extérieur d'une expression de générateur génère une erreur, ou lorsque l'itérable le plus à l'extérieur s'avère ne pas être itérable. Les compréhensions partagent ce comportement pour la cohérence.
Vous pouvez voir tout cela en action en utilisant le module dis
. J'utilise Python 3.3 dans les exemples suivants, car il ajoute noms qualifiés qui identifient soigneusement les objets de code que nous voulons inspecter. Le bytecode produit est par ailleurs fonctionnellement identique à Python 3.2.
Pour créer une classe, Python prend essentiellement toute la suite qui compose le corps de la classe (donc tout indenté d'un niveau plus profond que le class <name>:
), Et l'exécute comme s'il s'agissait d'une fonction:
>>> import dis
>>> def foo():
... class Foo:
... x = 5
... y = [x for i in range(1)]
... return Foo
...
>>> dis.dis(foo)
2 0 LOAD_BUILD_CLASS
1 LOAD_CONST 1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>)
4 LOAD_CONST 2 ('Foo')
7 MAKE_FUNCTION 0
10 LOAD_CONST 2 ('Foo')
13 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
16 STORE_FAST 0 (Foo)
5 19 LOAD_FAST 0 (Foo)
22 RETURN_VALUE
Le premier LOAD_CONST
Y charge un objet de code pour le corps de classe Foo
, puis en fait une fonction et l'appelle. Le résultat de cet appel est ensuite utilisé pour créer l'espace de noms de la classe, son __dict__
. Jusqu'ici tout va bien.
La chose à noter ici est que le bytecode contient un objet de code imbriqué; en Python, les définitions de classe, les fonctions, les compréhensions et les générateurs sont tous représentés comme des objets de code qui contiennent non seulement du bytecode, mais aussi des structures qui représentent des variables locales, des constantes, des variables tirées des globales et des variables tirées de la portée imbriquée. Le bytecode compilé se réfère à ces structures et l'interpréteur python sait comment accéder à ceux donnés les bytecodes présentés.
La chose importante à retenir ici est que Python crée ces structures au moment de la compilation; la suite class
est un objet de code (<code object Foo at 0x10a436030, file "<stdin>", line 2>
) Qui est déjà compilé.
Inspectons cet objet de code qui crée le corps de classe lui-même; les objets de code ont une structure co_consts
:
>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo')
>>> dis.dis(foo.__code__.co_consts[1])
2 0 LOAD_FAST 0 (__locals__)
3 STORE_LOCALS
4 LOAD_NAME 0 (__name__)
7 STORE_NAME 1 (__module__)
10 LOAD_CONST 0 ('foo.<locals>.Foo')
13 STORE_NAME 2 (__qualname__)
3 16 LOAD_CONST 1 (5)
19 STORE_NAME 3 (x)
4 22 LOAD_CONST 2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>)
25 LOAD_CONST 3 ('foo.<locals>.Foo.<listcomp>')
28 MAKE_FUNCTION 0
31 LOAD_NAME 4 (range)
34 LOAD_CONST 4 (1)
37 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
40 GET_ITER
41 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
44 STORE_NAME 5 (y)
47 LOAD_CONST 5 (None)
50 RETURN_VALUE
Le bytecode ci-dessus crée le corps de classe. La fonction est exécutée et l'espace de noms locals()
résultant, contenant x
et y
est utilisé pour créer la classe (sauf que cela ne fonctionne pas car x
n'est pas défini comme global). Notez qu'après avoir stocké 5
Dans x
, il charge un autre objet de code; c'est la compréhension de la liste; il est enveloppé dans un objet fonction tout comme le corps de la classe; la fonction créée prend un argument positionnel, la range(1)
itérable à utiliser pour son code en boucle, transtypée en un itérateur. Comme indiqué dans le bytecode, range(1)
est évalué dans la portée de la classe.
De cela, vous pouvez voir que la seule différence entre un objet de code pour une fonction ou un générateur, et un objet de code pour une compréhension est que ce dernier est exécuté immédiatement lorsque le code parent l'objet est exécuté; le bytecode crée simplement une fonction à la volée et l'exécute en quelques petites étapes.
Python 2.x utilise le bytecode en ligne à la place, voici la sortie de Python 2.7:
2 0 LOAD_NAME 0 (__name__)
3 STORE_NAME 1 (__module__)
3 6 LOAD_CONST 0 (5)
9 STORE_NAME 2 (x)
4 12 BUILD_LIST 0
15 LOAD_NAME 3 (range)
18 LOAD_CONST 1 (1)
21 CALL_FUNCTION 1
24 GET_ITER
>> 25 FOR_ITER 12 (to 40)
28 STORE_NAME 4 (i)
31 LOAD_NAME 2 (x)
34 LIST_APPEND 2
37 JUMP_ABSOLUTE 25
>> 40 STORE_NAME 5 (y)
43 LOAD_LOCALS
44 RETURN_VALUE
Aucun objet de code n'est chargé, mais une boucle FOR_ITER
Est exécutée en ligne. Ainsi, dans Python 3.x, le générateur de liste a reçu un objet de code propre, ce qui signifie qu'il a sa propre portée.
Cependant, la compréhension a été compilée avec le reste du code source python lorsque le module ou le script a été chargé pour la première fois par l'interpréteur et que le compilateur ne pas considérer une suite de classes comme une portée valide. Toute variable référencée dans une compréhension de liste doit regarder dans la portée entourant la définition de classe, récursivement. Si la variable n'a pas été trouvée par le compilateur, il le marque comme un global. Le démontage de l'objet code de compréhension de liste montre que x
est en effet chargé comme un global:
>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None)
>>> dis.dis(foo.__code__.co_consts[1].co_consts[2])
4 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_GLOBAL 0 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
Ce morceau de bytecode charge le premier argument passé (l'itérateur range(1)
), et tout comme la version Python 2.x utilise FOR_ITER
Pour boucler et créer sa sortie.
Si nous avions plutôt défini x
dans la fonction foo
, x
serait une variable de cellule (les cellules se réfèrent à des étendues imbriquées):
>>> def foo():
... x = 2
... class Foo:
... x = 5
... y = [x for i in range(1)]
... return Foo
...
>>> dis.dis(foo.__code__.co_consts[2].co_consts[2])
5 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_DEREF 0 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
Le LOAD_DEREF
Chargera indirectement x
à partir des objets de la cellule objet code:
>>> foo.__code__.co_cellvars # foo function `x`
('x',)
>>> foo.__code__.co_consts[2].co_cellvars # Foo class, no cell variables
()
>>> foo.__code__.co_consts[2].co_consts[2].co_freevars # Refers to `x` in foo
('x',)
>>> foo().y
[2]
Le référencement réel recherche la valeur à partir des structures de données de trame actuelles, qui ont été initialisées à partir de l'attribut .__closure__
D'un objet fonction. Étant donné que la fonction créée pour l'objet de code de compréhension est à nouveau supprimée, nous ne pouvons pas inspecter la fermeture de cette fonction. Pour voir une fermeture en action, nous devons plutôt inspecter une fonction imbriquée:
>>> def spam(x):
... def eggs():
... return x
... return eggs
...
>>> spam(1).__code__.co_freevars
('x',)
>>> spam(1)()
1
>>> spam(1).__closure__
>>> spam(1).__closure__[0].cell_contents
1
>>> spam(5).__closure__[0].cell_contents
5
Donc, pour résumer:
Si vous deviez créer une portée explicite pour la variable x
, comme dans une fonction, vous pouvez utiliser des variables de portée de classe pour une compréhension de liste:
>>> class Foo:
... x = 5
... def y(x):
... return [x for i in range(1)]
... y = y(x)
...
>>> Foo.y
[5]
La fonction 'temporaire' y
peut être appelée directement; nous le remplaçons quand nous le faisons avec sa valeur de retour. Sa portée est prise en compte lors de la résolution de x
:
>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)
Bien sûr, les personnes lisant votre code se gratteront un peu la tête; vous voudrez peut-être y mettre un gros commentaire expliquant pourquoi vous faites cela.
La meilleure solution consiste à utiliser simplement __init__
Pour créer une variable d'instance à la place:
def __init__(self):
self.y = [self.x for i in range(1)]
et évitez tous les grattements de tête et les questions pour vous expliquer. Pour votre propre exemple concret, je ne stockerais même pas le namedtuple
sur la classe; soit utilisez la sortie directement (ne stockez pas du tout la classe générée), soit utilisez un global:
from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])
class StateDatabase:
db = [State(*args) for args in [
('Alabama', 'Montgomery'),
('Alaska', 'Juneau'),
# ...
]]
À mon avis, c'est une faille dans Python 3. J'espère qu'ils le changeront.
Old Way (fonctionne en 2.7, jette NameError: name 'x' is not defined
dans 3+):
class A:
x = 4
y = [x+i for i in range(1)]
REMARQUE: il suffit de l'étendre avec A.x
ne le résoudrait pas
New Way (fonctionne en 3+):
class A:
x = 4
y = (lambda x=x: [x+i for i in range(1)])()
Parce que la syntaxe est si moche, j'initialise simplement toutes mes variables de classe dans le constructeur généralement
La réponse acceptée fournit d'excellentes informations, mais il semble y avoir quelques autres rides ici - des différences entre la compréhension de la liste et les expressions génératrices. Une démo avec laquelle j'ai joué:
class Foo:
# A class-level variable.
X = 10
# I can use that variable to define another class-level variable.
Y = sum((X, X))
# Works in Python 2, but not 3.
# In Python 3, list comprehensions were given their own scope.
try:
Z1 = sum([X for _ in range(3)])
except NameError:
Z1 = None
# Fails in both.
# Apparently, generator expressions (that's what the entire argument
# to sum() is) did have their own scope even in Python 2.
try:
Z2 = sum(X for _ in range(3))
except NameError:
Z2 = None
# Workaround: put the computation in lambda or def.
compute_z3 = lambda val: sum(val for _ in range(3))
# Then use that function.
Z3 = compute_z3(X)
# Also worth noting: here I can refer to XS in the for-part of the
# generator expression (Z4 works), but I cannot refer to XS in the
# inner-part of the generator expression (Z5 fails).
XS = [15, 15, 15, 15]
Z4 = sum(val for val in XS)
try:
Z5 = sum(XS[i] for i in range(len(XS)))
except NameError:
Z5 = None
print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)
Il s'agit d'un bug en Python. Les compréhensions sont annoncées comme étant équivalentes aux boucles for, mais ce n'est pas vrai dans les classes. Au moins jusqu'à Python 3.6.6, dans une compréhension utilisée dans une classe, une seule variable extérieure à la compréhension est accessible à l'intérieur de la compréhension, et elle doit être utilisée comme itérateur le plus externe. une fonction, cette limitation de portée ne s'applique pas.
Pour illustrer pourquoi il s'agit d'un bogue, revenons à l'exemple d'origine. Cela échoue:
class Foo:
x = 5
y = [x for i in range(1)]
Mais cela fonctionne:
def Foo():
x = 5
y = [x for i in range(1)]
La limitation est indiquée à la fin de cette section dans le guide de référence.
Étant donné que l'itérateur le plus à l'extérieur est évalué dans la portée environnante, nous pouvons utiliser Zip
avec itertools.repeat
pour reporter les dépendances sur la portée de la compréhension:
import itertools as it
class Foo:
x = 5
y = [j for i, j in Zip(range(3), it.repeat(x))]
On peut également utiliser des boucles imbriquées for
dans la compréhension et inclure les dépendances dans l'itératif le plus externe:
class Foo:
x = 5
y = [j for j in (x,) for i in range(3)]
Pour l'exemple spécifique de l'OP:
from collections import namedtuple
import itertools as it
class StateDatabase:
State = namedtuple('State', ['name', 'capital'])
db = [State(*args) for State, args in Zip(it.repeat(State), [
['Alabama', 'Montgomery'],
['Alaska', 'Juneau'],
# ...
])]