Blog

Cuándo usar una clase en Python

La mayoría de los proyectos en Python tienen clases que debieron ser funciones. Tienen un solo método, ningún estado que valga la pena proteger, y existen porque alguien aprendió que el código orientado a objetos significa clases.

Así que esta serie empieza con el argumento en contra de escribir una.

La función disfrazada

# 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))

Imprime:

20.0
20.0

La misma respuesta. La función tiene una línea, no necesita un paso de construcción, y se puede probar sin instanciar nada. La clase no te dio nada.

Esta es la clase innecesaria más común que existe: guarda configuración y tiene un método que la usa. En Python eso es una función con un argumento por defecto.

Cuando los datos son solo datos

Si nada más estás agrupando valores que viajan juntos, un diccionario o un namedtuple lo dice con más claridad que una clase:

# 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)

Imprime:

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

El namedtuple te da acceso por atributo, un repr legible e inmutabilidad en una sola línea. La parte 8 de esta serie compara las cuatro opciones como corresponde.

La prueba que sí funciona

No es «¿esto es un sustantivo?». Los sustantivos están por todas partes y la mayoría son diccionarios.

La prueba es: ¿existe un estado inválido que la clase pueda 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)

Imprime:

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

Aquí la clase se gana su lugar. balance e history tienen que cambiar juntos: un depósito que actualiza uno y no el otro es un error, y no hay forma de escribir ese error desde afuera.

Mírala negarse:

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

Imprime:

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

Un diccionario habría dejado pasar eso y te habría dejado con un saldo negativo y sin la entrada en el historial.

Qué recordar

  • Un solo método y nada de estado significa que quiere ser una función.

  • Los valores que viajan juntos quieren un diccionario, un namedtuple o una dataclass.

  • Una clase se gana su lugar cuando dos o más piezas de estado tienen que cambiar juntas, y puede negarse a quedar en un estado inválido.

  • «¿Es un sustantivo?» no es la prueba. «¿Puede impedir un error?» sí lo es.

Si no puedes nombrar un estado inválido que tu clase descarte, escribiste un espacio de nombres con pasos de más. Eso no siempre está mal, pero debería ser una decisión y no un reflejo.

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.