Classes e POO em Python: __init__, self, métodos, herança e dataclass
Classe em Python descreve os campos e métodos de um tipo de objeto, e os objetos (instâncias) são criados chamando a classe; o __init__ preenche os campos, e o self aponta para o objeto atual. A herança permite reaproveitar o comportamento do pai e redefinir parte dos métodos, e o decorador @dataclass gera __init__, __repr__ e __eq__ para classes que, na maior parte, armazenam dados.
Uma classe em Python é um modelo: ela descreve quais dados o objeto guarda e o que se pode fazer com eles. O objeto (instância) é um item concreto feito a partir desse modelo. A POO em Python se apoia em três coisas: classes com métodos, herança e métodos especiais como __init__ e __repr__. Vamos ver tudo em ordem, num exemplo único — o controle de gastos.
Para que servem as classes, se já existem dicionários
Os dados podem ser guardados num dicionário: {"title": "Café", "amount": 250}. O problema é que o dicionário não sabe quais chaves estão certas. Um erro de digitação em expense.get("amout", 0) não gera exceção — volta zero, e o relatório começa a mentir sem avisar.
Num objeto de classe, os campos são declarados antecipadamente. O acesso a um atributo inexistente gera AttributeError de imediato, e o Python moderno ainda sugere o nome correto. Além disso, a classe reúne os dados e as operações sobre eles em um só lugar: a despesa sabe calcular o valor com imposto, e o relatório calcula o total sozinho.
Como criar uma classe em Python
A classe é declarada com a palavra class, e o nome, segundo a PEP 8, é escrito em CamelCase. Exemplo mínimo:
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
coffee = Expense("Café", 250)
taxi = Expense("Táxi", 300)
print(coffee.title, coffee.amount) # Café 250
A chamada Expense("Café", 250) cria uma nova instância da classe. coffee e taxi são dois objetos diferentes do mesmo tipo, cada um com seus próprios valores de campos. Para verificar a pertinência, use isinstance(coffee, Expense) — retorna True.
Construtor __init__: inicialização da classe
O __init__ costuma ser chamado de construtor da classe, embora o correto seja «inicializador». O objeto em si é criado pelo método __new__, e o __init__ é chamado logo depois e distribui os argumentos recebidos pelos campos. Ele não precisa devolver nada (na verdade, deve devolver None).
No __init__ é cômodo validar os dados de entrada, para que o programa não tenha objetos incorretos:
class Expense:
def __init__(self, title, amount):
if amount < 0:
raise ValueError("O valor não pode ser negativo")
self.title = title
self.amount = amount
O que é self em Python
self é a referência ao próprio objeto com o qual o método está trabalhando. Quando você escreve coffee.with_tax(), o Python na prática chama Expense.with_tax(coffee) e passa o objeto como primeiro argumento. Por isso, na declaração do método, self vem primeiro, e na chamada ele não é escrito.
self não é palavra reservada, e sim uma convenção: tecnicamente o parâmetro pode ter qualquer nome, mas ninguém faz isso. Erro comum de iniciantes é esquecer o self na declaração do método. Nesse caso a chamada dá TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given.
Métodos da classe e métodos especiais
Um método é uma função declarada dentro da classe. Ele enxerga os campos do objeto por meio de self:
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
def with_tax(self, rate=0.2):
return self.amount * (1 + rate)
def __repr__(self):
return f"Expense({self.title!r}, {self.amount})"
coffee = Expense("Café", 250)
print(coffee.with_tax()) # 300.0
print(coffee) # Expense('Café', 250)
Sem o __repr__, a saída seria algo como <__main__.Expense object at 0x7f...> — inútil para depuração. Métodos com dois sublinhados no início e no fim são chamados especiais (métodos dunder): o Python os invoca sozinho. O __str__ cuida da exibição «amigável» para o usuário, o __eq__ — da comparação com ==, e o __len__ — de len(obj).
@classmethod e @staticmethod
O método comum trabalha com a instância. O método de classe, com o decorador @classmethod, recebe a própria classe (cls) como primeiro argumento — é comum usá-lo como construtor alternativo. O método estático @staticmethod não recebe nem o objeto, nem a classe: é uma função comum que, logicamente, pertence à classe.
class Expense:
def __init__(self, title, amount):
self.title = title
self.amount = amount
@classmethod
def from_csv(cls, line):
title, amount = line.split(";")
return cls(title, float(amount))
@staticmethod
def is_valid_amount(value):
return value >= 0
e = Expense.from_csv("Almoço;520")
print(e.amount) # 520.0
print(Expense.is_valid_amount(-1)) # False
Destrutor da classe: __del__
Existe o método __del__, chamado antes de o objeto ser destruído pelo coletor de lixo. Não convém depender dele para liberar recursos: o momento da chamada não é garantido. Para arquivos e conexões use o gerenciador de contexto with ou o método explícito close().
Atributos da classe e atributos da instância
Os campos definidos por self.name = ... pertencem a um objeto específico. Variáveis declaradas diretamente no corpo da classe são atributos de classe: são comuns a todas as instâncias. Assim costumam ser guardadas as constantes da classe.
class Expense:
CURRENCY = "BRL" # atributo de classe, constante
def __init__(self, title, amount):
self.title = title # atributos da instância
self.amount = amount
a = Expense("Café", 250)
b = Expense("Chá", 150)
print(a.CURRENCY, Expense.CURRENCY) # BRL BRL
Uma armadilha perigosa é o atributo de classe mutável. Se no corpo da classe for declarado rows = [] e os itens forem acrescentados com self.rows.append(...), a lista será compartilhada por todos os objetos. Campos mutáveis devem ser sempre criados no __init__.
Ver os campos de um objeto é possível com vars(obj) ou obj.__dict__, e ler um atributo pelo nome vindo de uma string — com getattr(obj, "amount").
Propriedades da classe: @property
A propriedade é um método que, de fora, é lido como um campo comum, sem parênteses. É cômoda para valores calculados: o total do relatório não precisa ser armazenado separadamente, e você não se esquece de atualizá-lo.
class Report:
def __init__(self):
self.rows = []
def add(self, expense):
self.rows.append(expense)
@property
def total(self):
return sum(e.amount for e in self.rows)
r = Report()
r.add(Expense("Café", 250))
r.add(Expense("Táxi", 300))
print(r.total) # 550
Se for preciso controlar a gravação, acrescenta-se um setter com @total.setter. Sem setter, a atribuição r.total = 0 gera AttributeError.
Sobre encapsulamento: no sentido estrito, não existem campos privados em Python. Um sublinhado (_cache) é uma convenção de «não mexa de fora», e dois (__secret) ativam o name mangling — o nome é modificado para que um herdeiro não sobrescreva o campo por acidente.
Herança de classes em Python
A herança permite criar uma classe filha a partir de uma classe-pai: a filha recebe todos os campos e métodos, e redefine os que precisar. A classe-pai é indicada entre parênteses.
class MonthlyReport(Report):
def __init__(self, month):
super().__init__() # chama o __init__ do pai
self.month = month
def add(self, expense): # redefinição do método
print(f"[{self.month}] + {expense}")
super().add(expense) # mantém o comportamento do pai
m = MonthlyReport("outubro")
m.add(Expense("Café", 250)) # [outubro] + Expense('Café', 250)
print(m.total) # 250 — a propriedade foi herdada
super() devolve um objeto intermediário para chamar os métodos do pai. Esquecer o super().__init__() no __init__ da filha é um erro clássico: o campo rows não é criado, e o primeiro add falha com AttributeError.
Verificar o parentesco é possível com issubclass(MonthlyReport, Report). Todas as classes do Python 3 herdam implicitamente de object — daí os métodos básicos __repr__, __eq__ e outros. O Python também suporta herança múltipla (class C(A, B)); a ordem de busca dos métodos é mostrada por C.__mro__.
Classe abstrata em Python
A classe abstrata define uma interface: quais métodos as subclasses são obrigadas a implementar. Não é possível criar um objeto dela. Para isso serve o módulo abc:
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, report): ...
class CsvExporter(Exporter):
def export(self, report):
return "\n".join(f"{e.title};{e.amount}" for e in report.rows)
Exporter() # TypeError: Can't instantiate abstract class Exporter ...
Uma subclasse que não implementou todos os métodos abstratos também continua abstrata e, ao criar o objeto, dá o mesmo erro.
Composição em vez de hierarquias profundas
Depois de aprender a herança, dá vontade de construir escadas: despesa básica, despesa em dinheiro, despesa em reais — e assim por cinco níveis. Em Python isso é feito raramente. A herança descreve a relação «é um», enquanto a composição descreve «tem»: o relatório não é uma lista de despesas, ele contém essa lista no campo rows.
Regra prática: se você precisa apenas usar a capacidade de outro objeto, coloque-o num campo, em vez de herdar. Para dados muitas vezes basta um dataclass, e a lógica pode ficar em funções comuns.
Dataclasses em Python: classe sem código repetitivo
Para classes que, na maioria das vezes, apenas guardam dados, escrever à mão __init__, __repr__ e __eq__ é cansativo. O decorador @dataclass, do módulo dataclasses, gera esses métodos a partir das anotações dos campos:
from dataclasses import dataclass, field
@dataclass
class Expense:
title: str
amount: float
tags: list[str] = field(default_factory=list)
def __post_init__(self):
if self.amount < 0:
raise ValueError("O valor não pode ser negativo")
e = Expense("Café", 250)
print(e) # Expense(title='Café', amount=250, tags=[])
print(e == Expense("Café", 250)) # True
A validação que antes ficava no __init__ passa para o __post_init__ — ele é chamado logo depois do inicializador gerado. Valores padrão mutáveis são definidos somente por field(default_factory=list): a forma tags: list = [] é recusada pelo dataclass, que gera ValueError.
frozen e slots
O parâmetro frozen=True torna o objeto imutável, e slots=True (a partir do Python 3.10) cria __slots__ e economiza memória. Tentar alterar um campo de um objeto congelado causa FrozenInstanceError. Para obter um objeto com outro valor, cria-se uma cópia:
from dataclasses import dataclass, replace
@dataclass(frozen=True, slots=True)
class Expense:
title: str
amount: float
e = Expense("Café", 250)
# e.amount = 300 -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2) # Expense(title='Café', amount=250) Expense(title='Café', amount=300)
A partir do Python 3.13, a mesma operação é feita pela função universal copy.replace(). Objetos dataclass congelados podem ser colocados em conjuntos e usados como chaves de dicionário — eles têm __hash__.
Novidades nas classes: Self, @override, __slots__
Para a tipagem de classes existem duas ferramentas úteis. typing.Self (Python 3.11, PEP 673) significa «será devolvido um objeto da mesma classe» — funciona também em métodos herdados. O decorador @override (Python 3.12, PEP 698) marca um método que deve redefinir um método do pai: se você errar o nome, o mypy ou o pyright avisam que não há método base com esse nome.
from typing import Self, override
class Report:
def __init__(self):
self.rows = []
def add(self, e) -> Self:
self.rows.append(e)
return self # pode ser usado em cadeia
class MonthlyReport(Report):
@override
def add(self, e) -> Self:
return super().add(e)
No Python 3.15 foi removida uma restrição de nicho: subclasses de tuple podem ter __slots__ não vazios, e __dict__ e __weakref__ podem ser declarados em __slots__ de qualquer classe. No 3.14, essa declaração em um herdeiro de tupla dava TypeError: nonempty __slots__ not supported for subtype of 'tuple'.
Erros frequentes com classes
| Erro | Sintoma | Como corrigir |
|---|---|---|
Esquecido o self no método | TypeError: ... takes 0 positional arguments but 1 was given | O primeiro parâmetro do método é sempre self |
Campo sem self. no __init__ | O valor some depois do __init__ | Escrever self.amount = amount |
| Lista como atributo da classe | Os dados «vazam» entre objetos | Criar listas no __init__ ou com default_factory |
Falta super().__init__() | AttributeError nos campos do pai | Chamar o inicializador do pai |
| Propriedade chamada com parênteses | TypeError: 'int' object is not callable | Ler a propriedade sem parênteses: r.total |
Resumo
A POO em Python começa de forma simples: a classe descreve campos e métodos, o __init__ preenche os campos, e o self aponta para o objeto atual. A herança e o super() permitem ampliar o comportamento, as classes abstratas definem interfaces, e a @property cria campos calculados. Para classes de dados use @dataclass, e para hierarquias profundas prefira a composição. Classes e dataclasses são praticadas na aula do curso de Python, no exemplo do controle de gastos.
Perguntas frequentes
Qual a diferença entre classe e objeto em Python?
A classe é a descrição do tipo: quais campos e métodos ele tem. O objeto, ou instância, é criado chamando a classe, por exemplo Expense("Café", 250), e guarda valores concretos dos campos.
É obrigatório escrever __init__ na classe?
Não. Se o __init__ não for declarado, é usado o herdado de object, que não aceita argumentos. No dataclass quem o gera é o decorador.
Qual a diferença entre __str__ e __repr__?
O __repr__ é a representação não ambígua para o desenvolvedor e para o depurador; o __str__ é a saída amigável para o usuário em print e nas f-strings. Sem __str__, o Python usa o __repr__.
Quando usar dataclass e quando uma classe comum?
O dataclass convém quando a classe guarda sobretudo dados e precisa de __init__, __repr__ e comparação prontos. A classe comum é melhor quando a lógica de inicialização é complexa ou o comportamento importa mais que os dados.
Existem campos privados em Python?
Não há privacidade estrita. Um sublinhado no início do nome é a convenção de «não usar de fora»; dois sublinhados ativam o name mangling, que protege o campo de ser sobrescrito por acidente em uma subclasse.
Este tema é abordado em profundidade, com prática, no curso “Python 3.15: от новичка до профи”.
Ir para o curso