1. Da primeira parte aos protocolos da linguagem
Na Parte 1, construímos a base do modelo de dados do Python: objetos possuem identidade, tipo e valor; nomes guardam referências para esses objetos; mutabilidade determina quais mudanças podem acontecer sem substituir o objeto; e containers armazenam referências para outros objetos.
Agora vamos avançar uma camada.
O tipo de um objeto não determina apenas quais valores ele pode representar. Ele também determina quais operações aquele objeto suporta: se possui tamanho, se pode ser percorrido, comparado, indexado, chamado como função, usado em um with e assim por diante (PYTHON SOFTWARE FOUNDATION, 2026a).
É aqui que entram os métodos especiais.
Uma observação importante antes de continuar: na Parte 1 usamos o endereço de memória como modelo mental para identidade. De forma mais rigorosa, Python garante que a identidade de um objeto é estável durante sua existência. No CPython, especificamente, id(obj) corresponde ao endereço de memória do objeto; isso é um detalhe da implementação de referência, não uma garantia da linguagem (PYTHON SOFTWARE FOUNDATION, 2026a).
A mesma distinção será importante quando falarmos sobre garbage collection e finalização: Python especifica o comportamento da linguagem; CPython é uma implementação desse comportamento.
Versão de referência
Os comportamentos e referências deste artigo foram revisados com base na documentação oficial do Python 3.14.7. Detalhes exclusivos do CPython serão identificados explicitamente.
2. O que são métodos especiais
Na documentação oficial, nomes como __len__, __iter__ e __add__ são chamados de special methods, ou métodos especiais, na comunidade também é comum encontrar os termos métodos mágicos ou dunder methods (dunder vem de double underscore por causa do padrão __nome__).
Eles permitem que classes definidas por nós participem de operações que fazem parte da própria sintaxe e das funções embutidas da linguagem.
Por exemplo:
Você escreve Comportamento que Python precisa resolver Métodos relacionadosrepr(obj)
representação oficial
__repr__
str(obj) / print(obj)
representação informal
__str__, com fallback para __repr__
len(obj)
tamanho
__len__
obj[chave]
subscrição
__getitem__
obj[chave] = valor
atribuição por subscrição
__setitem__
for item in obj
iteração
__iter__, __next__
a == b
comparação de igualdade
__eq__
a + b
operação aritmética
__add__, possivelmente __radd__
obj(...)
chamada
__call__
with obj:
gerenciamento de contexto
__enter__, __exit__
Essa tabela é um mapa conceitual, não uma tradução literal linha por linha do que o interpretador executa. É justamente essa diferença que precisamos entender.
2.1. Operação primeiro, método depois
Considere len().
Quando escrevemos:
len([10, 20, 30])
Enter fullscreen mode Exit fullscreen mode
obtemos:
3
Enter fullscreen mode Exit fullscreen mode
O mesmo acontece com uma string:
len("Python")
# 6
Enter fullscreen mode Exit fullscreen mode
Mas len() não precisa possuir uma grande sequência de verificações como:
# modelo que NÃO representa o funcionamento de len()
if isinstance(obj, list):
...
elif isinstance(obj, str):
...
elif isinstance(obj, tuple):
...
Enter fullscreen mode Exit fullscreen mode
Uma classe criada por nós também pode participar desse comportamento:
class Caixa:
def __len__(self) -> int:
return 3
caixa = Caixa()
len(caixa)
# 3
Enter fullscreen mode Exit fullscreen mode
Caixa não herda de list, tuple ou str.
O que importa é que seu tipo oferece o comportamento esperado pela operação de tamanho.
Podemos pensar no caminho conceitualmente assim:
len(caixa)
↓
operação de tamanho
↓
protocolo correspondente
↓
__len__
Enter fullscreen mode Exit fullscreen mode
É esse tipo de contrato comportamental que chamaremos de protocolo ao longo do artigo.
Python não precisa conhecer antecipadamente todas as classes que poderão existir. A classe informa à linguagem quais operações suporta através dos métodos especiais apropriados.
2.2. O lookup de métodos especiais é diferente
Existe, porém, uma nuance fundamental.
Uma primeira aproximação útil é imaginar:
len(obj) ≈ type(obj).__len__(obj)
Enter fullscreen mode Exit fullscreen mode
O símbolo ≈ é proposital. Isso é um modelo mental, e não uma reescrita literal do código executado pelo interpretador.
Invocações implícitas de métodos especiais possuem regras próprias de lookup. Para classes definidas pelo usuário, Python procura esses métodos no tipo do objeto, e não simplesmente no dicionário de atributos da instância (PYTHON SOFTWARE FOUNDATION, 2026b).
Veja a diferença:
class Caixa:
def __len__(self) -> int:
return 3
caixa = Caixa()
caixa.__len__ = lambda: 99
Enter fullscreen mode Exit fullscreen mode
Agora temos:
caixa.__len__()
# 99
Enter fullscreen mode Exit fullscreen mode
A chamada acima é explícita. Estamos acessando normalmente o atributo __len__ da instância.
Mas:
len(caixa)
# 3
Enter fullscreen mode Exit fullscreen mode
continua usando o método definido em Caixa.
Podemos observar o modelo aproximado:
type(caixa).__len__(caixa)
# 3
Enter fullscreen mode Exit fullscreen mode
Portanto:
caixa.__len__()
Enter fullscreen mode Exit fullscreen mode
e:
len(caixa)
Enter fullscreen mode Exit fullscreen mode
não possuem exatamente o mesmo mecanismo de resolução.
Detalhe importante
O lookup implícito de métodos especiais geralmente ignora atributos definidos diretamente na instância e também evita parte do mecanismo normal de
__getattribute__.Por isso, para que operações como
len(obj)funcionem consistentemente, o método especial correspondente deve ser definido na classe, isto é, no tipo do objeto (PYTHON SOFTWARE FOUNDATION, 2026b).
Essa regra explica por que dunder methods não devem ser encarados simplesmente como métodos comuns com nomes estranhos. Eles são pontos de integração entre nossos tipos e o modelo de dados da linguagem.
2.3. Um mapa dos principais grupos
Não precisamos decorar todos os special methods existentes. É mais útil agrupá-los pelo comportamento que permitem implementar:
Categoria Métodos principais Documentação oficial Criação e inicialização__new__, __init__
Basic customization
Finalização
__del__
object.del
Representação
__repr__, __str__, __format__
Basic customization
Acesso a atributos
__getattribute__, __getattr__, __setattr__
Customizing attribute access
Descritores
__get__, __set__, __delete__
Implementing Descriptors
Containers e sequências
__len__, __getitem__, __setitem__, __contains__
Emulating container types
Iteração
__iter__, __next__
Iterator Types
Comparações
__eq__, __lt__, __gt__ etc.
Basic customization
Operações numéricas
__add__, __mul__, __rmul__, __abs__ etc.
Emulating numeric types
Chamabilidade
__call__
Emulating callable objects
Gerenciadores de contexto
__enter__, __exit__
With Statement Context Managers
A tabela é deliberadamente incompleta. O objetivo não é transformar esta parte da série em um catálogo de dunders, mas entender por que eles existem e como diferentes métodos formam comportamentos coerentes.
2.4. Criação: __new__ e __init__
Uma das primeiras distinções importantes aparece na própria criação de objetos.
É comum dizer que “__init__ cria o objeto”, mas isso não é exatamente o que acontece.
Conceitualmente:
Classe(...)
↓
__new__(cls, ...)
↓
nova instância
↓
__init__(instância, ...)
↓
objeto pronto para o chamador
Enter fullscreen mode Exit fullscreen mode
__new__ é chamado para criar e retornar a nova instância.
__init__ recebe uma instância que já existe e realiza sua inicialização ou customização (PYTHON SOFTWARE FOUNDATION, 2026c).
Considere:
from typing import Self
class Conexao:
def __new__(cls, host: str) -> Self:
print("1. __new__ criou a instância")
instancia = super().__new__(cls)
return instancia
def __init__(self, host: str) -> None:
print("2. __init__ inicializou a instância")
self.host = host
Conexao("db.local")
Enter fullscreen mode Exit fullscreen mode
O resultado é:
1. __new__ criou a instância
2. __init__ inicializou a instância
Enter fullscreen mode Exit fullscreen mode
Observe que __new__ recebe cls, e não self.
Isso ocorre porque a instância ainda está sendo criada. O argumento cls informa qual classe foi solicitada, permitindo que o método devolva uma instância apropriada inclusive quando herança está envolvida.
Na implementação mais comum:
super().__new__(cls)
Enter fullscreen mode Exit fullscreen mode
delegamos a criação efetiva para o método da classe base.
É tentador resumir esse processo dizendo:
“
__new__aloca memória.”
Mas essa formulação mistura níveis diferentes.
__new__ é o hook de criação de instâncias exposto pelo modelo de dados Python. O trabalho de baixo nível necessário para produzir o objeto, incluindo detalhes de alocação, pertence ao runtime e à implementação utilizada. Portanto, é mais preciso dizer que __new__ controla a criação e o objeto que será retornado, e não tratá-lo como sinônimo do allocator de memória.
Existe outra regra importante.
__init__ só é chamado automaticamente se __new__ retornar uma instância da classe solicitada — ou de uma subclasse dela (PYTHON SOFTWARE FOUNDATION, 2026c).
Por exemplo:
class Fabrica:
def __new__(cls) -> str:
return "já existe"
def __init__(self) -> None:
print("esta linha não será executada")
resultado = Fabrica()
resultado
# 'já existe'
type(resultado)
# <class 'str'>
Enter fullscreen mode Exit fullscreen mode
Como __new__ retornou uma str, e não uma instância de Fabrica, o __init__ de Fabrica não é executado.
__new__ ganha importância especial ao criar subclasses de tipos imutáveis, como int, str e tuple. Como o objeto imutável não pode ter seu valor alterado depois de criado, mudanças que afetam seu valor frequentemente precisam acontecer durante a própria criação da instância (PYTHON SOFTWARE FOUNDATION, 2026c).
Para classes comuns, entretanto, __init__ continua sendo o ponto de customização necessário na grande maioria dos casos.
2.5. Finalização: __del__ não é um gerenciador de recursos
No outro extremo do ciclo de vida existe:
__del__
Enter fullscreen mode Exit fullscreen mode
A documentação o chama de finalizer. O termo “destrutor” é usado informalmente, mas pode induzir a um modelo mental incorreto (PYTHON SOFTWARE FOUNDATION, 2026c).
Para entender o problema, precisamos separar alguns conceitos.
Um objeto pode deixar de ser alcançável pelo programa:
objeto
↓
nenhum caminho acessível do programa chega até ele
↓
objeto tornou-se inalcançável
Enter fullscreen mode Exit fullscreen mode
Isso não significa, como regra geral da linguagem:
inalcançável = destruído imediatamente
Enter fullscreen mode Exit fullscreen mode
Python permite que uma implementação adie a coleta de lixo ou até deixe de realizá-la em determinadas situações. O que a linguagem garante é que objetos ainda alcançáveis não sejam coletados (PYTHON SOFTWARE FOUNDATION, 2026a).
No CPython, especificamente, existe um esquema baseado principalmente em contagem de referências, complementado por um garbage collector capaz de detectar ciclos. Por isso muitos objetos são liberados rapidamente quando deixam de possuir referências. Esse comportamento, porém, é um detalhe do CPython, não um contrato universal de Python (PYTHON SOFTWARE FOUNDATION, 2026a).
__del__ também possui outras dificuldades:
- pode ser executado durante o encerramento do interpretador;
- outros objetos globais dos quais ele depende podem já estar indisponíveis;
- exceções levantadas dentro de
__del__não são propagadas normalmente; - sua execução pode ocorrer em circunstâncias delicadas, tornando operações bloqueantes — como adquirir um lock — particularmente perigosas (PYTHON SOFTWARE FOUNDATION, 2026c).
Isso torna __del__ uma escolha ruim para ser o único responsável pela liberação de recursos externos importantes.
Exemplos:
- arquivos;
- sockets;
- locks;
- conexões com bancos de dados;
- transações;
- handles fornecidos pelo sistema operacional.
Esses recursos normalmente precisam de um momento determinístico de liberação.
É exatamente aqui que entra with. Por exemplo:
with open("dados.txt", "r") as arquivo:
conteudo = arquivo.read()
Enter fullscreen mode Exit fullscreen mode
O bloco possui um ponto bem definido de entrada e saída.
Conceitualmente:
with recurso:
↓
__enter__()
↓
executa o bloco
↓
__exit__()
Enter fullscreen mode Exit fullscreen mode
Mesmo quando uma exceção ocorre dentro do bloco, o protocolo de context manager fornece um ponto apropriado para executar a lógica de saída (PYTHON SOFTWARE FOUNDATION, 2026d).
Essa diferença é fundamental:
__del__
→ finalização ligada ao ciclo de vida do objeto
with / context manager
→ gerenciamento explícito do ciclo de vida de um recurso
Enter fullscreen mode Exit fullscreen mode
Para recursos que precisam ser liberados em um momento previsível, prefira context managers ou APIs explícitas, não dependa exclusivamente de __del__.
2.6. Aritmética: NotImplemented e operações refletidas
Métodos especiais também permitem que objetos participem de operadores.
Alguns exemplos:
Operação Métodoa + b
__add__
a - b
__sub__
a * b
__mul__
a / b
__truediv__
a ** b
__pow__
-a
__neg__
abs(a)
__abs__
Mas implementar operadores corretamente exige mais do que simplesmente escrever o cálculo.
Considere uma duração armazenada em minutos:
from __future__ import annotations
from dataclasses import dataclass
from types import NotImplementedType
@dataclass(frozen=True)
class Duracao:
minutos: int
def __add__(self, outra: object) -> Duracao | NotImplementedType:
if not isinstance(outra, Duracao):
return NotImplemented
return Duracao(self.minutos + outra.minutos)
def __mul__(self, fator: object) -> Duracao | NotImplementedType:
if not isinstance(fator, int):
return NotImplemented
return Duracao(self.minutos * fator)
__rmul__ = __mul__
def __str__(self) -> str:
horas, minutos = divmod(self.minutos, 60)
return f"{horas}h{minutos:02d}"
Enter fullscreen mode Exit fullscreen mode
Agora:
Duracao(90) + Duracao(45)
# Duracao(minutos=135)
3 * Duracao(50)
# Duracao(minutos=150)
print(Duracao(135))
# 2h15
Enter fullscreen mode Exit fullscreen mode
Mas:
Duracao(90) + 45
# TypeError
Enter fullscreen mode Exit fullscreen mode
O ponto central está nesta linha:
return NotImplemented
Enter fullscreen mode Exit fullscreen mode
NotImplemented é um objeto singleton especial utilizado pelos métodos numéricos e pelas comparações quando aquele método não implementa a operação para o par de operandos recebido (PYTHON SOFTWARE FOUNDATION, 2026e).
Isso é diferente de dizer:
“Nunca levante uma exceção dentro de um operador.”
Essa regra seria excessivamente ampla.
Se o problema é:
“Meu
__add__não sabe somarDuracaocom esse tipo de objeto.”
então NotImplemented é normalmente a resposta apropriada.
Se a operação é suportada, mas um valor recebido viola alguma regra legítima do domínio, uma exceção adequada ainda pode fazer sentido.
E por que existe __rmul__?
Considere:
3 * Duracao(50)
Enter fullscreen mode Exit fullscreen mode
Neste caso específico, int está à esquerda.
Para esse par de tipos:
int.__mul__(3, Duracao(50))
Enter fullscreen mode Exit fullscreen mode
não sabe produzir um resultado e devolve:
NotImplemented
Enter fullscreen mode Exit fullscreen mode
Isso permite que Python tente a forma refletida da operação no outro operando:
Duracao.__rmul__(Duracao(50), 3)
Enter fullscreen mode Exit fullscreen mode
Nosso método aceita o fator inteiro e devolve:
Duracao(150)
Enter fullscreen mode Exit fullscreen mode
Um modelo simplificado é:
a * b
↓
método apropriado suporta a operação?
↓ não
NotImplemented
↓
tentativa refletida
↓
suporta?
├─ sim → resultado
└─ não → TypeError
Enter fullscreen mode Exit fullscreen mode
Há uma nuance importante: essa não é sempre uma regra rígida de “lado esquerdo primeiro, lado direito depois”.
Quando os operandos possuem tipos diferentes e o tipo da direita é uma subclasse do tipo da esquerda, o método refletido da subclasse pode receber prioridade. Isso permite que a implementação mais específica tenha a oportunidade de controlar o resultado (PYTHON SOFTWARE FOUNDATION, 2026f).
Para int e Duracao, entretanto, não existe essa relação de herança, e o fluxo descrito acima representa corretamente:
3 * Duracao(50)
Enter fullscreen mode Exit fullscreen mode
Neste exemplo:
__rmul__ = __mul__
Enter fullscreen mode Exit fullscreen mode
é válido porque multiplicar uma duração por um escalar possui a mesma lógica independentemente da ordem sintática.
Isso não deve ser copiado automaticamente para qualquer operador: operações como subtração e divisão não são comutativas.
NotImplementednão éNotImplementedError
NotImplementedé um valor retornado por protocolos como operações numéricas e comparações para indicar que aquela combinação de operandos não é suportada.
NotImplementedErroré uma exceção, normalmente utilizada quando uma API ou implementação deliberadamente deixa determinada operação sem implementação.A documentação oficial explicita que os dois não são intercambiáveis (PYTHON SOFTWARE FOUNDATION, 2026g).
2.7. Atributos: __getattr__ e __setattr__
O acesso:
obj.nome
Enter fullscreen mode Exit fullscreen mode
também passa pelo modelo de dados.
Antes de chegar a __getattr__, Python realiza o mecanismo normal de busca de atributos, que envolve a instância, sua classe, a hierarquia de classes e, quando aplicável, descriptors.
Só quando essa busca termina em AttributeError, __getattr__ funciona como fallback (PYTHON SOFTWARE FOUNDATION, 2026h).
Conceitualmente:
obj.nome
↓
busca normal de atributos
↓
encontrou?
├─ sim → devolve o valor
└─ não
↓
__getattr__(obj, "nome")
Enter fullscreen mode Exit fullscreen mode
Isso é importante porque __getattr__ não participa de toda leitura de atributos.
Existe outro método:
__getattribute__
Enter fullscreen mode Exit fullscreen mode
que participa incondicionalmente do acesso a atributos de instâncias e permite controlar o processo de forma muito mais ampla.
Não vamos aprofundá-lo aqui, mas a distinção é importante:
__getattribute__
→ participa de toda leitura normal
__getattr__
→ fallback quando a busca falha
Enter fullscreen mode Exit fullscreen mode
Agora podemos construir uma configuração somente leitura:
class Config:
_dados: dict[str, str]
def __init__(self, **valores: str) -> None:
object.__setattr__(self, "_dados", valores)
def __getattr__(self, nome: str) -> str:
try:
return self._dados[nome]
except KeyError:
raise AttributeError(f"'{nome}' não existe na configuração") from None
def __setattr__(self, nome: str, valor: str) -> None:
raise AttributeError("Config é somente leitura")
Enter fullscreen mode Exit fullscreen mode
Uso:
cfg = Config(
host="localhost",
porta="5432",
)
cfg.host
# 'localhost'
cfg.porta
# '5432'
Enter fullscreen mode Exit fullscreen mode
host e porta não existem como atributos normais da instância.
Quando:
cfg.host
Enter fullscreen mode Exit fullscreen mode
não é encontrado pela busca normal, __getattr__ recebe:
nome == "host"
Enter fullscreen mode Exit fullscreen mode
e procura a informação em:
self._dados
Enter fullscreen mode Exit fullscreen mode
Por que _dados começa com underscore?
Um único underscore inicial é uma convenção de uso interno usada para comunicar:
“Este nome faz parte da implementação interna e não deveria ser tratado como API pública.”
Ele não cria verdadeiro controle de acesso.
Ainda é possível escrever:
cfg._dados
Enter fullscreen mode Exit fullscreen mode
Python não bloqueia esse acesso.
Isso é diferente de:
__dados
Enter fullscreen mode Exit fullscreen mode
Dois underscores iniciais ativam um mecanismo chamado name mangling, que transforma o nome internamente para reduzir colisões acidentais, principalmente em cenários de herança.
Para nosso exemplo, _dados comunica exatamente o que queremos: trata-se de um detalhe não público da implementação.
E por que usamos object.__setattr__?
Agora observe:
def __setattr__(self, nome: str, valor: str) -> None:
raise AttributeError("Config é somente leitura")
Enter fullscreen mode Exit fullscreen mode
__setattr__ é chamado quando tentamos realizar uma atribuição de atributo (PYTHON SOFTWARE FOUNDATION, 2026h).
Portanto:
cfg.host = "outro"
Enter fullscreen mode Exit fullscreen mode
resulta em:
AttributeError: Config é somente leitura
Enter fullscreen mode Exit fullscreen mode
O mesmo aconteceria dentro do próprio __init__ se escrevêssemos:
self._dados = valores
Enter fullscreen mode Exit fullscreen mode
Essa instrução também é uma atribuição de atributo:
self._dados = valores
↓
self.__setattr__("_dados", valores)
↓
AttributeError
Enter fullscreen mode Exit fullscreen mode
Por isso fazemos:
object.__setattr__(self, "_dados", valores)
Enter fullscreen mode Exit fullscreen mode
Não estamos “desligando” o modelo de atributos inteiro. Estamos chamando diretamente a implementação da classe base e, com isso, evitando nosso Config.__setattr__ customizado.
A própria documentação recomenda esse padrão quando um __setattr__ precisa realizar uma atribuição real na instância (PYTHON SOFTWARE FOUNDATION, 2026h).
Uma implementação como:
def __setattr__(self, nome, valor):
self.nome = valor
Enter fullscreen mode Exit fullscreen mode
chamaria novamente __setattr__, que executaria outra atribuição, que chamaria __setattr__ outra vez…
Quando é necessário delegar ao comportamento base, use explicitamente object.__setattr__.
Por que transformar KeyError em AttributeError?
Dentro de __getattr__ temos:
try:
return self._dados[nome]
except KeyError:
raise AttributeError(f"'{nome}' não existe na configuração") from None
Enter fullscreen mode Exit fullscreen mode
O dicionário expressa ausência através de:
KeyError
Enter fullscreen mode Exit fullscreen mode
Mas estamos implementando acesso a atributos.
Nesse protocolo, a ausência deve ser comunicada através de:
AttributeError
Enter fullscreen mode Exit fullscreen mode
Isso é importante porque outros mecanismos de Python também dependem dessa exceção para determinar se um atributo existe. from None suprime a exibição automática do encadeamento com o KeyError anterior no traceback, deixando visível apenas a exceção que representa corretamente a abstração pública da classe (PYTHON SOFTWARE FOUNDATION, 2026j). Sem from None, Python mostraria também o contexto da exceção anterior.
A próxima camada: descriptors
Até aqui customizamos operações diretamente sobre a instância.
Descriptors resolvem outro problema: permitir que um atributo armazenado na classe controle o que acontece quando é lido, escrito ou removido.
O protocolo utiliza:
__get__
__set__
__delete__
Enter fullscreen mode Exit fullscreen mode
Conceitualmente:
obj.atributo
↓
Python encontra um objeto descriptor na classe
↓
__get__(...)
Enter fullscreen mode Exit fullscreen mode
ou, durante escrita:
obj.atributo = valor
↓
__set__(...)
Enter fullscreen mode Exit fullscreen mode
Esse mecanismo está por trás de recursos fundamentais da própria linguagem, incluindo property, métodos ligados e classmethod (PYTHON SOFTWARE FOUNDATION, 2026k).
Por exemplo:
class Pessoa:
def __init__(self, nome: str) -> None:
self._nome = nome
@property
def nome(self) -> str:
return self._nome
Enter fullscreen mode Exit fullscreen mode
O objeto criado por property participa do protocolo de descriptors.
Não precisamos aprofundar a mecânica inteira agora. O ponto importante é perceber que acesso a atributos também é extensível através de protocolos do modelo de dados.
2.8. Protocolos são mais do que nomes de métodos
Podemos agora refinar a definição.
Um protocolo não significa apenas:
“Existe um método com determinado nome.”
Ele também envolve o contrato semântico esperado daquele método.
Por exemplo:
-
__len__deve representar tamanho e retornar um inteiro não negativo; -
__getitem__de uma sequência deve usar exceções apropriadas para índices inválidos; - um iterador deve sinalizar seu fim através de
StopIteration; -
__eq__pode devolverNotImplementedpara operandos que não sabe comparar; -
__hash__precisa permanecer coerente com igualdade.
É possível escrever um método com o nome correto e ainda implementar o protocolo de maneira ruim.
Por isso, vamos começar aplicando esse princípio a uma única classe, que continuará evoluindo na próxima parte da série.
Nossa base será um vetor N-dimensional:
class Vector:
def __init__(self, *componentes: float) -> None:
self._componentes = list(componentes)
Enter fullscreen mode Exit fullscreen mode
Agora podemos criar:
Vector(3, 4)
Vector(1, 2, 3)
Vector(2, 5, 8, 13)
Enter fullscreen mode Exit fullscreen mode
Os componentes ficam armazenados em uma coleção interna:
_componentes
Enter fullscreen mode Exit fullscreen mode
O Vector deste artigo terá duas decisões de projeto importantes:
- Os valores dos componentes poderão ser alterados;
- A quantidade de componentes permanecerá fixa depois da construção.
Portanto, ele será mutável quanto aos valores, mas não oferecerá operações que adicionem ou removam dimensões.
Para fins didáticos também permitiremos:
Vector()
Enter fullscreen mode Exit fullscreen mode
representando um vetor sem componentes. Um domínio real poderia rejeitar essa construção caso ela não fizesse sentido.
A partir daqui, cada novo método deve respeitar essas mesmas decisões. Nesta parte, começaremos pela representação textual; na próxima, continuaremos evoluindo o mesmo Vector através dos protocolos de tamanho, indexação, iteração e comparação.
3. Representação: __repr__ e __str__
Comecemos pelo comportamento mais visível.
Sem __repr__ ou __str__ customizados:
v = Vector(3, 4)
v
Enter fullscreen mode Exit fullscreen mode
no CPython normalmente produz algo parecido com:
<__main__.Vector object at 0x...>
Enter fullscreen mode Exit fullscreen mode
O formato exato dessa representação padrão não deve ser tratado como parte do contrato da nossa classe.
O que queremos responder é:
Como um
Vectordeve se representar textualmente?
Python oferece dois métodos principais para isso.
Aspecto__repr__
__str__
Papel na documentação
representação oficial
representação informal ou facilmente imprimível
Objetivo comum
rica em informação e pouco ambígua
conveniente e legível
Uso típico
REPL, debugging, logs técnicos
apresentação para leitura humana
Acionado por
repr(v), f"{v!r}"
str(v), print(v), f"{v}", f"{v!s}"
A distinção “desenvolvedor versus usuário final” pode ser uma analogia útil, mas não é a definição normativa.
A documentação define __repr__ como responsável pela representação “oficial” e recomenda, quando possível, uma forma parecida com uma expressão Python capaz de recriar um objeto equivalente. Já __str__ pode produzir uma representação mais conveniente ou concisa (PYTHON SOFTWARE FOUNDATION, 2026c).
Vamos começar por __repr__:
class Vector:
# ... __init__ da seção anterior
def __repr__(self) -> str:
componentes = ", ".join(
repr(componente)
for componente in self._componentes
)
return f"Vector({componentes})"
Enter fullscreen mode Exit fullscreen mode
Agora:
v = Vector(3, 4)
repr(v)
# 'Vector(3, 4)'
v
# Vector(3, 4)
Enter fullscreen mode Exit fullscreen mode
E para dimensões diferentes:
Vector(1, 2, 3)
# Vector(1, 2, 3)
Vector()
# Vector()
Enter fullscreen mode Exit fullscreen mode
A representação acompanha os dados realmente armazenados. Não existe mais nenhuma hipótese hardcoded de que um vetor precise possuir apenas x e y.
Agora adicionamos uma forma mais compacta para __str__:
class Vector:
# ...
def __str__(self) -> str:
componentes = ", ".join(
str(componente)
for componente in self._componentes
)
return f"({componentes})"
Enter fullscreen mode Exit fullscreen mode
Com isso:
v = Vector(3, 4)
repr(v)
# 'Vector(3, 4)'
str(v)
# '(3, 4)'
print(v)
# (3, 4)
f"{v}"
# '(3, 4)'
f"{v!s}"
# '(3, 4)'
f"{v!r}"
# 'Vector(3, 4)'
Enter fullscreen mode Exit fullscreen mode
O fallback de __str__
Existe ainda uma relação importante entre os dois métodos.
Se definirmos __repr__, mas não __str__, a representação oficial também é utilizada quando uma representação informal é necessária (PYTHON SOFTWARE FOUNDATION, 2026c).
Por isso, uma classe com apenas:
def __repr__(self) -> str:
return "..."
Enter fullscreen mode Exit fullscreen mode
já melhora tanto:
repr(obj)
Enter fullscreen mode Exit fullscreen mode
quanto:
str(obj)
print(obj)
Enter fullscreen mode Exit fullscreen mode
O contrário não possui a mesma relação: implementar apenas __str__ não substitui a necessidade de uma representação oficial apropriada.
E os containers?
Observe:
v = Vector(3, 4)
[v]
# [Vector(3, 4)]
Enter fullscreen mode Exit fullscreen mode
Mesmo que:
str(v)
Enter fullscreen mode Exit fullscreen mode
produza:
(3, 4)
Enter fullscreen mode Exit fullscreen mode
a representação de uma list utiliza a representação oficial de seus elementos.
Isso é útil para debugging:
vetores = [
Vector(1, 2),
Vector(3, 4),
]
vetores
# [Vector(1, 2), Vector(3, 4)]
Enter fullscreen mode Exit fullscreen mode
O container consegue mostrar claramente quais objetos contém, sem depender da versão mais informal produzida por __str__.
4. Conclusão
Ao longo desta parte, saímos da ideia de que métodos como __len__, __repr__ ou __add__ são apenas nomes especiais que Python chama automaticamente. O ponto central é mais amplo: eles são pontos de integração entre os nossos tipos e os protocolos da linguagem.
Quando escrevemos:
len(obj)
Enter fullscreen mode Exit fullscreen mode
ou:
a * b
Enter fullscreen mode Exit fullscreen mode
ou ainda:
obj.nome
Enter fullscreen mode Exit fullscreen mode
não estamos apenas chamando funções ou operadores isolados. Estamos** acionando comportamentos definidos pelo modelo de dados, cada um com suas próprias regras de resolução e seus próprios contratos**.
Foi por isso que, ao longo do artigo, vimos que:
-
len(obj)não é equivalente a uma chamada comum deobj.__len__(); -
__new__e__init__participam de momentos diferentes da criação de uma instância; -
__del__não deve ser confundido com gerenciamento determinístico de recursos; -
NotImplementedfaz parte da negociação entre operandos; -
__getattr__funciona como fallback no acesso a atributos; - descriptors formam outro protocolo por trás de mecanismos como
property; -
__repr__e__str__representam contratos diferentes para a representação textual de um objeto.
Esses exemplos também mostram por que implementar um dunder method não significa apenas usar o nome correto. Para participar bem de um protocolo, a classe precisa respeitar a semântica esperada daquela operação.
O Vector introduzido no final desta parte será nosso ponto de continuidade.
Até aqui, definimos como ele armazena seus componentes e como deve se representar:
Vector(3, 4)
Enter fullscreen mode Exit fullscreen mode
ou, em sua forma informal:
(3, 4)
Enter fullscreen mode Exit fullscreen mode
Na próxima parte, a pergunta muda.
Em vez de observar protocolos de forma mais isolada, vamos acompanhar o que acontece quando vários deles precisam coexistir no mesmo tipo. Vamos decidir, entre outras coisas:
len(v)
Enter fullscreen mode Exit fullscreen mode
deve representar o quê?
Como:
v[0]
v[-1]
v[1:3]
Enter fullscreen mode Exit fullscreen mode
devem se comportar?
O que torna:
for componente in v:
...
Enter fullscreen mode Exit fullscreen mode
possível?
E se o Vector for mutável, como isso afeta:
v1 == v2
Enter fullscreen mode Exit fullscreen mode
e sua possibilidade de ser usado em um set ou como chave de dict?
É nesse ponto que os protocolos deixam de parecer recursos independentes e passam a revelar algo ainda mais importante sobre o modelo de dados: decisões de comportamento em uma parte da classe podem impor consequências sobre várias outras.
Na Parte 3, continuaremos justamente daí, evoluindo o mesmo Vector através dos protocolos de tamanho, indexação, iteração, igualdade e hashing.
Referências
PYTHON SOFTWARE FOUNDATION. 3.1. Objects, values and types. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026a. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#objects-values-and-types. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 3.3.13. Special method lookup. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026b. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#special-method-lookup. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 3.3.1. Basic customization. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026c. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#basic-customization. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 3.3.9. With Statement Context Managers. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026d. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#with-statement-context-managers. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 3.2.2. NotImplemented. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026e. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#notimplemented. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 3.3.8. Emulating numeric types. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026f. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#emulating-numeric-types. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. NotImplementedError. In: Built-in Exceptions — Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026g. Disponível em: https://docs.python.org/3.14/library/exceptions.html#NotImplementedError. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 3.3.2. Customizing attribute access. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026h. Disponível em: https://docs.python.org/3.14/reference/datamodel.html#customizing-attribute-access. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 9.6. Private Variables. In: The Python Tutorial — Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026i. Disponível em: https://docs.python.org/3.14/tutorial/classes.html#private-variables. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. 7.8. The raise statement. In: Python Language Reference — Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026j. Disponível em: https://docs.python.org/3.14/reference/simple_stmts.html#the-raise-statement. Acesso em: 14 ago. 2026.
PYTHON SOFTWARE FOUNDATION. Descriptor Guide — Descriptor protocol. In: Python 3.14.7 documentation. [S. l.]: Python Software Foundation, 2026k. Disponível em: https://docs.python.org/3.14/howto/descriptor.html#descriptor-protocol. Acesso em: 14 ago. 2026.
Nota editorial: Este artigo foi desenvolvido e revisado pelo autor com apoio de IA nas etapas de pesquisa, auditoria técnica e revisão editorial. As afirmações técnicas foram verificadas com base nas fontes oficiais indicadas nas referências.