web-dev-qa-db-fra.com

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

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.

147
Mark Lodato

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.

Le pourquoi; ou, le mot officiel sur ce

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)}

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

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.

Regarder sous le capot; ou bien plus de détails que jamais

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:

  • Les compréhensions de liste obtiennent leurs propres objets de code dans Python 3, et il n'y a pas de 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 de 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 étendues imbriquées du code. Le corps de classe n'est pas 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. Comme le compilateur n'a pas inclus le corps de 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 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'),
       # ...
    ]]
198
Martijn Pieters

À 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

13
Jonathan

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)
5
FMc

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.

1
bzip2

É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'],
        # ...
    ])]
0
a_guest