Acessando variáveis ​​de classe a partir de uma compreensão de lista na definição de classe

174

Como você acessa outras variáveis ​​de classe a partir de uma compreensão de lista na definição de classe? O seguinte funciona no Python 2, mas falha no Python 3:

class Foo:
    x = 5
    y = [x for i in range(1)]

O Python 3.2 fornece o erro:

NameError: global name 'x' is not defined

Tentar Foo.xtambém não funciona. Alguma idéia de como fazer isso no Python 3?

Um exemplo motivador um pouco mais complicado:

from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

Neste exemplo, apply()teria sido uma solução decente, mas infelizmente foi removida do Python 3.

Mark Lodato
fonte
Sua mensagem de erro está incorreta. Eu entro NameError: global name 'x' is not definedem Python 3.2 e 3.3, que é o que eu esperaria.
Martijn Pieters
Interessante ... Uma solução óbvia é atribuir y depois que você sai da definição de classe. Foo.y = [Foo.x para i no intervalo (1)]
gps
3
O link + martijn-pieters está correto, há um comentário de + matt-b com a explicação: as compreensões de lista do Python 2.7 não têm seu próprio espaço para nome (ao contrário das compreensões de conjunto ou ditado ou expressões de gerador ... substitua seu [ ] com {} para ver isso em ação). Todos eles têm seu próprio espaço para nome em 3.
gps
@ gps: ou use um escopo aninhado, inserindo uma função (temporária) no conjunto de definições de classe.
Martijn Pieters
Acabei de testar no 2.7.11. Obtido o erro de nome
Junchao Gu

Respostas:

244

Escopo de classe e compreensão de lista, conjunto ou dicionário, bem como expressões geradoras, não se misturam.

O porquê; ou, a palavra oficial neste

No Python 3, as compreensões de lista receberam um escopo próprio (espaço de nome local) próprio, para impedir que suas variáveis ​​locais se espalhem pelo escopo ao redor (consulte Compreensão de lista do Python para religar nomes mesmo após o escopo de compreensão. Isso está certo? ). Isso é ótimo quando se usa essa compreensão de lista em um módulo ou em uma função, mas nas classes o escopo é um pouco, uhm, estranho .

Isso está documentado no pep 227 :

Os nomes no escopo da classe não estão acessíveis. Os nomes são resolvidos no escopo da função mais interna. Se uma definição de classe ocorrer em uma cadeia de escopos aninhados, o processo de resolução ignorará as definições de classe.

e na classdocumentação da declaração composta :

O conjunto da classe é então executado em um novo quadro de execução (consulte a seção Nomeando e vinculando ), usando um espaço para nome local recém-criado e o espaço para nome global original. (Geralmente, o conjunto contém apenas definições de funções.) Quando o conjunto da classe termina a execução, seu quadro de execução é descartado, mas seu espaço de nomes local é salvo . [4] Um objeto de classe é criado usando a lista de herança para as classes base e o espaço de nomes local salvo para o dicionário de atributo.

Ênfase minha; o quadro de execução é o escopo temporário.

Como o escopo é redirecionado como os atributos em um objeto de classe, permitir que ele seja usado como um escopo não local também leva a um comportamento indefinido; o que aconteceria se um método de classe referido xcomo variável de escopo aninhado também manipulasse Foo.x, por exemplo? Mais importante, o que isso significaria para as subclasses de Foo? O Python precisa tratar um escopo de classe de maneira diferente, pois é muito diferente de um escopo de função.

Por fim, mas definitivamente não menos importante, a seção Nomeação e ligação vinculada na documentação do modelo de Execução menciona explicitamente os escopos de classe:

O escopo dos nomes definidos em um bloco de classes é limitado ao bloco de classes; não se estende aos blocos de código dos métodos - isso inclui compreensões e expressões geradoras, pois são implementadas usando um escopo de função. Isso significa que o seguinte falhará:

class A:
     a = 42
     b = list(a + i for i in range(10))

Portanto, para resumir: você não pode acessar o escopo da classe a partir de funções, compreensões de lista ou expressões geradoras incluídas nesse escopo; eles agem como se esse escopo não existisse. No Python 2, as compreensões da lista foram implementadas usando um atalho, mas no Python 3 elas têm seu próprio escopo de função (como deveriam ter sido o tempo todo) e, portanto, seu exemplo é interrompido. Outros tipos de compreensão têm seu próprio escopo, independentemente da versão do Python; portanto, um exemplo semelhante com uma compreensão de conjunto ou de dict seria interrompido no Python 2.

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

A (pequena) exceção; ou por que uma parte ainda pode funcionar

Há uma parte de uma expressão de compreensão ou gerador que é executada no escopo circundante, independentemente da versão do Python. Essa seria a expressão para o iterável mais externo. No seu exemplo, é o range(1):

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

Portanto, o uso xdessa expressão não geraria um erro:

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

Isso se aplica apenas ao iterável mais externo; se uma compreensão tiver várias forcláusulas, os iteráveis ​​para forcláusulas internas serão avaliados no escopo da compreensão:

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

