[…] precisávamos de toda uma teoria melhor sobre herança (e ainda precisamos). Por exemplo, herança e instanciação (que é um tipo de herança) embaralham tanto a pragmática (tal como fatorar o código para economizar espaço) quanto a semântica (usada para um excesso de tarefas tais como: especialização, generalização, especiação, etc.).[1]
Os Primórdios do Smalltalk
Esse capítulo é sobre herança e criação de subclasses. Vou presumir um entendimento básico desses conceitos, que você pode ter aprendido lendo O Tutorial do Python, ou por experiências com outra linguagem orientada a objetos popular, tal como Java, C# ou C++. Aqui vamos nos concentrar em quatro características do Python:
-
A função
super()
-
As armadilhas na criação de subclasses de tipos embutidos
-
Herança múltipla e a ordem de resolução de métodos
-
Classes mixin
Herança múltipla acontece quando uma classe tem mais de uma classe base. O C++ a suporta; Java e C# não. Muitos consideram que a herança múltipla não vale a quantidade de problemas que causa. Ela foi deliberadamente deixada de fora do Java, após seu aparente abuso nas primeiras bases de código C++.
Esse capítulo introduz a herança múltipla para aqueles que nunca a usaram, e oferece orientações sobre como lidar com herança simples ou múltipla, se você precisar usá-la.
Em 2021, quando escrevo essas linhas, há uma forte reação contra o uso excessivo de herança em geral—não apenas herança múltipla—porque superclasses e subclasses são fortemente acopladas, ou seja, interdependentes. Esse acoplamento forte significa que modificações em uma classe pode ter efeitos inesperados e de longo alcance em suas subclasses, tornando os sistemas frágeis e difíceis de entender.
Entretanto, ainda temos que manter os sistemas existentes, que podem ter complexas hierarquias de classe, ou trabalhar com frameworks que nos obrigam a usar herança—algumas vezes até herança múltipla.
Vou ilustrar as aplicações práticas da herança múltipla com a biblioteca padrão, o framework para programação web Django e o toolkit para programação de interface gráfica Tkinter.
Não há nenhum recurso novo no Python no que diz respeito ao assunto desse capítulo, mas fiz inúmeras modificações baseadas nos comentários dos revisores técnicos da segunda edição, especialmente Leonardo Rochael e Caleb Hattingh.
Escrevi uma nova seção de abertura, tratando especificamente da função embutida super()
, e mudei os exemplos na Herança múltipla e a Ordem de Resolução de Métodos, para explorar mais profundamente a forma como super()
suporta a herança múltipla cooperativa.
A Classes mixin também é nova. A Herança múltipla no mundo real foi reorganizada, e apresenta exemplos mais simples de mixin vindos da bilbioteca padrão, antes de apresentar o exemplos com o framework Django e as complicadas hierarquias do Tkinter.
Como o próprio título sugere, as ressalvas à herança sempre foram um dos temas principais desse capítulo. Mas como cada vez mais desenvolvedores consideram essa técnica problemática, acrescentei alguns parágrafos sobre como evitar a herança no final da Resumo do capítulo e da Leitura complementar.
Vamos começar com uma revisão da enigmática função super()
.
O uso consistente da função embutida super()
é essencial na criação de programas Python orientados a objetos fáceis de manter.
Quando uma subclasse sobrepõe um método de uma superclasse, esse novo método normalmente precisa invocar o método correspondente na superclasse. Aqui está o modo recomendado de fazer isso, tirado de um exemplo da documentação do módulo collections, na seção "OrderedDict Examples and Recipes" (OrderedDict: Exemplos e Receitas) (EN).:[2]
class LastUpdatedOrderedDict(OrderedDict):
"""Armazena itens mantendo por ordem de atualização."""
def __setitem__(self, key, value):
super().__setitem__(key, value)
self.move_to_end(key)
Para executar sua tarefa, LastUpdatedOrderedDict
sobrepõe __setitem__
para:
-
Usar
super().__setitem__
, invocando aquele método na superclasse e permitindo que ele insira ou atualize o par chave/valor. -
Invocar
self.move_to_end
, para garantir que akey
atualizada esteja na última posição.
Invocar um __init__
sobreposto é particulamente importante, para permitir que a superclasse execute sua parte na inicialização da instância.
Tip
|
Se você aprendeu programação orientada a objetos com Java, com certeza se lembra que, naquela linguagem, um método construtor invoca automaticamente o construtor sem argumentos da superclasse. O Python não faz isso. Se acostume a escrever o seguinte código padrão: def __init__(self, a, b) :
super().__init__(a, b)
... # more initialization code |
Você pode já ter visto código que não usa super()
, e em vez disso chama o método na superclasse diretamente, assim:
class NotRecommended(OrderedDict):
"""Isto é um contra-exemplo!"""
def __setitem__(self, key, value):
OrderedDict.__setitem__(self, key, value)
self.move_to_end(key)
Essa alternativa até funciona nesse caso em particular, mas não é recomendado por duas razões.
Primeiro, codifica a superclasse explicitamente.
O nome OrderedDict
aparece na declaração class
e também dentro de __setitem__
.
Se, no futuro, alguém modificar a declaração class
para mudar a classe base ou adicionar outra, pode se esquecer de atualizar o corpo de __setitem__
, introduzindo um bug.
A segunda razão é que super
implementa lógica para tratar hierarquias de classe com herança múltipla.
Voltaremos a isso na Herança múltipla e a Ordem de Resolução de Métodos.
Para concluir essa recapitulação de super
, é útil rever como essa função era invocada no Python 2, porque a assinatura antiga, com dois argumentos, é reveladora:
class LastUpdatedOrderedDict(OrderedDict):
"""Funciona igual em Python 2 e Python 3"""
def __setitem__(self, key, value):
super(LastUpdatedOrderedDict, self).__setitem__(key, value)
self.move_to_end(key)
Os dois argumento de super
são agora opcionais.
O compilador de bytecode do Python 3 obtém e fornece ambos examinando o contexto circundante, quando super()
é invocado dentro de um método.
Os argumentos são:
type
-
O início do caminho para a superclasse que implementa o método desejado. Por default, é a classe que possui o método onde a chamada a
super()
aparece. object_or_type
-
O objeto (para chamadas a métodos de instância) ou classe (para chamadas a métodos de classe) que será o receptor da chamada ao método.[3] Por default, é
self
se a chamadasuper()
acontece no corpo de um método de instância.
Independente desses argumentos serem fornecidos por você ou pelo compilador, a chamada a super()
devolve um objeto proxy dinâmico que encontra um método (tal como __setitem__
no exemplo)
em uma superclasse do parâmetro type
e a vincula ao object_or_type
,
de modo que não precisamos passar explicitamente o receptor (self
) quando invocamos o método.
No Python 3, ainda é permitido passar explicitamente o primeiro e o segundo argumentos a super()
.[4] Mas eles são necessários apenas em casos especiais, tal como pular parte do MRO (sigla de Method Resolution Order—Ordem de Resolução de Métodos), para testes ou depuração, ou para contornar algum comportamento indesejado em uma superclasse.
Vamos agora discutir as ressalvas à criação de subclasses de tipos embutidos.
Nas primeiras versões do Python não era possível criar subclasses de tipos embutidos como list
ou dict
.
Desde o Python 2.2 isso é possível, mas há restrição importante:
o código (escrito em C) dos tipos embutidos normalmente não chama os métodos sobrepostos por classes definidas pelo usuário.
Há uma boa descrição curta do problema na documentação do PyPy, na seção "Differences between PyPy and CPython" ("Diferenças entre o PyPy e o CPython"),
"Subclasses of built-in types" (Subclasses de tipos embutidos):
Oficialmente, o CPython não tem qualquer regra sobre exatamente quando um método sobreposto de subclasses de tipos embutidos é ou não invocado implicitamente. Como uma aproximação, esses métodos nunca são chamados por outros métodos embutidos do mesmo objeto. Por exemplo, um
__getitem__
sobreposto em uma subclasse dedict
nunca será invocado pelo métodoget()
do tipo embutido.
O Exemplo 1 ilustra o problema.
__setitem__
é ignorado pelos métodos __init__
e __update__
to tipo embutido dict
>>> class DoppelDict(dict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2) # (1)
...
>>> dd = DoppelDict(one=1) # (2)
>>> dd
{'one': 1}
>>> dd['two'] = 2 # (3)
>>> dd
{'one': 1, 'two': [2, 2]}
>>> dd.update(three=3) # (4)
>>> dd
{'three': 3, 'one': 1, 'two': [2, 2]}
-
DoppelDict.__setitem__
duplica os valores ao armazená-los (por nenhuma razão em especial, apenas para termos um efeito visível). Ele funciona delegando para a superclasse. -
O método
__init__
, herdado dedict
, claramente ignora que__setitem__
foi sobreposto: o valor de'one'
não foi duplicado. -
O operador
[]
chama nosso__setitem__
e funciona como esperado:'two'
está mapeado para o valor duplicado[2, 2]
. -
O método
update
dedict
também não usa nossa versão de__setitem__
: o valor de'three'
não foi duplicado.
Esse comportamento dos tipos embutidos é uma violação de uma regra básica da programação orientada a objetos:
a busca por métodos deveria sempre começar pela classe do receptor (self
), mesmo quando a chamada ocorre dentro de um método implementado na superclasse.
Isso é o que se chama "vinculação tardia" ("late binding"), que Alan Kay—um dos criadores do Smalltalk—considera ser uma característica básica da programação orientada a objetos:
em qualquer chamada na forma x.method()
, o método exato a ser chamado deve ser determinado durante a execução, baseado na classe do receptor x
.[5]
Este triste estado de coisas contribui para os problemas que vimos na [inconsistent_missing].
O problema não está limitado a chamadas dentro de uma instância—saber se self.get()
chama
self.getitem()
—mas também acontece com métodos sobrepostos de outras classes que deveriam ser chamados por métodos embutidos.
O Exemplo 2 foi adaptado da documentação do PyPy (EN).
__getitem__
de AnswerDict
é ignorado por dict.update
>>> class AnswerDict(dict):
... def __getitem__(self, key): # (1)
... return 42
...
>>> ad = AnswerDict(a='foo') # (2)
>>> ad['a'] # (3)
42
>>> d = {}
>>> d.update(ad) # (4)
>>> d['a'] # (5)
'foo'
>>> d
{'a': 'foo'}
-
AnswerDict.__getitem__
sempre devolve42
, independente da chave. -
ad
é umAnswerDict
carregado com o par chave-valor('a', 'foo')
. -
ad['a']
devolve42
, como esperado. -
d
é uma instância direta dedict
, que atualizamos comad
. -
O método
dict.update
ignora nossoAnswerDict.__getitem__
.
Warning
|
Criar subclasses diretamente de tipos embutidos como |
Se você criar uma subclasse de collections.UserDict
em vez de dict
, os problemas expostos no Exemplo 1 e no Exemplo 2 desaparecem. Veja o Exemplo 3.
DoppelDict2
and AnswerDict2
funcionam como esperado, porque estendem UserDict
e não dict
>>> import collections
>>>
>>> class DoppelDict2(collections.UserDict):
... def __setitem__(self, key, value):
... super().__setitem__(key, [value] * 2)
...
>>> dd = DoppelDict2(one=1)
>>> dd
{'one': [1, 1]}
>>> dd['two'] = 2
>>> dd
{'two': [2, 2], 'one': [1, 1]}
>>> dd.update(three=3)
>>> dd
{'two': [2, 2], 'three': [3, 3], 'one': [1, 1]}
>>>
>>> class AnswerDict2(collections.UserDict):
... def __getitem__(self, key):
... return 42
...
>>> ad = AnswerDict2(a='foo')
>>> ad['a']
42
>>> d = {}
>>> d.update(ad)
>>> d['a']
42
>>> d
{'a': 42}
Como um experimento, para medir o trabalho extra necessário para criar uma subclasse de um tipo embutido, reescrevi a classe StrKeyDict
do [ex_strkeydict], para torná-la uma subclasse de dict
em vez de UserDict
. Para fazê-la passar pelo mesmo banco de testes, tive que implementar
__init__
, get
, e update
, pois as versões herdadas de dict
se recusaram a cooperar com os métodos sobrepostos __missing__
, __contains__
e __setitem__
. A subclasse de UserDict
no [ex_strkeydict] tem 16 linhas, enquanto a subclasse experimental de dict
acabou com 33 linhas.[6]
Para deixar claro: essa seção tratou de um problema que se aplica apenas à delegação a métodos dentro do código em C dos tipos embutidos, e afeta apenas classes derivadas diretamente daqueles tipos.
Se você criar uma subclasse de uma classe escrita em Python, tal como UserDict
ou MutableMapping
, não vai encontrar esse problema.[7]
Vamos agora examinar uma questão que aparece na herança múltipla:
se uma classe tem duas superclasses, como o Python decide qual atributo usar quando invocamos super().attr
, mas ambas as superclasses tem um atributo com esse nome?
Qualquer linguagem que implemente herança múltipla precisa lidar com o potencial conflito de nomes, quando superclasses contêm métodos com nomes iguais. Isso é chamado "o problema do diamante", ilustrado na Figura 1 e no Exemplo 4.
leaf1.ping()
. Direita: Sequência de ativação para a chamada leaf1.pong()
.link:code/14-inheritance/diamond.py[role=include]
-
Root
forneceping
,pong
, e__repr__
(para facilitar a leitura da saída). -
Os métodos
ping
epong
na classeA
chamamsuper()
. -
Apenas o método
ping
na classeB
chamasuper()
. -
A classe
Leaf
implementa apenasping
, e chamasuper()
.
Vejamos agora o efeito da invocação dos métodos ping
e pong
em uma instância de Leaf
(Exemplo 5).
ping
e pong
em um objeto Leaf
link:code/14-inheritance/diamond.py[role=include]
-
leaf1
é uma instância deLeaf
. -
Chamar
leaf1.ping()
ativa os métodosping
emLeaf
,A
,B
, eRoot
, porque os métodosping
nas três primeiras classes chamamsuper().ping()
. -
Chamar
leaf1.pong()
ativapong
emA
através da herança, que por sua vez chamasuper.pong()
, ativandoB.pong
.
As sequências de ativação que aparecem no Exemplo 5 e na Figura 1 são determinadas por dois fatores:
-
A ordem de resolução de métodos da classe
Leaf
. -
O uso de
super()
em cada método.
Todas as classes possuem um atributo chamado __mro__
, que mantém uma tupla de referências a superclasses, na ordem de resolução dos métodos, indo desde a classe corrente até a classe object
.[8]
Para a classe Leaf
class, o __mro__
é o seguinte:
link:code/14-inheritance/diamond.py[role=include]
Note
|
Olhando para a Figura 1, pode parecer que a MRO descreve uma busca em largura (ou amplitude), mas isso é apenas uma coincidência para essa hierarquia de classes em particular. A MRO é computada por um algoritmo conhecido, chamado C3. Seu uso no Python está detalhado no artigo "The Python 2.3 Method Resolution Order" (A Ordem de Resolução de Métodos no Python 2.3), de Michele Simionato. É um texto difícil, mas Simionato escreve: "…a menos que você faça amplo uso de herança múltipla e mantenha hierarquias não-triviais, não é necessário entender o algoritmo C3, e você pode facilmente ignorar este artigo." |
A MRO determina apenas a ordem de ativação, mas se um método específico será ou não ativado em cada uma das classes vai depender de cada implementação chamar ou não super()
.
Considere o experimento com o método pong
.
A classe Leaf
não sobrepõe aquele método, então a chamada leaf1.pong()
ativa a implementação na próxima classe listada em Leaf.__mro__
: a classe A
.
O método A.pong
chama super().pong()
.
A classe B
class é e próxima na MRO, portanto B.pong
é ativado.
Mas aquele método não chama super().pong()
, então a sequência de ativação termina ali.
Além do grafo de herança, a MRO também leva em consideração a ordem na qual as superclasses aparecem na declaração da uma subclasse.
Em outras palavras, se em diamond.py (no Exemplo 4) a classe Leaf
fosse declarada como
Leaf(B, A)
, daí a classe B
apareceria antes de A
em Leaf.__mro__
.
Isso afetaria a ordem de ativação dos métodos ping
, e também faria leaf1.pong()
ativar B.pong
através da herança, mas A.pong
e Root.pong
nunca seriam executados, porque B.pong
não chama super()
.
Quando um método invoca super()
, ele é um método cooperativo.
Métodos cooperativos permitem a herança múltipla cooperativa.
Esses termos são intencionais: para funcionar, a herança múltipla no Python exige a cooperação ativa dos métodos envolvidos. Na classe B
, ping
coopera, mas pong
não.
Warning
|
Um método não-cooperativo pode ser a causa de bugs sutis.
Muitos programadores, lendo o Exemplo 4, poderiam esperar que, quando o método |
Métodos cooperativos devem ter assinaturas compatíveis, porque nunca se sabe se A.ping
será chamado antes ou depois de B.ping
.
A sequência de ativação depende da ordem de A
e B
na declaração de cada subclasse que herda de ambos.
O Python é uma linguagem dinâmica, então a interação de super()
com a MRO também é dinâmica.
O Exemplo 6 mostra um resultado surpreendente desse comportamento dinâmico.
super()
link:code/14-inheritance/diamond2.py[role=include]
-
A classe
A
vem de diamond.py (no Exemplo 4). -
A classe
U
não tem relação comA
ou`Root` do módulodiamond
. -
O que
super().ping()
faz? Resposta: depende. Continue lendo. -
LeafUA
é subclasse deU
eA
, nessa ordem.
Se você criar uma instância de U
e tentar chamar ping
, ocorre um erro:
link:code/14-inheritance/diamond2.py[role=include]
O objeto 'super'
devolvido por super()
não tem um atributo 'ping'
, porque o MRO de U
tem duas classes:
U
e object
, e este último não tem um atributo chamado 'ping'
.
Entretanto, o método U.ping
não é inteiramente sem solução. Veja isso:
link:code/14-inheritance/diamond2.py[role=include]
A chamada super().ping()
em LeafUA
ativa U.ping
,
que também coopera chamando super().ping()
,
ativando A.ping
e, por fim, Root.ping
.
Observe que as clsses base de LeafUA
são (U, A)
, nessa ordem.
Se em vez disso as bases fossem (A, U)
, daí leaf2.ping()
nunca chegaria a U.ping
,
porque o super().ping()
em A.ping
ativaria Root.ping
, e esse último não chama super()
.
Em um programa real, uma classe como U
poderia ser uma classe mixin:
uma classe projetada para ser usada junto com outras classes em herança múltipla, fornecendo funcionalidade adicional.
Vamos estudar isso em breve, na Classes mixin.
Para concluir essa discussão sobre a MRO, a Figura 2 ilustra parte do complexo grafo de herança múltipla do toolkit de interface gráfica Tkinter, da biblioteca padrão do Python.
Text
do Tkinter. Direita: O longo e sinuoso caminho de Text.__mro__
, desenhado com as setas pontilhadas.Para estudar a figura, comece pela classe Text
, na parte inferior.
A classe Text
implementa um componente de texto completo, editável e com múltiplas linhas.
Ele sozinho fornece muita funcionalidade, mas também herda muitos métodos de outras classes.
A imagem à esquerda mostra um diagrama de classe UML simples.
À direita, a mesma imagem é decorada com setas mostrando a MRO, como listada no Exemplo 7 com a ajuda de uma função de conveniência print_mro
.
tkinter.Text
>>> def print_mro(cls):
... print(', '.join(c.__name__ for c in cls.__mro__))
>>> import tkinter
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object
Vamos agora falar sobre mixins.
Uma classe mixin é projetada para ser herdada em conjunto com pelo menos uma outra classe, em um arranjo de herança múltipla. Uma mixin não é feita para ser a única classe base de uma classe concreta, pois não fornece toda a funcionalidade para um objeto concreto, apenas adicionando ou personalizando o comportamento de classes filhas ou irmãs.
Note
|
Classes mixin são uma convenção sem qualquer suporte explícito no Python e no C++. O Ruby permite a definição explícita e o uso de módulos que funcionam como mixins—coleções de métodos que podem ser incluídas para adicionar funcionalidade a uma classe. C#, PHP, e Rust implementam traits (características ou traços ou aspectos), que são também uma forma explícita de mixin. |
Vamos ver um exemplo simples mas conveniente de uma classe mixin.
O Exemplo 8 mostra a UpperCaseMixin
,
uma classe criada para fornecer acesso indiferente a maiúsculas/minúsculas para mapeamentos com chaves do tipo string, convertendo todas as chaves para maiúsculas quando elas são adicionadas ou consultadas.
UpperCaseMixin
suporta mapeamentos indiferentes a maiúsculas/minúsculaslink:code/14-inheritance/uppermixin.py[role=include]
-
Essa função auxiliar recebe uma
key
de qualquer tipo e tenta devolverkey.upper()
; se isso falha, devolve akey
inalterada. -
A mixin implementa quatro métodos essenciais de mapeamentos, sempre chamando `super()`com a chave em maiúsculas, se possível.
Como todos os métodos de UpperCaseMixin
chamam super()
,
esta mixin depende de uma classe irmã que implemente ou herde métodos com a mesma assinatura.
Para dar sua contribuição, uma mixin normalmente precisa aparecer antes de outras classes na MRO de uma subclasse que a use.
Na prática, isso significa que mixins devem aparecer primeiro na tupla de classes base em uma declaração de classe.
O Exemplo 9 apresenta dois exemplos.
UpperCaseMixin
link:code/14-inheritance/uppermixin.py[role=include]
-
UpperDict
não precisa de qualquer implementação própria, masUpperCaseMixin
deve ser a primeira classe base, caso contrário os métodos chamados seriam os deUserDict
. -
UpperCaseMixin
também funciona comCounter
. -
Em vez de
pass
, é melhor fornecer uma docstring para satisfazer a necessidade sintática de um corpo não-vazio na declaraçãoclass
.
Aqui estão alguns doctests de
uppermixin.py, para UpperDict
:
link:code/14-inheritance/uppermixin.py[role=include]
E uma rápida demonstração de UpperCounter
:
link:code/14-inheritance/uppermixin.py[role=include]
UpperDict
e UpperCounter
parecem quase mágica, mas tive que estudar cuidadosamente o código de UserDict
e Counter
para fazer UpperCaseMixin
trabalhar com eles.
Por exemplo, minha primeira versão de UpperCaseMixin
não incluía o método get
.
Aquela versão funcionava com UserDict
, mas não com Counter
.
A classe UserDict
herda get
de collections.abc.Mapping
, e aquele get
chama __getitem__
, que implementei.
Mas as chaves não eram transformadas em maiúsculas quando uma UpperCounter
era carregada no
__init__
.
Isso acontecia porque Counter.__init__
usa Counter.update
, que por sua vez recorre ao método get
herdado de dict
. Entretanto, o método get
na classe dict
não chama __getitem__
.
Esse é o núcleo do problema discutido na [inconsistent_missing].
É também uma dura advertência sobre a natureza frágil e intrincada de programas que se apoiam na herança, mesmo nessa pequena escala.
A próxima seção apresenta vários exemplos de herança múltipla, muitas vezes usando classes mixin.
No livro Design Patterns ("Padrões de Projetos"),[9] quase todo o código está em C++, mas o único exemplo de herança múltipla é o padrão Adapter ("Adaptador"). Em Python a herança múltipla também não é regra, mas há exemplos importantes, que comentarei nessa seção.
Na biblioteca padrão do Python, o uso mais visível de herança múltipla é o pacote collections.abc
.
Nenhuma controvérsia aqui: afinal, até o Java suporta herança múltipla de interfaces, e ABCs são declarações de interface que podem, opcionalmente, fornecer implementações concretas de métodos.[10]
A documentação oficial do Python para
collections.abc
(EN)
usa o termo mixin method ("método mixin") para os métodos concretos implementados em muitas das coleções nas ABCs.
As ABCs que oferecem métodos mixin cumprem dois papéis: elas são definições de interfaces e também classes mixin.
Por exemplo, a implementação de collections.UserDict
(EN)
recorre a vários dos métodos mixim fornecidos por collections.abc.MutableMapping
.
O pacote http.server inclui as classes HTTPServer
e ThreadingHTTPServer
. Essa última foi adicionada ao Python 3.7. Sua documentação diz:
- classe
http.server.ThreadingHTTPServer
(server_address, RequestHandlerClass) -
Essa classe é idêntica a
HTTPServer
, mas trata requisições com threads, usando aThreadingMixIn
. Isso é útil para lidar com navegadores web que abrem sockets prematuramente, situação na qual oHTTPServer
esperaria indefinidamente.
Este é o
código-fonte completo da classe ThreadingHTTPServer
no Python 3.10:
class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
O código-fonte de socketserver.ThreadingMixIn
tem 38 linhas, incluindo os comentários e as docstrings.
O Exemplo 10 apresenta um resumo de sua implementação.
class ThreadingMixIn:
"""Mixin class to handle each request in a new thread."""
# 8 lines omitted in book listing
def process_request_thread(self, request, client_address): # (1)
... # 6 lines omitted in book listing
def process_request(self, request, client_address): # (2)
... # 8 lines omitted in book listing
def server_close(self): # (3)
super().server_close()
self._threads.join()
-
process_request_thread
não chamasuper()
porque é um método novo, não uma sobreposição. Sua implementação chama três métodos de instância queHTTPServer
oferece ou herda. -
Isso sobrepõe o método
process_request
, queHTTPServer
herda desocketserver.BaseServer
, iniciando uma thread e delegando o trabalho efetivo para aprocess_request_thread
que roda naquela thread. O método não chamasuper()
. -
server_close
chamasuper().server_close()
para parar de receber requisições, e então espera que as threads iniciadas porprocess_request
terminem sua execução.
A ThreadingMixIn
aparece junto com ForkingMixIn
na documentação do módulo socketserver
.
Essa última classe foi projetada para suportar servidores concorrentes baseados na
os.fork()
, uma API para iniciar processos filhos, disponível em sistemas Unix (ou similares) compatíveis com a POSIX.
Note
|
Não é necessário entender de Django para acompanhar essa seção. Uso uma pequena parte do framework como um exemplo prático de herança múltipla, e tentarei fornecer todo o pano de fundo necessário (supondo que você tenha alguma experiência com desenvolvimento web no lado servidor, com qualquer linguagem ou framework). |
No Django, uma view é um objeto invocável que recebe um argumento request
—um objeto representando uma requisição HTTP—e devolve um objeto representando uma resposta HTTP.
Nosso interesse aqui são as diferentes respostas.
Elas podem ser tão simples quanto um redirecionamento, sem nenhum conteúdo em seu corpo, ou tão complexas quando uma página de catálogo de uma loja online, renderizada a partir de uma template HTML e listando múltiplas mercadorias, com botões de compra e links para páginas com detalhes.
Originalmente, o Django oferecia uma série de funções, chamadas views genéricas, que implementavam alguns casos de uso comuns. Por exemplo, muitos sites precisam exibir resultados de busca que incluem dados de inúmeros itens, com listagens ocupando múltiplas páginas, cada resultado contendo também um link para uma página de informações detalhadas sobre aquele item. No Django, uma view de lista e uma view de detalhes são feitas para funcionarem juntas, resolvendo esse problema: uma view de lista renderiza resultados de busca , e uma view de detalhes produz uma página para cada item individual.
Entretanto, as views genéricas originais eram funções, então não eram extensíveis. Se quiséssemos algo algo similar mas não exatamente igual a uma view de lista genérica, era preciso começar do zero.
O conceito de views baseadas em classes foi introduzido no Django 1.3, juntamente com um conjunto de classes de views genéricas divididas em classes base, mixins e classes concretas prontas para o uso. No Django 3.2, as classes base e as mixins estão no módulo base
do pacote django.views.generic
, ilustrado na Figura 3. No topo do diagrama vemos duas classes que se encarregam de responsabilidades muito diferentes: View
e TemplateResponseMixin
.
Tip
|
Um recurso fantástico para estudar essas classes é o site Classy Class-Based Views (EN), onde se pode navegar por elas facilmente, ver todos os métodos em cada classe (métodos herdados, sobrepostos e adicionados), os diagramas de classes, consultar sua documentação e estudar seu código-fonte no GitHub. |
View
é a classe base de todas as views (ela poderia ser uma ABC), e oferece funcionalidade essencial como o método dispatch
, que delega para métodos de "tratamento" como get
, head
, post
, etc., implementados por subclasses concretas para tratar os diversos verbos HTTP.[11] A classe RedirectView
herda apenas de View
, e podemos ver que ela implementa get
, head
, post
, etc.
Se é esperado que as subclasses concretas de View
implementem os métodos de tratamento, por que aqueles métodos não são parte da interface de View
? A razão: subclasses são livres para implementar apenas os métodos de tratamento que querem suportar. Uma TemplateView
é usada apenas para exibir conteúdo, então ela implementa apenas get
. Se uma requisição HTTP POST
é enviada para uma TemplateView
, o método herdado View.dispatch
verifica que não há um método de tratamento para post
, e produz uma resposta HTTP 405 Method Not Allowed
.[12]
A TemplateResponseMixin
fornece funcionalidade que interessa apenas a views que precisam usar uma template. Uma RedirectView
, por exemplo, não tem qualquer conteúdo em seu corpo, então não precisa de uma template e não herda dessa mixin. TemplateResponseMixin
fornece comportamentos para TemplateView
e outras views que renderizam templates, tal como ListView
, DetailView
, etc., definidas nos sub-pacotes de django.views.generic
. A Figura 4 mostra o módulo django.views.generic.list
e parte do módulo base
.
Para usuários do Django, a classe mais importante na Figura 4 é ListView
, uma classe agregada sem qualquer código (seu corpo é apenas uma docstring). Quando instanciada, uma ListView
tem um atributo de instância object_list
, através do qual a template pode interagir para mostrar o conteúdo da página, normalmente o resultado de uma consulta a um banco de dados, composto de múltiplos objetos. Toda a funcionalidade relacionada com a geração desse iterável de objetos vem da MultipleObjectMixin
. Essa mixin também oferece uma lógica complexa de paginação—para exibir parte dos resultados em uma página e links para mais páginas.
Suponha que você queira criar uma view que não irá renderizar uma template, mas sim produzir uma lista de objetos em formato JSON. Para isso existe BaseListView
. Ela oferece um ponto inicial de extensão fácil de usar, unindo a funcionalidade de View
e de MultipleObjectMixin
, mas sem a sobrecarga do mecanismo de templates.
A API de views baseadas em classes do Django é um exemplo melhor de herança múltipla que o Tkinter. Em especial, é fácil entender suas classes mixin: cada uma tem um propósito bem definido, e todos os seus nomes contêm o sufixo …Mixin
.
django.views.generic.list
. Aqui as três classes do módulo base aparecem recolhidas (veja a Figura 3). A classe ListView
não tem métodos ou atributos: é uma classe agregada.Views baseadas em classes não são universalmente aceitas por usuários do Django. Muitos as usam de forma limitada, como caixas opacas. Mas quando é necessário criar algo novo, muitos programadores Django continuam criando funções monolíticas de views, para abarcar todas aquelas responsabilidades, ao invés de tentar reutilizar as views base e as mixins.
Demora um certo tempo para aprender a usar as views baseadas em classes e a forma de estendê-las para suprir necessidades específicas de uma aplicação, mas considero que vale a pena estudá-las. Elas eliminam muito código repetitivo, tornam mais fácil reutilizar soluções, e melhoram até a comunicação das equipes—por exemplo, pela definição de nomes padronizados para as templates e para as variáveis passadas para contextos de templates. Views baseadas em classes são views do Django "on rails"[13].
Um exemplo extremo de herança múltipla na biblioteca padrão do Python é o
toolkit de interface gráfica Tkinter.
Usei parte da hierarquia de componentes do Tkinter para ilustrar a MRO na Figura 2. A
Figura 5 mostra todos as classes de componentes no pacote base tkinter
(há mais componentes gráficos no subpacote tkinter.ttk
).
No momento em que escrevo essa seção, o Tkinter já tem 25 anos de idade. Ele não é um exemplo das melhores práticas atuais. Mas mostra como a herança múltipla era usada quando os programadores ainda não conheciam suas desvantagens. E vai nos servir de contra-exemplo, quando tratarmos de algumas boas práticas, na próxima seção.
Considere as seguintes classes na Figura 5:
➊ Toplevel
: A classe de uma janela principal em um aplicação Tkinter.
➋ Widget
: A superclasse de todos os objetos visíveis que podem ser colocados em uma janela.
➌ Button
: Um componente de botão simples.
➍ Entry
: Um campo de texto editável de uma única linha.
➎ Text
: Um campo de texto editável de múltiplas linhas.
Aqui estão as MROs dessas classes, como exibidas pela função print_mro
do Exemplo 7:
>>> import tkinter
>>> print_mro(tkinter.Toplevel)
Toplevel, BaseWidget, Misc, Wm, object
>>> print_mro(tkinter.Widget)
Widget, BaseWidget, Misc, Pack, Place, Grid, object
>>> print_mro(tkinter.Button)
Button, Widget, BaseWidget, Misc, Pack, Place, Grid, object
>>> print_mro(tkinter.Entry)
Entry, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, object
>>> print_mro(tkinter.Text)
Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object
Note
|
Pelos padrões atuais, a hierarquia de classes do Tkinter é muito profunda. Poucas partes da bilbioteca padrão do Python tem mais que três ou quatro níveis de classes concretas, e o mesmo pode ser dito da biblioteca de classes do Java.
Entretanto, é interessante observar que algumas das hierarquias mais profundas da biblioteca de classes do Java são precisamente os pacotes relacionados à programação de interfaces gráficas:
|
Observe como essas classes se relacionam com outras:
-
Toplevel
é a única classe gráfica que não herda deWidget
, porque ela é a janela primária e não se comporta como um componente; por exemplo, ela não pode ser anexada a uma janela ou moldura (frame).Toplevel
herda deWm
, que fornece funções de acesso direto ao gerenciador de janelas do ambiente, para tarefas como definir o título da janela e configurar suas bordas. -
Widget
herda diretamente deBaseWidget
e dePack
,Place
, eGrid
. As últimas três classes são gerenciadores de geometria: são responsáveis por organizar componentes dentro de uma janela ou moldura. Cada uma delas encapsula uma estratégia de layout e uma API de colocação de componentes diferente. -
Button
, como a maioria dos componentes, descende diretamente apenas deWidget
, mas indiretamente deMisc
, que fornece dezenas de métodos para todos os componentes. -
Entry
é subclasse deWidget
eXView
, que suporta rolagem horizontal. -
Text
é subclasse deWidget
,XView
eYView
(para rolagem vertical).
Vamos agora discutir algumas boas práticas de herança múltipla e examinar se o Tkinter as segue.
Aquilo que Alan Kay escreveu na epígrafe continua sendo verdade: ainda não existe um teoria geral sobre herança que possa guiar os programadores. O que temos são regras gerais, padrões de projetos, "melhores práticas", acrônimos perspicazes, tabus, etc. Alguns desses nos dão orientações úteis, mas nenhum deles é universalmente aceito ou sempre aplicável.
É fácil criar designs frágeis e incompreensíveis usando herança, mesmo sem herança múltipla. Como não temos uma teoria abrangente, aqui estão algumas dicas para evitar grafos de classes parecidos com espaguete.
O título dessa subseção é o segundo princípio do design orientado a objetos, do livro Padrões de Projetos,[14] e é o melhor conselho que posso oferecer aqui. Uma vez que você se sinta confortável com a herança, é fácil usá-la em excesso. Colocar objetos em uma hierarquia elegante apela para nosso senso de ordem; programadores fazem isso por pura diversão.
Preferir a composição leva a designs mais flexíveis. Por exemplo, no caso da classe
tkinter.Widget
, em vez de herdar os métodos de todos os gerenciadores de geometria, instâncias do componente poderiam manter uma referência para um gerenciador de geometria, e invocar seus métodos. Afinal, um Widget
não deveria "ser" um gerenciador de geometria, mas poderia usar os serviços de um deles por delegação. E daí você poderia adicionar um novo gerenciador de geometria sem afetar a hierarquia de classes do componente e sem se preocupar com colisões de nomes. Mesmo com herança simples, este princípio aumenta a flexibilidade, porque a subclasses são uma forma de acoplamento forte, e árvores de herança muito altas tendem a ser frágeis.
A composição e a delegação podem substituir o uso de mixins para tornar comportamentos disponíveis para diferentes classes, mas não podem substituir o uso de herança de interfaces para definir uma hierarquia de tipos.
Ao lidarmos com herança múltipla, é útil ter claras as razões pelas quais subclasses são criadas em cada caso específico. As principais razões são:
-
Herança de interface cria um subtipo, implicando em uma relação "é-um". A melhor forma de fazer isso é usando ABCs.
-
Herança de implementação evita duplicação de código pela reutilização. Mixins podem ajudar nisso.
Na prática, frequentemente ambos os usos são simultâneos, mas sempre que você puder tornar a intenção clara, vá em frente. Herança para reutilização de código é um detalhe de implementação, e muitas vezes pode ser substituída por composição e delegação. Por outro lado, herança de interfaces é o fundamento de qualquer framework. Se possível, a herança de interfaces deveria usar apenas ABCs como classes base.
No Python moderno, se uma classe tem por objetivo definir uma interface, ela deveria ser explicitamente uma ABC ou uma subclasse de typing.Protocol
.
Uma ABC deveria ser subclasse apenas de abc.ABC
ou de outras ABCs.
A herança múltipla de ABCs não é problemática.
Se uma classe é projetada para fornecer implementações de métodos para reutilização por múltiplas subclasses não relacionadas, sem implicar em uma relação do tipo "é-uma", ele deveria ser uma classe mixin explícita. Conceitualmente, uma mixin não define um novo tipo; ela simplesmente empacota métodos para reutilização. Uma mixin não deveria nunca ser instanciada, e classes concretas não devem herdar apenas de uma mixin. Cada mixin deveria fornecer um único comportamento específico, implementando poucos métodos intimamente relacionados. Mixins devem evitar manter qualquer estado interno; isto é, uma classe mixin não deve ter atributos de instância.
No Python, não há uma maneira formal de declarar uma classe como mixin. Assim, é fortemente recomendado que seus nomes incluam o sufixo Mixin
.
Uma classe construída principalmente herdando de mixins, sem adicionar estrutura ou comportamento próprios, é chamada de classe agregada.[15]
Object-Oriented Analysis and Design with Applications
Se alguma combinação de ABCs ou mixins for especialmente útil para o código cliente, ofereça uma classe que una essas funcionalidades de uma forma sensata.
Por exemplo, aqui está o código-fonte completo
da classe ListView
do Django, do canto inferior direito da Figura 4:
class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
"""
Render some list of objects, set by `self.model` or `self.queryset`.
`self.queryset` can actually be any iterable of items, not just a queryset.
"""
O corpo de ListView
é vazio[16], mas a classe fornece um serviço útil: ela une uma mixin e uma classe base que devem ser usadas em conjunto.
Outro exemplo é tkinter.Widget
, que tem quatro classes base e nenhum método ou atributo próprios—apenas uma docstring.
Graças à classe agregada Widget
, podemos criar um novo componente com as mixins necessárias, sem precisar descobrir em que ordem elas devem ser declaradas para funcionarem como desejado.
Observe que classes agregadas não precisam ser inteiramente vazias (mas frequentemente são).
Em um comentário sobre esse capítulo, o revisor técnico Leonardo Rochael sugeriu o alerta abaixo.
Warning
|
Criar subclasses e sobrepor métodos de qualquer classe complexa é um processo muito suscetível a erros, porque os métodos da superclasse podem ignorar as sobreposições da subclasse de formas inesperadas. Sempre que possível, evite sobrepor métodos, ou pelo menos se limite a criar subclasses de classes projetadas para serem facilmente estendidas, e apenas daquelas formas pelas quais a classe foi desenhada para ser estendida. |
É um ótimo conselho, mas como descobrimos se uma classe foi projetada para ser estendida?
A primeira resposta é a documentação (algumas vezes na forma de docstrings ou até de comentários no código).
Por exemplo, o pacote
socketserver
(EN) do Python é descrito como "um framework para servidores de rede".
Sua classe BaseServer
(EN) foi projetada para a criação de subclasses, como o próprio nome sugere.
E mais importante, a documentação e a docstring (EN) no código-fonte da classe informa explicitamente quais de seus métodos foram criados para serem sobrepostos por subclasses.
No Python ≥ 3.8 uma nova forma de tornar tais restrições de projeto explícitas foi oferecida pela
PEP 591—Adding a final qualifier to typing (Acrescentando um qualificador "final" à tipagem) (EN).
A PEP introduz um decorador @final
, que pode ser aplicado a classes ou a métodos individuais, de forma que IDEs ou verificadores de tipo podem identificar tentativas equivocadas de criar subclasses daquelas classes ou de sobrepor aqueles métodos.[17]
Criar subclasses de classes concretas é mais perigoso que criar subclasses de ABCs e mixins, pois instâncias de classes concretas normalmente tem um estado interno, que pode ser facilmente corrompido se sobrepusermos métodos que dependem daquele estado.
Mesmo se nossos métodos cooperarem chamando super()
, e o estado interno seja mantido através da sintaxe __x
, restarão ainda inúmeras formas pelas quais a sobreposição de um método pode introduzir bugs.
No [waterfowl_essay], Alex Martelli cita More Effective C++, de Scott Meyer, que diz: "toda classe não-final (não-folha) deveria ser abstrata". Em outras palavras, Meyer recomenda que subclasses deveriam ser criadas apenas a partir de classes abstratas.
Se você precisar usar subclasses para reutilização de código, então o código a ser reutilizado deve estar em métodos mixin de ABCs, ou em classes mixin explicitamente nomeadas.
Vamos agora analisar o Tkinter do ponto de vista dessas recomendações
A[18] maioria dos conselhos da seção anterior não são seguidos pelo Tkinter, com a notável excessão de "Ofereça classes agregadas aos usuários". E mesmo assim, esse não é um grande exemplo, pois a composição provavelmente funcionaria melhor para integrar os gerenciadores de geometria a Widget
, como discutido na Prefira a composição de objetos à herança de classes.
Mas lembre-se que o Tkinter é parte da biblioteca padrão desde o Python 1.1, lançado em 1994. O Tkinter é uma camada sobreposta ao excelente toolkit Tk GUI, da linguagem Tcl. O combo Tcl/Tk não é, na origem, orientado a objetos, então a API Tk é basicamente um imenso catálogo de funções. Entretanto, o toolkit é orientado a objetos por projeto, apesar de não o ser em sua implementação Tcl original.
A docstring de tkinter.Widget
começa com as palavras "Internal class" (Classe interna). Isso sugere que Widget
deveria provavelmente ser uma ABC. Apesar da classe Widget
não ter métodos próprios, ela define uma interface. Sua mensagem é: "Você pode contar que todos os componentes do Tkinter vão oferecer os métodos básicos de componente (__init__
, destroy
, e dezenas de funções da API Tk), além dos métodos de todos os três gerenciadores de geometria". Vamos combinar que essa não é uma boa definição de interface (é abrangente demais), mas ainda assim é uma interface, e Widget
a "define" como a união das interfaces de suas superclasses.
A classe Tk
, qie encapsula a lógica da aplicação gráfica, herda de Wm
e Misc
, nenhuma das quais é abstrata ou mixin (Wm
não é uma mixin adequada, porque TopLevel
é subclasse apenas dela). O nome da classe Misc
é, por sí só, um mau sinal. Misc
tem mais de 100 métodos, e todos os componentes herdam dela. Por que é necessário que cada um dos componentes tenham métodos para tratamento do clipboard, seleção de texto, gerenciamento de timer e coisas assim? Não é possível colar algo em um botão ou selecionar texto de uma barra de rolagem. Misc
deveria ser dividida em várias classes mixin especializadas, e nem todos os componentes deveriam herdar de todas aquelas mixins.
Para ser justo, como usuário do Tkinter você não precisa, de forma alguma, entender ou usar herança múltipla. Ela é um detalhe de implementação, oculto atrás das classes de componentes que serão instanciadas ou usadas como base para subclasses em seu código. Mas você sofrerá as consequências da herança múltipla excessiva quando digitar dir(tkinter.Button)
e tentar encontrar um método específico em meio aos 214 atributos listados.
E terá que enfrentar a complexidade, caso decida implementar um novo componente Tk.
Tip
|
Apesar de ter problemas, o Tkinter é estável, flexível, e fornece um visual moderno se você usar o pacote |
Aqui termina nossa viagem através do labirinto da herança.
Esse capítulo começou com uma revisão da função super()
no contexto de herança simples.
Daí discutimos o problema da criação de subclasses de tipos embutidos:
seus métodos nativos, implementados em C, não invocam os métodos sobrepostos em subclasses, exceto em uns poucos casos especiais.
É por isso que, quando precisamos de tipos list
, dict
, ou str
personalizados, é mais fácil criar subclasses de UserList
, UserDict
, ou UserString
—todos definidos no módulo
collections
—, que na verdade encapsulam os tipos embutidos correspondentes e delegam operações para aqueles—três exemplos a favor da composição sobre a herança na biblioteca padrão. Se o comportamento desejado for muito diferente daquilo que os tipos embutidos oferecem, pode ser mais fácil criar uma subclasse da ABC apropriada em collections.abc
, e escrever sua própria implementação.
O restante do capítulo foi dedicado à faca de dois gumes da herança múltipla. Primeiro vimos como a ordem de resolução de métodos, definida no atributo de classe __mro__
, trata o problema de conflitos potenciais de nomes em métodos herdados. Também examinamos como a função embutida
super()
se comporta em hierarquias com herança múltipla, e como ela algumas vezes se comporta de forma inesperada. O comportamento de super()
foi projetado para suportar classes mixin, que estudamos usando o exemplo simples de UpperCaseMixin
(para mapeamentos indiferentes a maiúsculas/minúsculas).
Exploramos como a herança múltipla e os métodos mixin são usados nas ABCs do Python, bem como nos mixins de threading e forking de socketserver
.
Usos mais complexos de herança múltipla foram exemplificados com as views baseadas em classes do Django e com o toolkit de interface gráfica Tkinter.
Apesar do Tkinter não ser um exemplo das melhores práticas modernas, é um exemplo de hierarquias de classe complexas que podemos encontrar em sistemas legados.
Encerrando o capítulo, apresentamos sete recomendações para lidar com herança, e aplicamos alguns daqueles conselhos em um comentário sobre a hierarquia de classes do Tkinter.
Rejeitar a herança—mesmo a herança simples—é uma tendência moderna. Go é uma das mais bem sucedidas linguagens criadas no século 21. Ela não inclui um elemento chamado "classe", mas você pode construir tipos que são estruturas (structs) de campos encapsulados, e anexar métodos a essas estruturas. Em Go é possível definir interfaces, que são verificadas pelo compilador usando tipagem estrutural, também conhecida como duck typing estática—algo muito similar ao que temos com os tipos protocolo desde o Python 3.8. Essa linguagem também tem uma sintaxe especial para a criação de tipos e interfaces por composição, mas não há suporte a herança—nem entre interfaces.
Então talvez o melhor conselho sobre herança seja: evite-a se puder. Mas, frequentemente, não temos essa opção: os frameworks que usamos nos impõe suas escolhas de design.
No que diz respeito à legibilidade, composição feita de forma adequada é superior a herança. Como é muito mais frequente ler o código que escrevê-lo, como regra geral evite subclasses, mas em especial não misture os vários tipos de herança e não crie subclasses para compartilhar código.
Subclassing in Python Redux
Durante a revisão final desse livro, o revisor técnico Jürgen Gmach recomendou o post "Subclassing in Python Redux" (O ressurgimento das subclasses em Python), de Hynek Schlawack—a fonte da citação acima. Schlawack é o autor do popular pacote attrs, e foi um dos principais contribuidores do framework de programação assíncrona Twisted, um projeto criado por Glyph Lefkowitz em 2002. De acordo com Schlawack, após algum tempo os desenvolvedores perceberam que tinham usado subclasses em excesso no projeto. O post é longo, e cita outros posts e palestras importantes. Muito recomendado.
Naquela mesma conclusão, Hynek Schlawack escreve: "Não esqueça que, na maioria dos casos, tudo o que você precisa é de uma função." Concordo, e é precisamente por essa razão que Python Fluente trata em detalhes das funções, antes de falar de classes e herança. Meu objetivo foi mostrar o quanto você pode alcançar com funções se valendo das classes na biblioteca padrão, antes de criar suas próprias classes.
A criação de subclasses de tipos embutidos, a função super
, e recursos avançados como descritores e metaclasses, foram todos introduzidos no artigo "Unifying types and classes in Python 2.2" (Unificando tipos e classes em Python 2.2) (EN), de Guido van Rossum.
Desde então, nada realmente importante mudou nesses recursos.
O Python 2.2 foi uma proeza fantástica de evolução da linguagem, adicionando vários novos recursos poderosos em um todo coerente, sem quebrar a compatibilidade com versões anteriores. Os novo recursos eram 100% opcionais. Para usá-los, bastava programar explicitamente uma subclasse de object
—direta ou indiretamente—, para criar uma assim chamada "classe no novo estilo". No Python 3, todas as classes são subclasses de object
.
O Python Cookbook, 3ª ed., de David Beazley e Brian K. Jones (O’Reilly) inclui várias receitas mostrando o uso de super()
e de classes mixin. Você pode começar pela esclarecedora seção "8.7. Calling a Method on a Parent Class" (Invocando um Método em uma Superclasse), e seguir as referências internas a partir dali.
O post "Python’s super() considered super!" (O super() do Python é mesmo super!) (EN), de Raymond Hettinger, explica o funcionamento de super
e a herança múltipla de uma perspectiva positiva. Ele foi escrito em resposta a "Python’s Super is nifty, but you can’t use it (Previously: Python’s Super Considered Harmful)" O Super do Python é bacana, mas você não deve usá-lo (Antes: Super do Python Considerado Nocivo) (EN), de James Knight.
A resposta de Martijn Pieters a
"How to use super() with one argument?" (Como usar super() com um só argumento?) (EN) inclui uma explicação concisa e aprofundada de super
, incluindo sua relação com descritores, um conceito que estudaremos apenas no [attribute_descriptors].
Essa é a natureza de super
. Ele é simples de usar em casos de uso básicos, mas é uma ferramenta poderosa e complexa, que alcança alguns dos recursos dinâmicos mais avançados do Python, raramente encontrados em outras linguagens.
Apesar dos títulos daqueles posts, o problema não é exatamente com a função embutida super
—que no Python 3 não é tão feia quanto era no Python 2.
A questão real é a herança múltipla, algo inerentemente complicado e traiçoeiro.
Michele Simionato vai além da crítica, e de fato oferece uma solução em seu
"Setting Multiple Inheritance Straight" (Colocando a Herança Múltipla em seu Lugar) (EN):
ele implementa traits ("traços"), uma forma explícita de mixin originada na linguagem Self.
Simionato escreveu, em seu blog, uma longa série de posts sobre herança múltipla em Python, incluindo
"The wonders of cooperative inheritance, or using super in Python 3" (As maravilhas da herança cooperativa, ou usando super em Python 3) (EN);
"Mixins considered harmful," part 1 (Mixins consideradas nocivas) (EN) e
part 2 (EN);
e "Things to Know About Python Super," part 1 (O que você precisa saber sobre o super do Python) (EN),
part 2 (EN), e part 3 (EN).
Os posts mais antigos usam a sintaxe de super
do Python 2, mas ainda são relevantes.
Eu li a primeira edição do Object-Oriented Analysis and Design, 3ª ed., de Grady Booch et al., e o recomendo fortemente como uma introdução geral ao pensamento orientado a objetos, independente da linguagem de programação. É um dos raros livros que trata da herança múltipla sem ideias pré-concebidas.
Hoje, mais que nunca, é de bom tom evitar a herança, então cá estão duas referências sobre como fazer isso. Brandon Rhodes escreveu "The Composition Over Inheritance Principle" (O Princípio da Composição Antes da Herança) (EN), parte de seu excelente guia Python Design Patterns (Padrões de Projetos no Python). Augie Fackler e Nathaniel Manista apresentaram "The End Of Object Inheritance & The Beginning Of A New Modularity" (O Fim da Herança de Objetos & O Início de Uma Nova Modularidade) na PyCon 2013. Fackler e Manista falam sobre organizar sistemas em torno de interfaces e das funções que lidam com os objetos que implementam aquelas interfaces, evitando o acoplamento estreito e os pontos de falha de classes e da herança. Isso me lembra muito a maneira de pensar do Go, mas aqui os autores a defendem para o Python.
Pense nas classes realmente necessárias
[Nós] começamos a defender a ideia de herança como uma maneira de permitir que iniciantes pudessem construir [algo] a partir de frameworks que só poderiam ser projetadas por especialistas[19].
The Early History of Smalltalk ("Os Primórdios do Smalltalk")
A imensa maioria dos programadores escreve aplicações, não frameworks. Mesmo aqueles que escrevem frameworks provavelmente passam muito (ou a maior parte) de seu tempo escrevendo aplicações. Quando escrevemos aplicações, normalmente não precisamos criar hierarquias de classes. No máximo escrevemos classes que são subclasses de ABCs ou de outras classes oferecidas pelo framework. Como desenvolvedores de aplicações, é muito raro precisarmos escrever uma classe que funcionará como superclasse de outra. As classes que escrevemos são, quase sempre, "classes folha" (isto é, folhas na árvore de herança).
Se, trabalhando como desenvolvedor de aplicações, você se pegar criando hierarquias de classe de múltiplos níveis, quase certamente uma ou mais das seguintes alternativas se aplica:
-
Você está reinventando a roda. Procure um framework ou biblioteca que forneça componentes que possam ser reutilizados em sua aplicação.
-
Você está usando um framework mal projetada. Procure uma alternativa.
-
Você está complicando demais o processo. Lembre-se do Princípio KISS.
-
Você ficou entediado programando aplicações e decidiu criar um novo framework. Parabéns e boa sorte!
Também é possível que todas as alternativas acima se apliquem à sua situação: você ficou entediado e decidiu reinventar a roda, escrevendo seu próprio framework mal projetado e excessivamente complexo, e está sendo forçado a programar classe após classe para resolver problemas triviais. Espero que você esteja se divertindo, ou pelo menos que esteja sendo pago para fazer isso.
Tipos embutidos mal-comportados: bug ou feature?
Os tipos embutidos dict
, list
, e str
são blocos básicos essenciais do próprio Python, então precisam ser rápidos—qualquer problema de desempenho ali teria severos impactos em praticamente todo o resto.
É por isso que o CPython adotou atalhos que fazem com que métodos embutidos se comportem mal, ao não cooperarem com os métodos sobrepostos por subclasses.
Um caminho possível para sair desse dilema seria oferecer duas implementações para cada um desses tipos: um "interno", otimizado para uso pelo interpretador, e um externo, facilmente extensível.
Mas isso nós já temos: UserDict
, UserList
,
e UserString
não são tão rápidos quanto seus equivalentes embutidos,
mas são fáceis de estender.
A abordagem pragmática tomada pelo CPython significa que nós também podemos usar,
em nossas próprias aplicações, as implementações altamente otimizadas mas difíceis estender.
E isso faz sentido, considerando que não é tão frequente precisarmos de um mapeamento,
uma lista ou uma string customizados, mas usamos dict
, list
, e str
diariamente.
Só precisamos estar cientes dos compromissos envolvidos.
Herança através das linguagens
Alan Kay criou o termo "orientado a objetos", e o Smalltalk tinha apenas herança simples, apesar de existirem versões com diferentes formas de suporte a herança múltipla, incluindo os dialetos modernos de Smalltalk, Squeak e Pharo, que suportam traits ("traços")—um dispositivo de linguagem que pode substituir classes mixin, mas evita alguns dos problemas da herança múltipla.
A primeira linguagem popular a implementar herança múltipla foi o C++, e esse recurso foi abusado o suficiente para que o Java—criado para ser um substituto do C++—fosse projetado sem suporte a herança múltipla de implementação (isto é, sem classes mixin). Quer dizer, isso até o Java 8 introduzir os métodos default, que tornam interfaces muito similares às classes abstratas usadas para definir interfaces em C++ e em Python. Depois do Java, a linguagem da JVM mais usada é provavelmente o Scala, que implementa traits.
Outras linguagens que suportam traits são a última versão estável do PHP e do Groovy, bem como o Rust e o Raku—a linguagem antes conhecida como Perl 6.[20] Então podemos dizer que traits estão na moda em 2021.
O Ruby traz uma perspectiva original para a herança múltipla: não a suporta, mas introduz mixins como um recurso explícito da linguagem. Uma classe Ruby pode incluir um módulo em seu corpo, e aí os métodos definidos no módulo se tornam parte da implementação da classe. Essa é uma forma "pura" de mixin, sem herança envolvida, e está claro que uma mixin Ruby não tem qualquer influência sobre o tipo da classe onde ela é usada. Isso oferece os benefícios das mixins, evitando muitos de seus problemas mais comuns.
Duas novas linguagens orientadas a objetos que estão recebendo muita atenção limitam severamente a herança: Go e Julia. Ambas giram em torno de programar "objetos" implementando "métodos", e suportam polimorfismo, mas evitam o termo "classe".
Go não tem qualquer tipo de herança, mas oferece uma sintaxe que facilita a composição. Julia tem uma hierarquia de tipos, mas subtipos não podem herdar estrutura, apenas comportamentos, e só é permitido criar subtipos de tipos abstratos. Além disso, os métodos de Julia são implementados com despacho múltiplo—uma forma mais avançada do mecanismo que vimos na [generic_functions].
LastUpdatedOrderedDict
.
o
vinculado um método m
no momento da chamada o.m()
.
super()
. Veja a discussão em "Is it time to deprecate unbound super methods?" (É hora de descontinuar métodos "super" não vinculados?).
.mro()
, mas este é um recurso avançado de programaçõa de metaclasses, mencionado no [anatomy_of_classes]. Durante o uso normal de uma classe, apenas o conteúdo do atributo __mro__
importa.
as_view
é a parte mais visível da interface View
, mas isso não é relevante para nós aqui.
View
não obriga subclasses a implementarem todos os métodos de tratamento, mas dispatch
verifica, durante a execução, se um método de tratamento concreto está disponível para cada requisição específica.
self.model
ou self.queryset
. self.queryset
na verdade pode ser qualquer iterável de itens, não apenas um queryset."
Final
para variáveis e atributos que não devem ser reatribuídos ou sobrepostos.