Fundamentos

Classes e POO em Python: __init__, self, métodos, herança e dataclass

Atualizado: · Skillaria.Top

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

ErroSintomaComo corrigir
Esquecido o self no métodoTypeError: ... takes 0 positional arguments but 1 was givenO 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 classeOs dados «vazam» entre objetosCriar listas no __init__ ou com default_factory
Falta super().__init__()AttributeError nos campos do paiChamar o inicializador do pai
Propriedade chamada com parêntesesTypeError: 'int' object is not callableLer 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.

Aprenda este tema em vídeo

Este tema é abordado em profundidade, com prática, no curso “Python 3.15: от новичка до профи”.

Ir para o curso