Qual é o propósito das classes internas do python?


Respostas:


85

Citado em http://www.geekinterview.com/question_details/64739 :

Vantagens da classe interna:

  • Agrupamento lógico de classes : se uma classe é útil para apenas uma outra classe, então é lógico incorporá-la nessa classe e manter as duas juntas. Aninhar essas "classes auxiliares" torna seu pacote mais simplificado.
  • Encapsulamento aumentado : Considere duas classes de nível superior A e B onde B precisa de acesso aos membros de A que de outra forma seriam declarados privados. Ao ocultar a classe B dentro da classe AA, os membros podem ser declarados privados e B pode acessá-los. Além disso, o próprio B pode ser escondido do mundo exterior.
  • Código mais legível e sustentável : aninhar pequenas classes nas classes de nível superior coloca o código mais perto de onde ele é usado.

A principal vantagem é a organização. Qualquer coisa que pode ser realizada com classes internas pode ser realizada sem elas.


50
O argumento do encapsulamento, é claro, não se aplica ao Python.
bobince,

30
O primeiro ponto também não se aplica ao Python. Você pode definir quantas classes quiser em um arquivo de módulo, mantendo-as juntas e a organização do pacote também não será afetada. O último ponto é muito subjetivo e não acredito que seja válido. Resumindo, não encontro nenhum argumento que dê suporte ao uso de classes internas em Python nesta resposta.
Chris Arndt

17
No entanto, essas SÃO as razões pelas quais as classes internas são usadas na programação. Você está apenas tentando derrubar uma resposta competitiva. A resposta que esse cara deu aqui é sólida.
Inversus de

16
@Inversus: Eu discordo. Isso não é uma resposta, é uma citação estendida da resposta de outra pessoa sobre uma linguagem diferente (a saber, Java). Votei negativamente e espero que outros façam o mesmo.
Kevin

4
Eu concordo com esta resposta e discordo das objeções. Embora as classes aninhadas não sejam as classes internas do Java, elas são úteis. O objetivo de uma classe aninhada é a organização. Efetivamente, você está colocando uma classe sob o namespace de outra. Quando faz sentido lógico fazer isso, isso é Pythônico: "Namespaces são uma ótima ideia buzinando - vamos fazer mais disso!". Por exemplo, considere uma DataLoaderclasse que pode lançar uma CacheMissexceção. Aninhar a exceção na classe principal DataLoader.CacheMisssignifica que você pode importar apenas, DataLoadermas ainda usar a exceção.
cbarrick de

50

Existe algo que não pode ser realizado sem eles?

Não. Eles são absolutamente equivalentes a definir a classe normalmente no nível superior e, em seguida, copiar uma referência a ela na classe externa.

Não acho que haja qualquer razão especial para as classes aninhadas serem 'permitidas', exceto que também não faz nenhum sentido específico 'proibi-las' explicitamente.

Se você está procurando por uma classe que existe dentro do ciclo de vida do objeto externo / proprietário e sempre tem uma referência a uma instância da classe externa - classes internas como o Java faz - então as classes aninhadas do Python não são isso. Mas você pode hackear algo assim :

import weakref, new

class innerclass(object):
    """Descriptor for making inner classes.

    Adds a property 'owner' to the inner class, pointing to the outer
    owner instance.
    """

    # Use a weakref dict to memoise previous results so that
    # instance.Inner() always returns the same inner classobj.
    #
    def __init__(self, inner):
        self.inner= inner
        self.instances= weakref.WeakKeyDictionary()

    # Not thread-safe - consider adding a lock.
    #
    def __get__(self, instance, _):
        if instance is None:
            return self.inner
        if instance not in self.instances:
            self.instances[instance]= new.classobj(
                self.inner.__name__, (self.inner,), {'owner': instance}
            )
        return self.instances[instance]


# Using an inner class
#
class Outer(object):
    @innerclass
    class Inner(object):
        def __repr__(self):
            return '<%s.%s inner object of %r>' % (
                self.owner.__class__.__name__,
                self.__class__.__name__,
                self.owner
            )

>>> o1= Outer()
>>> o2= Outer()
>>> i1= o1.Inner()
>>> i1
<Outer.Inner inner object of <__main__.Outer object at 0x7fb2cd62de90>>
>>> isinstance(i1, Outer.Inner)
True
>>> isinstance(i1, o1.Inner)
True
>>> isinstance(i1, o2.Inner)
False

(Isso usa decoradores de classe, que são novos no Python 2.6 e 3.0. Caso contrário, você teria que dizer “Inner = innerclass (Inner)” após a definição da classe.)