Essa decisão de design foi tomada para gerar um erro no momento da criação do genexp, em vez do tempo da iteração ao criar o iterável mais externo de uma expressão de gerador gera um erro ou quando o iterável mais externo acaba não sendo iterável. As compreensões compartilham esse comportamento para obter consistência.

Olhando sob o capô; ou, muito mais detalhes do que você sempre desejou

Você pode ver tudo isso em ação usando o dismódulo . Estou usando o Python 3.3 nos exemplos a seguir, porque ele adiciona nomes qualificados que identificam os objetos de código que queremos inspecionar. O bytecode produzido é funcionalmente idêntico ao Python 3.2.

Para criar uma classe, o Python pega essencialmente todo o conjunto que compõe o corpo da classe (então tudo recuou um nível mais do que a class <name>:linha) e executa isso como se fosse uma função:

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

O primeiro LOAD_CONSTcarrega um objeto de código para o Foocorpo da classe, transforma isso em uma função e o chama. O resultado dessa chamada é então usado para criar o espaço para nome da classe, its __dict__. Por enquanto, tudo bem.

O que deve ser observado aqui é que o bytecode contém um objeto de código aninhado; no Python, definições de classe, funções, compreensões e geradores são todos representados como objetos de código que contêm não apenas bytecode, mas também estruturas que representam variáveis ​​locais, constantes, variáveis ​​extraídas de globais e variáveis ​​extraídas do escopo aninhado. O bytecode compilado se refere a essas estruturas e o intérprete python sabe como acessar os dados fornecidos pelos bytecodes apresentados.

O importante a lembrar aqui é que o Python cria essas estruturas em tempo de compilação; o classconjunto é um objeto de código ( <code object Foo at 0x10a436030, file "<stdin>", line 2>) que já está compilado.

Vamos inspecionar o objeto de código que cria o próprio corpo da classe; objetos de código têm uma co_constsestrutura:

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

O bytecode acima cria o corpo da classe. A função é executada e o locals()namespace resultante , contendo xe yé usado para criar a classe (exceto que ela não funciona porque xnão está definida como global). Note que depois de armazenar 5em x, ele carrega um outro objeto de código; essa é a compreensão da lista; está envolto em um objeto de função exatamente como o corpo da classe; a função criada recebe um argumento posicional, o range(1)iterável a ser usado para seu código de loop, convertido em um iterador. Conforme mostrado no bytecode, range(1)é avaliado no escopo da classe.

A partir disso, você pode ver que a única diferença entre um objeto de código para uma função ou gerador e um objeto de código para uma compreensão é que o último é executado imediatamente quando o objeto de código pai é executado; o bytecode simplesmente cria uma função rapidamente e a executa em algumas pequenas etapas.

Em vez disso, o Python 2.x usa o bytecode embutido, aqui está o resultado do 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        

Nenhum objeto de código é carregado, em vez disso, um FOR_ITERloop é executado em linha. Portanto, no Python 3.x, o gerador de listas recebeu um objeto de código próprio, o que significa que ele tem seu próprio escopo.

No entanto, a compreensão foi compilada juntamente com o restante do código-fonte python quando o módulo ou script foi carregado pela primeira vez pelo intérprete, e o compilador não considera um conjunto de classes um escopo válido. Quaisquer variáveis ​​referenciadas em uma compreensão de lista devem procurar no escopo em torno da definição de classe, recursivamente. Se a variável não foi encontrada pelo compilador, ela a marca como global. A desmontagem do objeto de código de compreensão da lista mostra que xé realmente carregado como um 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         

Esse pedaço de bytecode carrega o primeiro argumento passado (o range(1)iterador) e, assim como a versão do Python 2.x usa FOR_ITERpara fazer um loop sobre ele e criar sua saída.

Se tivéssemos definido xna foofunção, xseria uma variável de célula (as células se referem a escopos aninhados):

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

O LOAD_DEREFcarregamento indireto xdos objetos da célula do objeto de código:

>>> 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]

A referência real procura o valor das estruturas de dados do quadro atual, que foram inicializadas a partir do .__closure__atributo de um objeto de função . Como a função criada para o objeto do código de compreensão é descartada novamente, não conseguimos inspecionar o fechamento dessa função. Para ver um fechamento em ação, teríamos que inspecionar uma função aninhada:

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

Então, para resumir:

  • As compreensões de lista obtêm seus próprios objetos de código no Python 3 e não há diferença entre os objetos de código para funções, geradores ou compreensões; objetos de código de compreensão são agrupados em um objeto de função temporário e chamados imediatamente.
  • Os objetos de código são criados em tempo de compilação e as variáveis ​​não locais são marcadas como variáveis ​​globais ou livres, com base nos escopos aninhados do código. O corpo da classe não é considerado um escopo para procurar essas variáveis.
  • Ao executar o código, o Python precisa apenas examinar os globais, ou o fechamento do objeto em execução no momento. Como o compilador não incluiu o corpo da classe como um escopo, o espaço para nome da função temporária não é considerado.

