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