5
Os casos de uso que exigem isso (ou seja, classes internas do tipo Java, cujas instâncias têm um relacionamento com instâncias da classe externa) podem ser normalmente endereçados em Python definindo a classe interna dentro dos métodos da classe externa - eles verão o externo selfsem nenhum trabalho extra necessário (apenas use um identificador diferente onde você normalmente colocaria o interno self; como innerself), e será capaz de acessar a instância externa por meio dele.
Evgeni Sergeev

O uso de um WeakKeyDictionaryneste exemplo não permite que as chaves sejam coletadas como lixo, porque os valores fazem referência a suas respectivas chaves por meio de seus owneratributos.
Kritzefitz de

36

Há algo em que você precisa entender para ser capaz de entender isso. Na maioria das linguagens, as definições de classe são diretivas para o compilador. Ou seja, a classe é criada antes que o programa seja executado. Em python, todas as instruções são executáveis. Isso significa que esta declaração:

class foo(object):
    pass

é uma instrução que é executada em tempo de execução como esta:

x = y + z

Isso significa que você não só pode criar classes dentro de outras classes, como também pode criar classes em qualquer lugar que desejar. Considere este código:

def foo():
    class bar(object):
        ...
    z = bar()

Assim, a ideia de uma "classe interna" não é realmente uma construção de linguagem; é uma construção do programador. Guido tem um ótimo resumo de como isso aconteceu aqui . Mas, essencialmente, a ideia básica é que isso simplifica a gramática do idioma.


16

Aninhando classes dentro de classes:

  • As classes aninhadas aumentam a definição da classe, tornando mais difícil ver o que está acontecendo.

  • Classes aninhadas podem criar acoplamento que tornaria o teste mais difícil.

  • No Python, você pode colocar mais de uma classe em um arquivo / módulo, ao contrário do Java, então a classe ainda permanece perto da classe de nível superior e pode até ter o nome da classe prefixado com um "_" para ajudar a significar que outras não deveriam ser usando isso.

O lugar onde as classes aninhadas podem ser úteis é dentro das funções

def some_func(a, b, c):
   class SomeClass(a):
      def some_method(self):
         return b
   SomeClass.__doc__ = c
   return SomeClass

A classe captura os valores da função permitindo que você crie dinamicamente uma classe como a metaprogramação de modelo em C ++


7

Eu entendo os argumentos contra classes aninhadas, mas há um caso para usá-los em algumas ocasiões. Imagine que estou criando uma classe de lista duplamente vinculada e preciso criar uma classe de nó para manter os nós. Tenho duas opções, criar a classe Node dentro da classe DoublyLinkedList ou criar a classe Node fora da classe DoublyLinkedList. Prefiro a primeira escolha neste caso, porque a classe Node só faz sentido dentro da classe DoublyLinkedList. Embora não haja nenhum benefício de ocultação / encapsulamento, há um benefício de agrupamento em poder dizer que a classe Node faz parte da classe DoublyLinkedList.


5
Isso é verdade assumindo que a mesma Nodeclasse não é útil para outros tipos de classes de lista encadeada que você também pode criar, caso em que provavelmente deve ser apenas externa.
Acumenus,

Outra forma de colocar isso: Nodeestá sob o namespace de DoublyLinkedList, e faz sentido lógico ser assim. Isto é Pythônico: "Os namespaces são uma ótima ideia - vamos fazer mais disso!"
cbarrick de

@cbarrick: Fazer "mais desses" não diz nada sobre aninhamento.
Ethan Furman

3

Existe algo que não pode ser realizado sem eles? Se sim, o que é isso?

Há algo que não pode ser feito facilmente sem : herança de classes relacionadas .

Aqui está um exemplo minimalista com as classes relacionadas Ae B:

class A(object):
    class B(object):
        def __init__(self, parent):
            self.parent = parent

    def make_B(self):
        return self.B(self)


class AA(A):  # Inheritance
    class B(A.B):  # Inheritance, same class name
        pass

Este código leva a um comportamento bastante razoável e previsível:

>>> type(A().make_B())
<class '__main__.A.B'>
>>> type(A().make_B().parent)
<class '__main__.A'>
>>> type(AA().make_B())
<class '__main__.AA.B'>
>>> type(AA().make_B().parent)
<class '__main__.AA'>

Se Bfosse uma classe de nível superior, você não poderia escrever self.B()no método, make_Bmas simplesmente escreveria B(), e assim perderia a vinculação dinâmica para as classes adequadas.