Uma solução alternativa; ou, o que fazer sobre isso

Se você criar um escopo explícito para a xvariável, como em uma função, poderá usar variáveis ​​de escopo de classe para entender a lista:

>>> class Foo:
...     x = 5
...     def y(x):
...         return [x for i in range(1)]
...     y = y(x)
... 
>>> Foo.y
[5]

A yfunção 'temporária' pode ser chamada diretamente; nós o substituímos quando o fazemos com seu valor de retorno. Seu escopo é considerado ao resolver 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',)

Obviamente, as pessoas que leem o seu código vão pensar um pouco sobre isso; convém colocar um grande comentário explicando por que você está fazendo isso.

A melhor solução é usar apenas __init__para criar uma variável de instância:

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

e evite todo o esforço e perguntas para se explicar. Para seu próprio exemplo concreto, eu nem armazenaria o material namedtuplena classe; use a saída diretamente (não armazene a classe gerada) ou use um global:

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

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]
Martijn Pieters
fonte
21
Você também pode usar um lambda para corrigir a ligação:y = (lambda x=x: [x for i in range(1)])()
ecatmur
3
@ecatmur: Exatamente, lambdasão apenas funções anônimas, afinal.
Martijn Pieters
2
Para o registro, a solução alternativa que usa um argumento padrão (para um lambda ou uma função) para passar na variável de classe tem uma pegadinha. Ou seja, ele passa o valor atual da variável. Portanto, se a variável for alterada posteriormente e a função ou lambda for chamada, a função ou lambda usará o valor antigo. Esse comportamento difere do comportamento de um fechamento (que capturaria uma referência à variável, em vez de seu valor), portanto, pode ser inesperado.
Neal Young
9
Se exigir uma página de informações técnicas para explicar por que algo não funciona intuitivamente, chamo isso de bug.
Jonathan
5
@ JonathanLeaders: Não chame isso de bug , chame de troca . Se você deseja A e B, mas pode obter apenas um deles, não importa como decida, em algumas situações você não gostará do resultado. Isso é vida.
Lutz Prechelt 23/11
15

Na minha opinião, é uma falha no Python 3. Espero que eles mudem.

Old Way (funciona em 2.7, joga NameError: name 'x' is not definedem 3 ou mais):

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

OBSERVAÇÃO: o simples escopo com A.xele não resolveria

Nova maneira (funciona em mais de 3):

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

Como a sintaxe é tão feia, eu apenas inicializo todas as minhas variáveis ​​de classe no construtor.

Jonathan
fonte
6
O problema também está presente no Python 2, ao usar expressões geradoras, bem como na compreensão de conjuntos e dicionários. Não é um bug, é uma consequência de como os namespaces de classe funcionam. Isso não vai mudar.
Martijn Pieters
4
E observo que sua solução alternativa faz exatamente o que minha resposta já indica: crie um novo escopo (um lambda não é diferente aqui de usar defpara criar uma função).
Martijn Pieters
1
Sim. Embora seja bom ter uma resposta rápida à solução, essa afirma incorretamente o comportamento como um bug, quando é um efeito colateral do modo como a linguagem funciona (e, portanto, não será alterada)
jsbueno
Esse é um problema diferente, que na verdade não é um problema no Python 3. Ele só ocorre no IPython quando você o chama no modo de incorporação usando o say python -c "import IPython;IPython.embed()". Execute o IPython diretamente usando o say ipythone o problema desaparecerá.
Riaz Rizvi
6

A resposta aceita fornece informações excelentes, mas parece haver algumas outras rugas aqui - diferenças entre a compreensão da lista e as expressões geradoras. Uma demonstração com a qual brinquei:

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)
FMc
fonte
2

Este é um erro no Python. As compreensões são anunciadas como equivalentes a loops, mas isso não é verdade nas classes. Pelo menos até Python 3.6.6, em uma compreensão usada em uma classe, apenas uma variável de fora da compreensão é acessível dentro da compreensão e deve ser usada como o iterador mais externo. Em uma função, essa limitação de escopo não se aplica.

Para ilustrar por que isso é um erro, vamos retornar ao exemplo original. Isso falha:

class Foo:
    x = 5
    y = [x for i in range(1)]

Mas isso funciona:

def Foo():
    x = 5
    y = [x for i in range(1)]

A limitação é declarada no final desta seção no guia de referência.

bzip2
fonte
1

Como o iterador mais externo é avaliado no escopo circundante, podemos usá-lo em zipconjunto itertools.repeatpara transportar as dependências para o escopo da compreensão:

import itertools as it

class Foo:
    x = 5
    y = [j for i, j in zip(range(3), it.repeat(x))]

Também é possível usar forloops aninhados na compreensão e incluir as dependências no iterável mais externo:

class Foo:
    x = 5
    y = [j for j in (x,) for i in range(3)]

Para o exemplo específico do 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'],
        # ...
    ])]
um convidado
fonte