Accéder aux variables de classe à partir d'une compréhension de liste dans la définition de classe


174

Comment accéder à 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.xne fonctionne pas non plus. Des idées sur la façon de faire cela dans Python 3?

Un exemple motivant 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()cela aurait été une solution de contournement décente, mais il est malheureusement supprimé de Python 3.


Votre message d'erreur est incorrect. J'utilise NameError: global name 'x' is not definedPython 3.2 et 3.3, ce à quoi je m'attendais.
Martijn Pieters

Intéressant ... Une solution de contournement évidente consiste à affecter y après avoir quitté la définition de classe. Foo.y = [Foo.x for i in range (1)]
gps

3
+ martijn-pieters lien vers un doublon est correct, il y a un commentaire de + matt-b là-dedans avec l'explication: les compréhensions de liste Python 2.7 n'ont pas leur propre espace de noms (contrairement aux compréhensions ensemblistes ou dict ou aux expressions génératrices ... remplacez votre [ ] avec {} pour voir cela en action). Ils ont tous leur propre espace de noms en 3.
gps

@gps: Ou utilisez une portée imbriquée, en insérant une fonction (temporaire) dans la suite de définitions de classe.
Martijn Pieters

Je viens de tester le 2.7.11. Erreur de nom obtenu
Junchao Gu

Réponses:


244

La portée de la classe et la liste, la compréhension d'ensemble ou de dictionnaire, ainsi que les expressions du générateur ne se mélangent pas.

Le pourquoi; ou, le mot officiel à ce sujet

Dans Python 3, les compréhensions de liste ont reçu une portée appropriée (espace de noms local) qui leur est propre, pour empêcher leurs variables locales de se répandre dans la portée environnante (voir Python list comprehension rebind names 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 de la portée de la classe ne sont pas accessibles. Les noms sont résolus dans la portée de la 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 classdocumentation de la déclaration composée :

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 fonction.) Lorsque la suite de la classe termine son exécution, son cadre d'exécution est ignoré mais son espace de noms local est enregistré . [4] Un objet de 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 l'autoriser à être utilisée comme une portée non locale conduit également à un comportement indéfini; que se passerait-il si une méthode de classe appelée xvariable de portée imbriquée, la manipule Foo.xégalement, par exemple? Plus important encore, qu'est-ce que cela signifierait pour les sous-classes de Foo? Python doit 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 des moindres, la section de dénomination et de liaison liée 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 génératrices puisqu'elles sont implémentées en utilisant une portée de fonction. Cela signifie que ce qui suit échouera:

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 compréhensions de liste ou des expressions génératrices 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 leur propre portée de fonction (comme elles auraient dû l'avoir depuis le début) et donc votre exemple est interrompu. D'autres types de compréhension ont leur propre portée quelle que soit la version de Python, donc un exemple similaire avec une compréhension d'ensemble ou de dict serait interrompu dans Python 2.

# Same error, in Python 2 or 3
y = {x: x for i in range(1)}

La (petite) exception; ou, pourquoi une partie peut encore fonctionner

Il y a 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 de Python. Ce serait l'expression de l'itérable le plus externe. Dans votre exemple, c'est le range(1):

y = [x for i in range(1)]
#               ^^^^^^^^

Ainsi, utiliser xdans cette expression ne générerait pas d'erreur:

# Runs fine
y = [i for i in range(x)]

Cela s'applique uniquement à l'itérable le plus externe; si une compréhension a plusieurs forclauses, les itérables des forclauses internes 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 lancer 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 externe d'une expression de générateur génère une erreur, ou lorsque l'itérable le plus externe s'avère ne pas être itérable. Les compréhensions partagent ce comportement par souci de cohérence.

Regardant sous le capot; ou bien plus de détails que vous ne l'auriez jamais voulu

Vous pouvez voir tout cela en action en utilisant le dismodule . J'utilise Python 3.3 dans les exemples suivants, car il ajoute des noms qualifiés qui identifient parfaitement 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 constitue le corps de la classe (donc tout est mis en retrait d'un niveau plus profond que la class <name>:ligne), 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_CONSTcharge un objet de code pour le Foocorps de la classe, puis le transforme en fonction et l'appelle. Le résultat de cet appel est ensuite utilisé pour créer l'espace de noms de la classe, its __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 également des structures qui représentent des variables locales, des constantes, des variables issues de globaux et des variables issues de la portée imbriquée. Le bytecode compilé fait référence à ces structures et l'interpréteur python sait comment accéder à celles données les bytecodes présentés.

La chose importante à retenir ici est que Python crée ces structures au moment de la compilation; la classsuite est un objet de code ( <code object Foo at 0x10a436030, file "<stdin>", line 2>) déjà compilé.

Inspectons cet objet de code qui crée le corps de classe lui-même; les objets de code ont une co_constsstructure:

>>> 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 la classe. La fonction est exécutée et l' locals()espace de noms résultant , contenant xet yest utilisé pour créer la classe (sauf que cela ne fonctionne pas car xn'est pas défini comme un global). On notera que , après le stockage 5dans x, il charge un autre objet de code; c'est la compréhension de la liste; il est enveloppé dans un objet fonction comme l'était le corps de la classe; la fonction créée prend un argument de position, l' range(1)itérable à utiliser pour son code en boucle, transtypé en itérateur. Comme indiqué dans le bytecode, range(1)est évalué dans la portée de la classe.

