O Modelo de Objeto Python
Classes, métodos dunder, herança, dataclasses e protocolos parecem recursos separados, mas são realmente pontos de entrada diferentes em um único modelo de objeto.
Esse modelo tem duas partes em movimento: como o Python busca um atributo em um objeto e como métodos dunder especiais permitem que a sintaxe comum (+, len(), for, with) opere em objetos que você define.
Esta página explica ambos, para que Métodos Dunder / Mágicos, Herança e MRO, Propriedades e Descritores e Protocolos e Tipagem Estrutural sejam lidos como variações de um tema, em vez de uma pilha de sintaxe não relacionada para memorizar.
Resumo
- Todo acesso a atributos em Python (
obj.nome) segue um algoritmo de busca - primeiro o dicionário da instância, depois a ordem de resolução de métodos da classe - e os métodos dunder são como os objetos se conectam à sintaxe integrada. - Por que Importa: Assim que você vê a busca de atributos e o despacho dunder como os dois mecanismos compartilhados, herança, propriedades e sobrecarga de operadores deixam de ser tópicos independentes e se tornam aplicações previsíveis das mesmas regras.
- Conceitos-Chave: classe como objeto,
__dict__da instância, ordem de resolução de métodos (MRO), protocolo de descritor, despacho de método dunder. - Quando Usar: Depurando por que um atributo resolve para um valor inesperado, decidindo entre herança e composição, ou entendendo por que
@propertye métodos vinculados se comportam da maneira que o fazem. - Limitações / Compromissos: A flexibilidade da busca dinâmica de atributos e da sobrecarga de operadores custa alguma previsibilidade - dois objetos que parecem semelhantes podem se comportar de maneiras muito diferentes, dependendo de quais métodos dunder eles implementam.
- Tópicos Relacionados: metaclasses, tipagem estrutural, o protocolo de descritor, composição versus herança.
Fundamentos
Em Python, uma classe é em si um objeto - especificamente, uma instância de type (ou uma metaclasse personalizada) - é por isso que você pode anexar atributos diretamente ao corpo de uma classe e por que type(MinhaClasse) retorna type, enquanto type(instancia) retorna MinhaClasse.
Criar uma instância (MinhaClasse()) constrói um novo objeto com seu próprio __dict__ (seu namespace pessoal para atributos de instância), mais um link oculto de volta para a classe que o criou.
Acessar instancia.nome aciona uma busca: Python primeiro verifica o __dict__ da própria instância e, se o nome não estiver lá, ele percorre a ordem de resolução de métodos (MRO) da classe - a própria classe, depois suas bases, em uma ordem bem definida - procurando o nome no __dict__ de cada classe.
Uma analogia simples: o dicionário da instância é um bloco de notas adesivas preso a um objeto específico, enquanto a classe (e suas bases) é um manual de referência compartilhado que todos desse tipo consultam quando suas próprias notas adesivas não têm a resposta.
Métodos funcionam através dessa mesma busca: def cumprimentar(self) dentro do corpo de uma classe é apenas um objeto de função armazenado no __dict__ da classe, e acessá-lo através de uma instância é o que o transforma em um "método vinculado" que fornece self automaticamente.
Mecanismos e Interações
O mecanismo que transforma uma função simples em um método vinculado - e que potencializa @property - é o protocolo de descritor: qualquer objeto com __get__ (e opcionalmente __set__/__delete__) definido em sua classe pode interceptar o acesso a atributos em vez de ser retornado como está.
Descritores que definem __set__ são chamados de descritores de dados, e eles têm prioridade sobre a entrada __dict__ da própria instância com o mesmo nome - que é a razão exata pela qual @property pode impor validação em cada atribuição, mesmo que a instância tecnicamente "tenha" esse atributo.
class Celsius:
def __get__(self, obj, owner):
return obj._celsius
def __set__(self, obj, value):
if value < -273.15:
raise ValueError("below absolute zero")
obj._celsius = value
class Weather:
temperature = Celsius() # a data descriptor beats instance __dict__ lookupMétodos dunder são a segunda metade do modelo: a sintaxe do Python nunca trata suas classes diretamente como casos especiais - len(obj) chama obj.__len__(), obj + outro chama obj.__add__(outro), for x in obj chama obj.__iter__(), e with obj: chama obj.__enter__()/obj.__exit__().
É por isso que implementar um punhado de métodos dunder permite que uma classe personalizada participe totalmente da sintaxe do Python - a linguagem foi projetada em torno de um sistema de ganchos pequeno e consistente, em vez de um grande conjunto de casos especiais integrados.
super() e o MRO interagem com essa mesma busca: super().metodo() não chama "a classe pai" ingenuamente, ele chama a próxima classe após a atual no MRO, que é o que faz a herança múltipla cooperativa (mixins) funcionar corretamente em vez de pular ou chamar bases compartilhadas duas vezes.
Considerações Avançadas e Aplicações
dataclasses e typing.Protocol parecem alternativas à OOP "real", mas ambos operam inteiramente dentro deste mesmo modelo em vez de substituí-lo: @dataclass é um decorador de classe que gera métodos dunder __init__, __repr__ e __eq__ para você no momento da criação da classe, economizando o código repetitivo de escrevê-los manualmente.
typing.Protocol inverte a direção do mesmo modelo de busca: em vez de exigir que o MRO de um objeto inclua uma classe base específica, um Protocol verifica (estaticamente ou em tempo de execução com @runtime_checkable) se a classe de um objeto por acaso define os métodos corretos - o mesmo mecanismo de busca de atributos, apenas verificado estruturalmente em vez de por cadeia de herança.
Metaclasses (type subclasses, ou __init_subclass__ como a alternativa de menor peso) ficam um nível acima: como as classes são em si objetos criados por type, uma metaclasse personalizada pode interceptar a criação da classe em si, que é como frameworks como ORMs registram automaticamente classes de modelo ou validam corpos de classe no momento da definição.
| Abordagem | Força | Fraqueza | Melhor ajuste |
|---|---|---|---|
| Herança (classe base) | Compartilha implementação e impõe um relacionamento "é-um" real | Hierarquias profundas ficam rígidas e difíceis de mudar com segurança | Um relacionamento de especialização genuíno com comportamento compartilhado |
| Composição (tem-um) | Flexível, trocável em tempo de execução, evita hierarquias frágeis | Mais código repetitivo para delegar chamadas explicitamente | Combinando comportamentos independentes sem forçar uma hierarquia de tipos |
typing.Protocol (estrutural) | Não requer herança; funciona com tipos de terceiros que você não pode modificar | Orientado a verificador estático; verificações em tempo de execução cobrem apenas a existência de métodos, não o comportamento | Definindo uma interface para código que você não possui, ou acoplamento mínimo |
@dataclass | Gera métodos dunder repetitivos automaticamente | Ainda é uma classe normal por baixo - não substitui decisões de herança | Registros centrados em dados sem __init__/__eq__ escritos manualmente |
O modo de falha prático que este modelo explica são os problemas de classe base frágil: como a busca de atributos percorre um MRO ativo no momento da chamada em vez de vincular estaticamente, alterar um método de uma classe base pode alterar silenciosamente o comportamento em todas as subclasses que dependiam da versão antiga, o que é um argumento central para favorecer a composição ou mixins pequenos e focados em vez de árvores de herança profundas.
Concepções Errôneas Comuns
- "Classes são apenas projetos, não objetos reais." Uma classe é um objeto de primeira classe (uma instância de
type), é por isso que você pode passar uma classe, armazená-la em uma variável ou anexar atributos a ela diretamente. - "Cada chamada de método busca a função novamente na instância." Métodos residem na classe, não na instância; acessá-los através de uma instância aciona o protocolo de descritor para produzir um método vinculado, não copia a função para lugar nenhum.
- "
super().metodo()chama a classe pai imediata." Ele chama a próxima classe no MRO após a atual, que na herança múltipla nem sempre é a mesma coisa que "o pai" em um sentido simples. - "
isinstance()sempre significa herança real." Com Protocols@runtime_checkable,isinstance()pode retornar verdadeiro puramente porque um objeto por acaso tem os nomes de método corretos, sem nenhuma classe base compartilhada. - "
@dataclassé um tipo diferente de classe." É uma classe comum com métodos dunder gerados; tudo sobre busca de atributos e MRO descrito aqui ainda se aplica a ela inalterado.
FAQs
Uma classe Python é realmente um objeto em si?
Sim - uma classe é uma instância de type (ou uma metaclasse personalizada), é por isso que as classes podem carregar seus próprios atributos e ser passadas como qualquer outro valor.
Qual é a ordem exata que o Python usa para buscar `instancia.nome`?
Ele verifica primeiro o __dict__ da própria instância, depois percorre a ordem de resolução de métodos da classe (a própria classe, depois suas bases em ordem MRO), procurando em cada __dict__ em sequência - a menos que um descritor de dados intercepte a busca primeiro.
O que é um descritor, em termos simples?
Um objeto cuja classe define __get__ (e opcionalmente __set__/__delete__) para que acessá-lo como um atributo de classe execute esse código em vez de retorná-lo diretamente - é o mecanismo por trás de métodos vinculados, @property e @staticmethod.
Por que `@property` vence uma entrada `__dict__` da própria instância com o mesmo nome?
Porque property é um descritor de dados (ele define __set__), e descritores de dados têm prioridade sobre entradas __dict__ da instância na ordem de busca de atributos, que é exatamente o que permite que uma propriedade valide ou transforme valores em cada atribuição.
Como os métodos dunder permitem que minha classe use `+`, `len()` ou `for`?
A sintaxe do Python não trata tipos integrados como casos especiais - a + b chama a.__add__(b), len(obj) chama obj.__len__() e for x in obj chama obj.__iter__() - portanto, implementar os métodos dunder corretos faz com que qualquer classe personalizada participe dessa sintaxe.
O `super()` sempre chama a classe pai da qual herdei diretamente?
Não - ele chama a próxima classe após a atual no MRO, que na herança múltipla pode ser um mixin irmão em vez do que você informalmente chamaria de "pai".
Qual a diferença entre `typing.Protocol` e herdar de uma classe base?
Uma classe base exige uma relação explícita de "é-um" declarada na definição da classe; um Protocol em vez disso verifica estruturalmente se a classe de um objeto por acaso define os nomes de método corretos, sem necessidade de nenhuma relação de herança.
O `@dataclass` substitui os mecanismos normais de classe?
Não - é um decorador que gera métodos dunder comuns (__init__, __repr__, __eq__ e mais) no momento da criação da classe; a classe resultante ainda segue exatamente as mesmas regras de busca de atributos e MRO como qualquer outra classe.
O que uma metaclasse realmente permite fazer?
Como as classes são em si objetos criados por type, uma metaclasse personalizada pode interceptar a criação da classe - validando um corpo de classe, registrando automaticamente a classe ou injetando atributos - antes que qualquer instância dela exista.
Por que hierarquias de herança profundas tendem a se tornar frágeis?
Como a busca de atributos percorre um MRO ativo no momento da chamada em vez de resolver estaticamente, alterar um método em uma classe base pode alterar silenciosamente o comportamento em todas as subclasses que dependem dele, o que se torna mais difícil de raciocinar à medida que a hierarquia se aprofunda.
Quando devo usar composição em vez de herança?
Quando você deseja combinar comportamentos independentes sem se comprometer com uma hierarquia rígida de "é-um" - a composição delega explicitamente a outros objetos, o que é mais verboso, mas muito mais fácil de mudar com segurança do que uma cadeia de herança profunda ou múltipla.
Relacionados
- Noções Básicas de OOP - classes práticas,
selfe atributos de instância - Métodos Dunder / Mágicos - os ganchos específicos em que o modelo de despacho desta página se baseia
- Herança e MRO - a ordem de resolução de métodos em detalhes completos
- Propriedades e Descritores - o protocolo de descritor que esta página introduz
- Protocolos e Tipagem Estrutural - verificações estruturais contra este mesmo modelo de busca
- Dataclasses - métodos dunder gerados em classes comuns
Versões da Stack: Esta página foi escrita para Python 3.14 (estável) e Python 3.13 (manutenção); o protocolo de descritor, MRO e despacho dunder descritos aqui têm sido comportamento estável do CPython em lançamentos recentes do 3.x.