Escopo das classes aninhadas?

116

Estou tentando entender o escopo em classes aninhadas em Python. Aqui está meu código de exemplo:

class OuterClass:
    outer_var = 1
    class InnerClass:
        inner_var = outer_var

A criação da classe não é concluída e recebo o erro:

<type 'exceptions.NameError'>: name 'outer_var' is not defined

Tentar inner_var = Outerclass.outer_varnão funciona. Eu recebo:

<type 'exceptions.NameError'>: name 'OuterClass' is not defined

Estou tentando acessar a estática outer_varde InnerClass.

Existe uma maneira de fazer isso?

martineau
fonte

Respostas:

105
class Outer(object):
    outer_var = 1

    class Inner(object):
        @property
        def inner_var(self):
            return Outer.outer_var

Isso não é exatamente o mesmo que coisas semelhantes funcionam em outras linguagens e usa pesquisa global em vez de definir o escopo de acesso outer_var. (Se você mudar qual objeto o nomeOuter está vinculado, este código usará esse objeto na próxima vez que for executado.)

Se, em vez disso, você quiser que todos os Innerobjetos tenham uma referência a um Outerporque outer_varé realmente um atributo de instância:

class Outer(object):
    def __init__(self):
        self.outer_var = 1

    def get_inner(self):
        return self.Inner(self)
        # "self.Inner" is because Inner is a class attribute of this class
        # "Outer.Inner" would also work, or move Inner to global scope
        # and then just use "Inner"

    class Inner(object):
        def __init__(self, outer):
            self.outer = outer

        @property
        def inner_var(self):
            return self.outer.outer_var

Observe que o aninhamento de classes é um tanto incomum em Python e não implica automaticamente em qualquer tipo de relacionamento especial entre as classes. É melhor você não aninhar. (Você ainda pode definir um atributo de classe Outerpara Inner, se quiser.)

martineau
fonte
2
Pode ser útil adicionar com quais versões do Python sua resposta funcionará.
AJ.
1
Eu escrevi isso com 2.6 / 2.x em mente, mas, olhando para ele, não vejo nada que não funcione da mesma forma no 3.x.
Não entendo muito bem o que você quer dizer nesta parte, "(Se você alterar a qual objeto o nome Outer está vinculado, este código usará esse objeto na próxima vez em que for executado.)" Você pode me ajudar a entender?
batbrat
1
@batbrat significa que a referência a Outeré pesquisada novamente toda vez que você o faz Inner.inner_var. Portanto, se você religar o nome Outera um novo objeto, Inner.inner_varcomeçará a retornar esse novo objeto.
Felipe
45

Acho que você pode simplesmente fazer:

class OuterClass:
    outer_var = 1

    class InnerClass:
        pass
    InnerClass.inner_var = outer_var

O problema que você encontrou é devido a este:

Um bloco é uma parte do texto do programa Python executado como uma unidade. A seguir estão os blocos: um módulo, um corpo de função e uma definição de classe.
(...)
Um escopo define a visibilidade de um nome dentro de um bloco.
(...)
O escopo dos nomes definidos em um bloco de classe é limitado ao bloco de classe; não se estende aos blocos de código de métodos - isso inclui 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))

http://docs.python.org/reference/executionmodel.html#naming-and-binding

O acima significa:
um corpo de função é um bloco de código e um método é uma função, então os nomes definidos fora do corpo da função presente em uma definição de classe não se estendem ao corpo da função.

Parafraseando isso para o seu caso:
uma definição de classe é um bloco de código, então os nomes definidos fora da definição de classe interna presente em uma definição de classe externa não se estendem para a definição de classe interna.