De là, vous pouvez voir que la seule différence entre un objet code pour une fonction ou un générateur, et un objet code pour une compréhension est que ce dernier est exécuté immédiatement lorsque l'objet code parent 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 à la place du bytecode en ligne, 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é, à la place une FOR_ITERboucle est exécutée en ligne. Ainsi, en 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 le compilateur ne considère pas une suite de classes comme une portée valide. Toutes les variables référencées dans une compréhension de liste doivent regarder dans la portée entourant la définition de classe, de manière récursive. Si la variable n'a pas été trouvée par le compilateur, il la marque comme une variable globale. Le démontage de l'objet de code de compréhension de liste montre qu'il xest 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' range(1)itérateur), et tout comme la version Python 2.x l'utilise FOR_ITERpour faire une boucle dessus et créer sa sortie.

Si nous avions défini xdans la foofonction à la place, xserait une variable de cellule (les cellules font référence à des portées 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_DEREFchargera indirectement à xpartir des objets de cellule de l'objet de 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' .__closure__attribut d'un objet fonction . Puisque 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 devrions 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:

  • Les compréhensions de liste obtiennent leurs propres objets de code dans Python 3, et il n'y a aucune différence entre les objets de code pour les fonctions, les générateurs ou les compréhensions; Les objets de code de compréhension sont enveloppés dans un objet fonction temporaire et appelés immédiatement.
  • Les objets de code sont créés au moment de la compilation et toutes les variables non locales sont marquées comme globales ou comme variables libres, en fonction des portées imbriquées du code. Le corps de la classe n'est pas considéré comme une portée pour rechercher ces variables.
  • Lors de l'exécution du code, Python n'a qu'à regarder dans les globaux ou la fermeture de l'objet en cours d'exécution. Étant donné que le compilateur n'a pas inclus le corps de la classe comme portée, l'espace de noms de la fonction temporaire n'est pas pris en compte.

Une solution de contournement; ou, que faire à ce sujet

Si vous deviez créer une portée explicite pour la xvariable, 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» ypeut être appelée directement; nous le remplaçons lorsque nous le faisons avec sa valeur de retour. Sa portée est prise en compte lors de la résolution 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 gens qui liront votre code se gratteront un peu la tête à ce sujet; vous voudrez peut-être y mettre un gros commentaire expliquant pourquoi vous faites cela.

La meilleure solution consiste simplement __init__à créer une variable d'instance à la place:

def __init__(self):
    self.y = [self.x for i in range(1)]

et évitez tous les grattages de tête et les questions pour vous expliquer. Pour votre propre exemple concret, je ne conserverais même pas le namedtuplesur la classe; soit utilisez la sortie directement (ne stockez pas du tout la classe générée), ou utilisez un global:

from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]

21
Vous pouvez également utiliser un lambda pour corriger la liaison:y = (lambda x=x: [x for i in range(1)])()
ecatmur

3
@ecatmur: Exactement, ce ne lambdasont que des fonctions anonymes, après tout.
Martijn Pieters

2
Pour mémoire, la solution de contournement qui utilise un argument par défaut (à un lambda ou à une fonction) pour passer la variable de classe a un gotcha. À savoir, il transmet la valeur actuelle de la variable. Ainsi, si la variable change plus tard et que le lambda ou la fonction est appelé, le lambda ou la fonction utilisera l'ancienne valeur. Ce comportement diffère du comportement d'une fermeture (qui capturerait une référence à la variable plutôt qu'à sa valeur), il peut donc être inattendu.
Neal Young

9
Si cela nécessite une page d'informations techniques pour expliquer pourquoi quelque chose ne fonctionne pas intuitivement, j'appelle cela un bogue.
Jonathan

5
@JonathanLeaders: N'appelez pas ça un bogue , appelez ça un compromis . Si vous voulez A et B, mais que vous ne pouvez en obtenir qu'un, peu importe comment vous décidez, dans certaines situations, vous n'aimerez pas le résultat. C'est la vie.
Lutz Prechelt

15

À mon avis, c'est une faille dans Python 3. J'espère qu'ils le changeront.

Old Way (fonctionne en 2.7, lance NameError: name 'x' is not defined3+):

class A:
    x = 4
    y = [x+i for i in range(1)]

REMARQUE: il suffit de le cadrer 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, je viens d'initialiser toutes mes variables de classe dans le constructeur généralement


6
Le problème est également présent dans Python 2, lors de l'utilisation d'expressions de générateur, ainsi que lors de la compréhension d'ensembles et de dictionnaires. Ce n'est pas un bogue, c'est une conséquence du fonctionnement des espaces de noms de classes. Ça ne changera pas.
Martijn Pieters

4
Et je note que votre solution de contournement fait exactement ce que ma réponse indique déjà: créer une nouvelle portée (un lambda n'est pas différent ici de l'utilisation defpour créer une fonction).
Martijn Pieters

1
oui. Bien qu'il soit agréable d'avoir une réponse avec la solution de contournement en un coup d'œil, celui-ci énonce de manière incorrigible le comportement comme un bogue, quand il s'agit d'un effet secondaire de la façon dont le langage fonctionne (et par conséquent, ne sera pas changé)
jsbueno

Il s'agit d'un problème différent, qui n'est en fait pas un problème dans Python 3. Il se produit uniquement dans IPython lorsque vous l'appelez en mode incorporé en utilisant say python -c "import IPython;IPython.embed()". Exécutez IPython directement en utilisant say ipythonet le problème disparaîtra.
Riaz Rizvi

6

La réponse acceptée fournit d'excellentes informations, mais il semble y avoir quelques autres défauts ici - des différences entre la compréhension de 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)

2

C'est un bogue en Python. Les compréhensions sont annoncées comme étant équivalentes aux boucles for, mais ce n'est pas le cas 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 extérieur. Dans 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.


1

Puisque l'itérateur le plus externe est évalué dans la portée environnante, nous pouvons utiliser zipavec itertools.repeatpour transférer les dépendances dans 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 forboucles imbriquées dans la compréhension et inclure les dépendances dans l'itérable 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'],
        # ...
    ])]
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.