¿Qué son las clases de datos y en qué se diferencian de las clases comunes?
PEP 557 introduce clases de datos en la biblioteca estándar de Python. Dice que al aplicar el @dataclass
decorador que se muestra a continuación, generará "entre otras cosas, un __init__()
".
from dataclasses import dataclass
@dataclass
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
También dice que las clases de datos son "tuplas nombradas mutables con valor predeterminado", pero no entiendo qué significa esto, ni en qué se diferencian las clases de datos de las clases comunes.
¿Qué son las clases de datos y cuándo es mejor utilizarlas?
Las clases de datos son simplemente clases normales que están orientadas a almacenar el estado, en lugar de contener mucha lógica. Cada vez que creas una clase que consta principalmente de atributos, creas una clase de datos.
Lo que hace el dataclasses
módulo es facilitar la creación de clases de datos. Se ocupa de muchos textos repetitivos por usted.
Esto es especialmente útil cuando su clase de datos debe ser hash; porque esto requiere un __hash__
método además de un __eq__
método. Si agrega un __repr__
método personalizado para facilitar la depuración, 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
puedes 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
(Ejemplo basado en el ejemplo de PEP ).
El mismo decorador de clase también puede generar métodos de comparación ( __lt__
, __gt__
etc.) y manejar la inmutabilidad.
namedtuple
Las clases 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 pueden estructurarse fácilmente de manera que puedan cumplir el mismo rol que una namedtuple
clase .
El PEP se inspiró en el attrs
proyecto , que puede hacer aún más (incluidos espacios, validadores, convertidores, metadatos, etc.).
Si desea ver algunos ejemplos que utilicé recientemente dataclasses
para varias de mis soluciones Advent of Code , consulte las soluciones para el día 7 , el día 8 , el día 11 y el día 20 .
Si desea utilizar dataclasses
el módulo en versiones de Python <3.7, puede instalar el módulo respaldado (requiere 3.6) o utilizar el attrs
proyecto mencionado anteriormente.
Descripción general
La cuestión 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?
- generadores de código : genera código repetitivo; puede optar por implementar métodos especiales en una clase normal o hacer que una clase de datos los implemente automáticamente.
- Contenedores de datos : estructuras que contienen datos (por ejemplo, tuplas y dictados), a menudo con acceso a atributos con puntos, como clases
namedtuple
y otros .
"tuplas con nombres mutables con valores predeterminados"
Esto es lo que significa la última frase:
- mutable : de forma predeterminada, los atributos de clase de datos se pueden reasignar. Opcionalmente, puede hacerlos inmutables (consulte los ejemplos a continuación).
- nametuple : tiene acceso a atributos punteados como una clase regular
namedtuple
o una clase regular. - predeterminado : puede asignar valores predeterminados a los atributos.
En comparación con las clases comunes, principalmente ahorra en escribir código repetitivo.
Características
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).
Lo que obtienes
Estas son las características que obtienes 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 estableciendo automáticamente las siguientes palabras clave en True
:
@dataclasses.dataclass(init=True, repr=True, eq=True)
¿Qué puedes activar?
Hay funciones adicionales disponibles si se configuran las palabras clave adecuadas en True
.
Orden
@dataclasses.dataclass(order=True)
class Color:
r : int = 0
g : int = 0
b : int = 0
Los métodos de pedido ahora están implementados (operadores de sobrecarga:) < > <= >=
, de manera similar a functools.total_ordering
las pruebas de igualdad más estrictas.
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 ni asignar atributos.
En general, el objeto es hash si cualquiera de los dos unsafe_hash=True
o frozen=True
.
Consulte también la tabla de lógica hash original con más detalles.
Mejoramiento
@dataclasses.dataclass(slots=True) # py310+
class SlottedColor:
#__slots__ = ["r", "b", "g"] # alternative
r : int
g : int
b : int
El tamaño del objeto ahora se reduce:
>>> imp sys
>>> sys.getsizeof(Color)
1056
>>> sys.getsizeof(SlottedColor)
888
slots=True
fue agregado en Python 3.10. (Gracias @ajskateboarder).
En algunas circunstancias, slots=True
/ __slots__
también mejora la velocidad de creación de instancias y acceso a atributos. Además, los espacios no permiten asignaciones predeterminadas; de lo contrario, ValueError
se eleva a. Si __slot__
ya existe, slots=True
provocará un archivo TypeError
.
Vea más sobre las tragamonedas en esta publicación de blog .
Vea más sobre los argumentos agregados en Python 3.10+ : match_args
, kw_only
, slots
.weakref_slot
lo que no entiendes
Para obtener las siguientes funciones, se deben implementar métodos especiales manualmente:
Desembalaje
@dataclasses.dataclass
class Color:
r : int = 0
g : int = 0
b : int = 0
def __iter__(self):
yield from dataclasses.astuple(self)
Tabla de resumen
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
| 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__ |
| Optimization | slots | sys.getsizeof(SlottedColor) -> 888 | __slots__ |
| | | | |
| Unpacking+ | - | r, g, b = Color() | __iter__ |
+----------------------+----------------------+----------------------------------------------------+-----------------------------------------+
* __ne__
no es necesario y por lo tanto no se implementa .
+ Estos métodos no se generan automáticamente y requieren implementación manual en una clase de datos.
Características adicionales
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 dict, de forma recursiva :
>>> dataclasses.astuple(Color(128, 0, 255))
(128, 0, 255)
>>> dataclasses.asdict(Color(128, 0, 255))
{'r': 128, 'g': 0, 'b': 255}
Limitaciones
- Carece de mecanismos para manejar argumentos destacados
- Trabajar con clases de datos anidadas puede resultar complicado
Referencias
- Charla de R. Hettinger sobre clases de datos: el generador de código para acabar con todos los generadores de código
- Charla de T. Hunner sobre clases más fáciles: clases de Python sin todo el cruft
- Documentación de Python sobre detalles de hash
- Guía de Real Python sobre la guía definitiva para clases de datos en Python 3.7
- Publicación del blog de A. Shaw sobre un breve recorrido por las clases de datos de Python 3.7
- Repositorio github de E. Smith sobre clases de datos
De la especificación PEP :
Se proporciona un decorador de clases que inspecciona una definición de clase en busca de variables con anotaciones de tipo como se define en PEP 526, "Sintaxis para anotaciones de variables". En este documento, dichas variables se denominan campos. Usando 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. Esta clase se llama clase de datos, pero en realidad no tiene nada de especial: el decorador agrega métodos generados a la clase y devuelve la misma clase que se le dio.
El @dataclass
generador agrega métodos a la clase que de otro modo tendrías que definir tú mismo como __repr__
, __init__
, __lt__
y __gt__
.
Considere esta clase simpleFoo
from dataclasses import dataclass
@dataclass
class Foo:
def bar():
pass
Aquí está la dir()
comparación incorporada. En el lado izquierdo está Foo
sin el decorador @dataclass y en el derecho está con el decorador @dataclass.
Aquí hay otra diferencia, después de usar el inspect
módulo para comparar.