Eyquem
fonte
2
Surpreendente. Seu exemplo falha, alegando que "o nome global 'a' não está definido". No entanto, a substituição de uma compreensão de lista [a + i for i in range(10)]vincula Ab à lista esperada [42..51].
George
6
@George Observe que o exemplo com classe A não é meu, é do documento oficial do Python cujo link eu forneci . Este exemplo falha e essa falha é o que se deseja mostrar neste exemplo. Na verdade list(a + i for i in range(10))é o list((a + i for i in range(10)))que quer dizer list(a_generator). Eles dizem que um gerador é implementado com um escopo semelhante ao escopo das funções.
eyquem de
@George Para mim, isso significa que as funções atuam de forma diferente se estiverem em um módulo ou em uma classe. No primeiro caso, uma função sai para encontrar o objeto vinculado a um identificador livre. No segundo caso, uma função, ou seja, um método, não sai de seu corpo. As funções em um módulo e os métodos em uma classe são, na realidade, dois tipos de objetos. Métodos não são apenas funções em sala de aula. Essa é minha ideia.
eyquem de
5
@George: FWIW, nem a list(...)chamada nem a compreensão funcionam no Python 3. A documentação para Py3 também é um pouco diferente refletindo isso. Agora diz: " O escopo dos nomes definidos em um bloco de classe é limitado ao bloco de classe; não se estende aos blocos de código de métodos - isso inclui compreensões e expressões geradoras, uma vez que são implementados usando um escopo de função. " (Ênfase minha )
martineau de
Estou curioso para saber por que list (Aa + i para i no intervalo (10)) também não funciona, onde fixei a por Aa acho que A pode ser um nome global.
gaoxinge
20

Você pode ficar melhor se simplesmente não usar classes aninhadas. Se você deve aninhar, tente isto:

x = 1
class OuterClass:
    outer_var = x
    class InnerClass:
        inner_var = x

Ou declare ambas as classes antes de aninhá-las:

class OuterClass:
    outer_var = 1

class InnerClass:
    inner_var = OuterClass.outer_var

OuterClass.InnerClass = InnerClass

(Depois disso, você pode del InnerClassse precisar.)

Jason Orendorff
fonte
3

Solução mais fácil:

class OuterClass:
    outer_var = 1
    class InnerClass:
        def __init__(self):
            self.inner_var = OuterClass.outer_var

Exige que você seja explícito, mas não exige muito esforço.

Cristian Garcia
fonte
NameError: o nome 'OuterClass' não está definido - -1
Mr_and_Mrs_D
3
O objetivo é acessá-lo do escopo da classe Outer - mais um erro semelhante ocorrerá se você chamar isso de um escopo estático dentro do Outer. Eu sugiro que você exclua esta postagem
Mr_and_Mrs_D
1

Em Python, objetos mutáveis ​​são passados ​​como referência, portanto, você pode passar uma referência da classe externa para a classe interna.

class OuterClass:
    def __init__(self):
        self.outer_var = 1
        self.inner_class = OuterClass.InnerClass(self)
        print('Inner variable in OuterClass = %d' % self.inner_class.inner_var)

    class InnerClass:
        def __init__(self, outer_class):
            self.outer_class = outer_class
            self.inner_var = 2
            print('Outer variable in InnerClass = %d' % self.outer_class.outer_var)
T. Alpers
fonte
2
Observe que você tem um ciclo de referência aqui e, em alguns cenários, a instância dessa classe não será liberada. Um exemplo, com cPython, se você tiver definido o __del__ método, o coletor de lixo não será capaz de lidar com o ciclo de referência e os objetos entrarão gc.garbage. O código acima, como está, não é problemático. A maneira de lidar com isso é usando uma referência fraca . Você pode ler a documentação sobre fracaref (2.7) ou fracaref (3.5)
guyarad 01 de
0

Todas as explicações podem ser encontradas na documentação do Python. O tutorial do Python

Por seu primeiro erro <type 'exceptions.NameError'>: name 'outer_var' is not defined. A explicação é:

Não há abreviatura para referenciar atributos de dados (ou outros métodos!) De dentro dos métodos. Acho que isso realmente aumenta a legibilidade dos métodos: não há chance de confundir variáveis ​​locais e variáveis ​​de instância ao percorrer um método.

citado em The Python Tutorial 9.4

Para o seu segundo erro <type 'exceptions.NameError'>: name 'OuterClass' is not defined

Quando uma definição de classe é deixada normalmente (por meio do final), um objeto de classe é criado.

citado em The Python Tutorial 9.3.1

Então, quando você tenta inner_var = Outerclass.outer_var, o Quterclassainda não foi criado, é por issoname 'OuterClass' is not defined

Uma explicação mais detalhada, mas entediante, para seu primeiro erro:

Embora as classes tenham acesso aos escopos de funções de fechamento, elas não agem como escopos de fechamento para código aninhado dentro da classe: Python pesquisa funções de fechamento para nomes referenciados, mas nunca qualquer classe de fechamento. Ou seja, uma classe é um escopo local e tem acesso a escopos locais incluídos, mas não serve como um escopo local fechado para código aninhado adicional.

citado em Learning.Python (5º) .Mark.Lutz

Andy Guo
fonte