Observe que, nesta construção, você nunca deve se referir à classe Ano corpo da classe B. Essa é a motivação para a introdução do parentatributo nas aulas B.

Obviamente, essa vinculação dinâmica pode ser recriada sem uma classe interna ao custo de uma instrumentação tediosa e sujeita a erros das classes.


1

O principal caso de uso para o qual uso isso é para evitar a proliferação de pequenos módulos e evitar a poluição do namespace quando módulos separados não são necessários. Se estou estendendo uma classe existente, mas essa classe existente deve fazer referência a outra subclasse que sempre deve ser acoplada a ela. Por exemplo, posso ter um utils.pymódulo que contém muitas classes auxiliares, que não são necessariamente acopladas, mas quero reforçar o acoplamento para algumas dessas classes auxiliares. Por exemplo, quando implemento https://stackoverflow.com/a/8274307/2718295

: utils.py:

import json, decimal

class Helper1(object):
    pass

class Helper2(object):
    pass

# Here is the notorious JSONEncoder extension to serialize Decimals to JSON floats
class DecimalJSONEncoder(json.JSONEncoder):

    class _repr_decimal(float): # Because float.__repr__ cannot be monkey patched
        def __init__(self, obj):
            self._obj = obj
        def __repr__(self):
            return '{:f}'.format(self._obj)

    def default(self, obj): # override JSONEncoder.default
        if isinstance(obj, decimal.Decimal):
            return self._repr_decimal(obj)
        # else
        super(self.__class__, self).default(obj)
        # could also have inherited from object and used return json.JSONEncoder.default(self, obj) 

Então nós podemos:

>>> from utils import DecimalJSONEncoder
>>> import json, decimal
>>> json.dumps({'key1': decimal.Decimal('1.12345678901234'), 
... 'key2':'strKey2Value'}, cls=DecimalJSONEncoder)
{"key2": "key2_value", "key_1": 1.12345678901234}

Claro, poderíamos ter evitado herdar json.JSONEnocdercompletamente e apenas substituir default ():

:

import decimal, json

class Helper1(object):
    pass

def json_encoder_decimal(obj):
    class _repr_decimal(float):
        ...

    if isinstance(obj, decimal.Decimal):
        return _repr_decimal(obj)

    return json.JSONEncoder(obj)


>>> json.dumps({'key1': decimal.Decimal('1.12345678901234')}, default=json_decimal_encoder)
'{"key1": 1.12345678901234}'

Mas às vezes, apenas por convenção, você deseja utilsser composto de classes para extensibilidade.

Aqui está outro caso de uso: eu quero uma fábrica para mutáveis ​​em minha OuterClass sem ter que invocar copy:

