Con PEP 557 , las clases de datos se introducen en la biblioteca estándar de Python.
Hacen uso del decorador @dataclass
y se supone que son "tuplas con nombre mutables con valor predeterminado", pero no estoy seguro de entender lo que esto significa realmente y en qué se diferencian de las clases comunes.
¿Qué son exactamente las clases de datos de Python y cuándo es mejor usarlas?
De la especificación PEP :
Se proporciona un decorador de clase que inspecciona una definición de clase para variables con anotaciones de tipo como se define en PEP 526, "Sintaxis para anotaciones de variables". En este documento, dichas variables se denominan campos. Con estos campos, el decorador agrega definiciones de métodos generados a la clase para admitir la inicialización de instancias, una repetición, métodos de comparación y, opcionalmente, otros métodos, como se describe en la sección Especificación. Tal clase se llama clase de datos, pero realmente no hay nada especial en la clase: el decorador agrega métodos generados a la clase y devuelve la misma clase que se le dio.
El generador de @dataclass
agrega métodos a la clase que, de lo contrario, definiría como __repr__
, __init__
, __lt__
y __gt__
.
Las clases de datos son solo clases regulares que están orientadas al estado de almacenamiento, en lugar de contener mucha lógica. Cada vez que crea una clase que consiste principalmente en atributos, crea una clase de datos.
Lo que hace el dataclasses
de clases de datos es facilitar la creación de clases de datos. Se ocupa de una gran cantidad de repetitivo para usted.
Esto es especialmente útil cuando su clase de datos debe ser hashable; porque esto requiere un método __hash__
así como un método __eq__
. Si agrega un método __repr__
personalizado para facilitar la depuración, eso puede volverse bastante detallado:
class InventoryItem: '''Class for keeping track of an item in inventory.''' name: str unit_price: float quantity_on_hand: int = 0 def __init__( self, name: str, unit_price: float, quantity_on_hand: int = 0 ) -> None: self.name = name self.unit_price = unit_price self.quantity_on_hand = quantity_on_hand def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand def __repr__(self) -> str: return ( 'InventoryItem(' f'name={self.name!r}, unit_price={self.unit_price!r}, ' f'quantity_on_hand={self.quantity_on_hand!r})' def __hash__(self) -> int: return hash((self.name, self.unit_price, self.quantity_on_hand)) def __eq__(self, other) -> bool: if not isinstance(other, InventoryItem): return NotImplemented return ( (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand))
Con dataclasses
de datos puede reducirlo a:
from dataclasses import dataclass @dataclass(unsafe_hash=True) class InventoryItem: '''Class for keeping track of an item in inventory.''' name: str unit_price: float quantity_on_hand: int = 0 def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand
El mismo decorador de clase también puede generar métodos de comparación ( __lt__
, __gt__
, etc.) y manejar la inmutabilidad.
Las clases namedtuple
con nombre también son clases de datos, pero son inmutables de forma predeterminada (además de ser secuencias). dataclasses
son mucho más flexibles en este sentido y se pueden estructurar fácilmente de modo que puedan cumplir el mismo rol que una clase de namedtuple
con nombre .
El PEP se inspiró en el proyecto attrs
, que puede hacer aún más (incluidos espacios, validadores, convertidores, metadatos, etc.).
Si desea ver algunos ejemplos, recientemente utilicé dataclasses
de datos para varias de mis soluciones Advent of Code , vea las soluciones para el día 7 , el día 8 , el día 11 y el día 20 .
Si desea utilizar el módulo de dataclasses
de datos en las versiones de Python < 3.7, puede instalar el módulo respaldado (requiere 3.6) o utilizar el proyecto attrs
mencionado anteriormente.
La pregunta ha sido abordada. Sin embargo, esta respuesta agrega algunos ejemplos prácticos para ayudar en la comprensión básica de las clases de datos.
¿Qué son exactamente las clases de datos de Python y cuándo es mejor usarlas?
namedtuple
y otros ."tuplas con nombre mutables con valores predeterminados"
Esto es lo que significa la última frase:
namedtuple
o una clase regular.En comparación con las clases comunes, principalmente ahorra en escribir código repetitivo.
Esta es una descripción general de las características de la clase de datos (TL; DR? Consulte la tabla de resumen en la siguiente sección).
Estas son las funciones que obtiene de forma predeterminada de las clases de datos.
Atributos + Representación + Comparación
import dataclasses @dataclasses.dataclass #@dataclasses.dataclass() # alternative class Color: r : int = 0 g : int = 0 b : int = 0
Estos valores predeterminados se proporcionan configurando automáticamente las siguientes palabras clave en True
:
@dataclasses.dataclass(init=True, repr=True, eq=True)
Hay funciones adicionales disponibles si las palabras clave apropiadas se establecen en True
.
Pedido
@dataclasses.dataclass(order=True) class Color: r : int = 0 g : int = 0 b : int = 0
Los métodos de ordenamiento ahora están implementados (operadores de sobrecarga: < > <= >=
), de manera similar a functools.total_ordering
con pruebas de igualdad más fuertes.
Hashable, Mutable
@dataclasses.dataclass(unsafe_hash=True) # override base `__hash__` class Color: ...
Aunque el objeto es potencialmente mutable (posiblemente no deseado), se implementa un hash.
Hashable, Inmutable
@dataclasses.dataclass(frozen=True) # `eq=True` (default) to be immutable class Color: ...
Ahora se implementa un hash y no se permite cambiar el objeto o asignar atributos.
En general, el objeto es hashable si unsafe_hash=True
o frozen=True
.
Consulte también la tabla de lógica hash original con más detalles.
Para obtener las siguientes funciones, se deben implementar manualmente métodos especiales:
Desembalaje
@dataclasses.dataclass class Color: r : int = 0 g : int = 0 b : int = 0 def __iter__(self): yield from dataclasses.astuple(self)
Mejoramiento
@dataclasses.dataclass class SlottedColor: __slots__ = ["r", "b", "g"] r : int g : int b : int
El tamaño del objeto ahora se reduce:
>>> imp sys >>> sys.getsizeof(Color) 1056 >>> sys.getsizeof(SlottedColor) 888
En algunas circunstancias, __slots__
también mejora la velocidad de creación de instancias y acceso a atributos. Además, las franjas horarias no permiten asignaciones predeterminadas; de lo contrario, se genera un ValueError
.
Vea más sobre tragamonedas en esta publicación de blog .
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+ | Feature | Keyword | Example | Implement in a Class | +----------------------+----------------------+----------------------------------------------------+-----------------------------------------+ | Attributes | init | Color().r -> 0 | __init__ | | Representation | repr | Color() -> Color(r=0, g=0, b=0) | __repr__ | | Comparision* | eq | Color() == Color(0, 0, 0) -> True | __eq__ | | | | | | | Order | order | sorted([Color(0, 50, 0), Color()]) -> ... | __lt__, __le__, __gt__, __ge__ | | Hashable | unsafe_hash/frozen | {Color(), {Color()}} -> {Color(r=0, g=0, b=0)} | __hash__ | | Immutable | frozen + eq | Color().r = 10 -> TypeError | __setattr__, __delattr__ | | | | | | | Unpacking+ | - | r, g, b = Color() | __iter__ | | Optimization+ | - | sys.getsizeof(SlottedColor) -> 888 | __slots__ | +----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
+ Estos métodos no se generan automáticamente y requieren una implementación manual en una clase de datos.
* __ne__
no es necesario y por lo tanto no está implementado .
Post-inicialización
@dataclasses.dataclass class RGBA: r : int = 0 g : int = 0 b : int = 0 a : float = 1.0 def __post_init__(self): self.a : int = int(self.a * 255) RGBA(127, 0, 255, 0.5) # RGBA(r=127, g=0, b=255, a=127)
Herencia
@dataclasses.dataclass class RGBA(Color): a : int = 0
Conversiones
Convierta una clase de datos en una tupla o un dictado, recursivamente :
>>> dataclasses.astuple(Color(128, 0, 255)) (128, 0, 255) >>> dataclasses.asdict(Color(128, 0, 255)) {'r': 128, 'g': 0, 'b': 255}
Limitaciones
Considere esta clase simple Foo
from dataclasses import dataclass @dataclass class Foo: def bar(): pass
Aquí está la comparación integrada de dir()
. En el lado izquierdo está el Foo
sin el decorador @dataclass, y a la derecha está con el decorador @dataclass.
Aquí hay otra diferencia, después de usar el módulo de inspect
para comparar.