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

URL: https://skillaria.top/pt/guides/python/classes-python-poo

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.

## FAQ

**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.


---
Source: Skillaria.Top (https://skillaria.top)
