As classes internas / aninhadas do Python me confundem. Existe algo que não pode ser realizado sem eles? Se sim, o que é isso?
As classes internas / aninhadas do Python me confundem. Existe algo que não pode ser realizado sem eles? Se sim, o que é isso?
Respostas:
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.
DataLoader
classe que pode lançar uma CacheMiss
exceção. Aninhar a exceção na classe principal DataLoader.CacheMiss
significa que você pode importar apenas, DataLoader
mas ainda usar a exceção.
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.)
self
sem 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.
WeakKeyDictionary
neste 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 owner
atributos.
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.
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 ++
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.
Node
classe 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.
Node
está 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!"
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 A
e 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 B
fosse uma classe de nível superior, você não poderia escrever self.B()
no método, make_B
mas 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 A
no corpo da classe B
. Essa é a motivação para a introdução do parent
atributo 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.
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.py
mó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.JSONEnocder
completamente 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 utils
ser 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 @staticmethod
invés do decorador que você usaria para uma função de fábrica.
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."
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 m1
em2
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)
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.
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?
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 _Meta
para 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
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 type
s das classes internas são
type(innercls1())
#mypackage.outer1.MyOuter1.Inner
type(innercls2())
#mypackage.outer2._InnerClass
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.py
arquivo:
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.
.__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.