Una dataclass escribe por ti los métodos aburridos. Declaras los campos con anotaciones de tipo y el decorador genera __init__, __repr__ y __eq__.
Está en la biblioteca estándar desde Python 3.7, así que no hay nada que instalar.
El código repetitivo que elimina
La clase vieja y la dataclass de abajo hacen el mismo trabajo.
# the boilerplate a dataclass removes
class PointOld:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'PointOld(x={self.x}, y={self.y})'
def __eq__(self, other):
return isinstance(other, PointOld) and (self.x, self.y) == (other.x, other.y)
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print(p == Point(1, 2))
Imprime:
Point(x=1, y=2)
True
La igualdad compara los campos y no la identidad, que es casi siempre lo que quieres para un objeto de valor.
Valores por defecto y la regla de las listas
Los valores por defecto simples se escriben normal. Un valor por defecto mutable necesita default_factory, que se llama una vez por instancia.
# defaults, and why lists need default_factory
@dataclass
class Basket:
owner: str
items: list = field(default_factory=list)
currency: str = 'INR'
b1, b2 = Basket('ada'), Basket('grace')
b1.items.append('apple')
print(b1)
print(b2)
try:
@dataclass
class Broken:
items: list = []
except ValueError as err:
print(type(err).__name__ + ':', err)
Imprime:
Basket(owner='ada', items=['apple'], currency='INR')
Basket(owner='grace', items=[], currency='INR')
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
Las últimas líneas son la parte útil. Python rechaza el valor por defecto mutable en lugar de dejar que todas las instancias compartan una sola lista, que es lo que pasa con un argumento normal de función.
Instancias congeladas
frozen=True hace que los campos sean de solo lectura y que la instancia se pueda usar como clave, así que puede ir en un diccionario o en un conjunto.
# frozen instances are hashable and read only
@dataclass(frozen=True)
class Config:
host: str
port: int = 8080
c = Config('localhost')
print(c, hash(c) == hash(Config('localhost')))
try:
c.port = 9090
except Exception as err:
print(type(err).__name__ + ':', err)
Imprime:
Config(host='localhost', port=8080) True
FrozenInstanceError: cannot assign to field 'port'
Ordenamiento
order=True genera los métodos de comparación, que comparan los campos en el orden en que se declararon, como haría una tupla.
# order gives you comparisons and sorting
@dataclass(order=True)
class Version:
major: int
minor: int
versions = [Version(1, 4), Version(1, 2), Version(0, 9)]
print(sorted(versions))
print(Version(1, 4) > Version(1, 2))
Imprime:
[Version(major=0, minor=9), Version(major=1, minor=2), Version(major=1, minor=4)]
True
El ordenamiento funciona sin función key.
Valores derivados con post_init
field(init=False) deja un campo fuera del constructor, y __post_init__ corre justo después de él.
# __post_init__ for derived values
@dataclass
class Rect:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
print(Rect(3, 4))
Imprime:
Rect(width=3, height=4, area=12)
asdict y replace
asdict convierte a diccionarios normales, de forma recursiva. replace construye una instancia nueva con algunos campos cambiados, que es como se actualiza una congelada.
# asdict and replace
print(asdict(Basket('ada', ['apple'])))
print(replace(Config('localhost'), port=9090))
Imprime:
{'owner': 'ada', 'items': ['apple'], 'currency': 'INR'}
Config(host='localhost', port=9090)
Dejar un campo fuera del repr o de la igualdad
Útil para secretos, cachés y cualquier cosa ruidosa.
# fields you do not want in repr or comparison
@dataclass
class User:
name: str
token: str = field(repr=False, compare=False)
print(User('ada', 'secret-token'))
print(User('ada', 'secret-token') == User('ada', 'different-token'))
Imprime:
User(name='ada')
True
El token no se imprime y no afecta la igualdad.
Qué recordar
-
El decorador genera
__init__,__repr__y__eq__a partir de los campos anotados. -
Los valores por defecto mutables necesitan
field(default_factory=list). -
frozen=Trueda instancias de solo lectura que se pueden usar como clave. -
asdictyreplacecubren serializar y actualizar.
Si además quieres validación y parseo en los bordes de tu programa, ahí es donde Pydantic se gana su lugar. Para contenedores de datos internos y simples, una dataclass alcanza y no cuesta nada.