Blog

Quando usar uma classe em Python

A maioria dos códigos Python tem classes que deveriam ser funções. Elas têm um método, nenhum estado que valha a pena proteger, e existem porque alguém aprendeu que código orientado a objetos significa classes.

Então esta série começa pelo argumento contra escrever uma.

A função fantasiada

# a class with one method and no state is a function wearing a costume
class TaxCalculator:
    def __init__(self, rate):
        self.rate = rate
    def calculate(self, amount):
        return amount * self.rate

calc = TaxCalculator(0.2)
print(calc.calculate(100))

def calculate_tax(amount, rate=0.2):
    return amount * rate

print(calculate_tax(100))

Ele imprime:

20.0
20.0

Mesma resposta. A função tem uma linha, não tem etapa de construção, e pode ser testada sem instanciar nada. A classe não te deu nada.

Essa é a classe desnecessária mais comum que existe: ela guarda uma configuração e tem um método que usa essa configuração. Em Python isso é uma função com argumento padrão.

Quando dado é só dado

Se você está apenas agrupando valores que andam juntos, um dicionário ou uma namedtuple diz isso com mais clareza que uma classe:

# a dict is enough when the data is just data
point = {'x': 3, 'y': 4}
print(point['x'], point['y'])

from collections import namedtuple
Point = namedtuple('Point', 'x y')
p = Point(3, 4)
print(p, p.x, p.x + p.y)

Ele imprime:

3 4
Point(x=3, y=4) 3 7

A namedtuple te dá acesso por atributo, um repr legível e imutabilidade em uma linha. A parte 8 desta série compara as quatro opções direito.

O teste que realmente funciona

Não é “isso é um substantivo”. Substantivos estão em toda parte e a maioria deles é dicionário.

O teste é: existe um estado inválido que a classe consegue impedir?

# state that changes together is what a class is for
class BankAccount:
    def __init__(self, balance=0):
        self.balance = balance
        self.history = []

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError(f"deposit must be positive, got {amount!r}")
        self.balance += amount
        self.history.append(('deposit', amount))

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError(f"balance is {self.balance}, cannot withdraw {amount}")
        self.balance -= amount
        self.history.append(('withdraw', amount))

acct = BankAccount(100)
acct.deposit(50)
acct.withdraw(30)
print(acct.balance, acct.history)

Ele imprime:

120 [('deposit', 50), ('withdraw', 30)]

Aqui a classe ganha o lugar dela. balance e history precisam mudar juntos — um depósito que atualiza um e não o outro é um bug, e não existe jeito de escrever esse bug de fora.

Veja ela recusando:

acct2 = BankAccount(10)
try:
    acct2.withdraw(999)
except ValueError as err:
    print(type(err).__name__ + ':', err)
print('balance untouched:', acct2.balance)

Ele imprime:

ValueError: balance is 10, cannot withdraw 999
balance untouched: 10

Um dicionário teria deixado isso passar e te deixado com saldo negativo e nenhuma entrada no histórico.

O que lembrar

  • Um método e nenhum estado significa que aquilo quer ser uma função.

  • Valores que andam juntos querem um dicionário, uma namedtuple ou uma dataclass.

  • Uma classe ganha o lugar dela quando duas ou mais partes do estado precisam mudar juntas, e ela consegue recusar ser colocada num estado inválido.

  • “É um substantivo?” não é o teste. “Consegue impedir um bug?” é.

Se você não consegue nomear um estado inválido que a sua classe elimina, você escreveu um namespace com etapas a mais. Isso nem sempre está errado — mas deveria ser uma decisão, não um reflexo.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.