class OuterClass(object):

    class DTemplate(dict):
        def __init__(self):
            self.update({'key1': [1,2,3],
                'key2': {'subkey': [4,5,6]})


    def __init__(self):
        self.outerclass_dict = {
            'outerkey1': self.DTemplate(),
            'outerkey2': self.DTemplate()}



obj = OuterClass()
obj.outerclass_dict['outerkey1']['key2']['subkey'].append(4)
assert obj.outerclass_dict['outerkey2']['key2']['subkey'] == [4,5,6]

Prefiro esse padrão ao @staticmethodinvés do decorador que você usaria para uma função de fábrica.


1

1. Duas maneiras funcionalmente equivalentes

As duas maneiras mostradas antes são funcionalmente idênticas. No entanto, existem algumas diferenças sutis e existem situações em que você gostaria de escolher um em vez do outro.

Modo 1: definição de classe aninhada
(= "classe aninhada")

class MyOuter1:
    class Inner:
        def show(self, msg):
            print(msg)

Modo 2: com a classe interna de nível de módulo anexada à classe externa
(= "Classe interna referenciada")

class _InnerClass:
    def show(self, msg):
        print(msg)

class MyOuter2:
    Inner = _InnerClass

O sublinhado é usado para seguir o PEP8 "interfaces internas (pacotes, módulos, classes, funções, atributos ou outros nomes) devem - ser prefixados com um único sublinhado inicial."

2. Semelhanças

O trecho de código abaixo demonstra as semelhanças funcionais da "Classe aninhada" vs "Classe interna referenciada"; Eles se comportariam da mesma maneira na verificação de código para o tipo de uma instância de classe interna. Escusado será dizer que o m.inner.anymethod()comportaria de forma semelhante com m1em2

m1 = MyOuter1()
m2 = MyOuter2()

innercls1 = getattr(m1, 'Inner', None)
innercls2 = getattr(m2, 'Inner', None)

isinstance(innercls1(), MyOuter1.Inner)
# True

isinstance(innercls2(), MyOuter2.Inner)
# True

type(innercls1()) == mypackage.outer1.MyOuter1.Inner
# True (when part of mypackage)

type(innercls2()) == mypackage.outer2.MyOuter2.Inner
# True (when part of mypackage)

3. Diferenças

As diferenças de "Classe aninhada" e "Classe interna referenciada" estão listadas abaixo. Eles não são grandes, mas às vezes você gostaria de escolher um ou outro com base neles.

3.1 Encapsulamento de código

Com "Classes aninhadas" é possível encapsular o código melhor do que com "Classe interna referenciada". Uma classe no namespace do módulo é uma variável global . O objetivo das classes aninhadas é reduzir a confusão no módulo e colocar a classe interna dentro da classe externa.

Embora ninguém * esteja usando from packagename import *, uma quantidade baixa de variáveis ​​de nível de módulo pode ser boa, por exemplo, ao usar um IDE com autocompletar de código / intellisense.

* Certo?

3.2 Legibilidade do código

A documentação do Django instrui o uso da classe interna Meta para os metadados do modelo. É um pouco mais claro * instruir os usuários do framework a escrever um class Foo(models.Model)com o inner class Meta;

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

em vez de "escreva a class _Meta, depois escreva a class Foo(models.Model)com Meta = _Meta";

class _Meta:
    ordering = ["horn_length"]
    verbose_name_plural = "oxen"

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Com a abordagem "Classe aninhada", o código pode ser lido como uma lista de marcadores aninhados , mas com o método "Classe interna referenciada" é necessário rolar para cima para ver a definição de _Metapara ver seus "itens filho" (atributos).

  • O método "Classe interna referenciada" pode ser mais legível se o nível de aninhamento do código aumentar ou as linhas forem longas por algum outro motivo.

* Claro, uma questão de gosto

3.3 Mensagens de erro ligeiramente diferentes

Isso não é grande coisa, mas apenas para completar: ao acessar um atributo inexistente para a classe interna, vemos exceções ligeiramente diferentes. Continuando o exemplo dado na Seção 2:

innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'

innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'

Isso ocorre porque os types das classes internas são

type(innercls1())
#mypackage.outer1.MyOuter1.Inner

type(innercls2())
#mypackage.outer2._InnerClass

0

Eu usei as classes internas do Python para criar subclasses com erros deliberados dentro das funções de teste de unidade (ou seja, dentro def test_something():) a fim de chegar mais perto de 100% de cobertura de teste (por exemplo, o teste de declarações de log muito raramente acionadas substituindo alguns métodos).

Em retrospecto, é semelhante à resposta de Ed https://stackoverflow.com/a/722036/1101109

Essas classes internas devem sair do escopo e estar prontas para a coleta de lixo depois que todas as referências a elas forem removidas. Por exemplo, pegue o seguinte inner.pyarquivo:

class A(object):
    pass

def scope():
    class Buggy(A):
        """Do tests or something"""
    assert isinstance(Buggy(), A)

Recebo os seguintes resultados curiosos no OSX Python 2.7.6:

>>> from inner import A, scope
>>> A.__subclasses__()
[]
>>> scope()
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A, scope
>>> from inner import A
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A
>>> import gc
>>> gc.collect()
0
>>> gc.collect()  # Yes I needed to call the gc twice, seems reproducible
3
>>> from inner import A
>>> A.__subclasses__()
[]

Dica - Não tente fazer isso com modelos Django, que pareciam manter outras referências (em cache?) Para minhas classes com erros.

Portanto, em geral, eu não recomendaria usar classes internas para esse tipo de propósito, a menos que você realmente valorize essa cobertura de teste de 100% e não possa usar outros métodos. Embora eu ache que é bom estar ciente de que, se você usar o __subclasses__(), ele às vezes pode ser poluído por classes internas. De qualquer forma, se você seguiu até aqui, acho que estamos muito envolvidos com Python neste ponto, com dunderscores privados e tudo.


3
Isso não é tudo sobre subclasses e não classes internas? A
klaas

No caso acima, Buggy herda de A. Então, sublass mostra isso. Veja também a função interna issubclass ()
klaas

Obrigado @klaas, acho que poderia ficar mais claro que estou apenas usando .__subclasses__()para entender como as classes internas interagem com o coletor de lixo quando as coisas saem do escopo no Python. Isso parece dominar visualmente o post, então os primeiros 1-3 parágrafos merecem um pouco mais de expansão.
pzrq
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.