PEP 557*
versus the world
* Data Classes
Guillaume Gelin
ramnes.eu 🇪🇺 🇫🇷
Let's go back in time...
collections.namedtuple
>>> from collections import namedtuple
>>>
>>> InventoryItem = namedtuple("InventoryItem",
... ["name", "unit_price", "quantity"])
...
>>> item = InventoryItem("hammer", 10.49, 12)
>>> total_cost = item.unit_price * item.quantity
>>> total_cost
125.88
Properties? Defaults?
typing.NamedTuple
>>> from typing import NamedTuple
>>>
>>> class InventoryItem(NamedTuple):
... name: str
... unit_price: float
... quantity: int = 0
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem("hammer", 10.49, 12)
>>> item.total_cost
125.88
Mutable defaults?
...
... def __new__(cls, name, unit_price, quantity=0, related_items=None):
... if related_items is None:
... related_items = []
... return super().__new__(cls, name, unit_price, quantity, related_items)
...
$ python smartnamedtuple.py
Traceback (most recent call last):
File "smartnamedtuple.py", line 4, in <module>
class InventoryItem(NamedTuple):
File "/usr/lib64/python3.6/typing.py", line 2163, in __new__
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
AttributeError: Cannot overwrite NamedTuple attribute __new__
>>> from typing import NamedTuple, List
>>>
>>> class InventoryItem(NamedTuple):
... name: str
... unit_price: float
... quantity: int = 0
... related_items: List = None
...
... def __real_new__(cls, name, unit_price, quantity=0, related_items=None):
... if related_items is None:
... related_items = []
... return tuple.__new__(cls, [name, unit_price, quantity, related_items])
...
>>> InventoryItem.__new__ = InventoryItem.__real_new__
>>>
>>> item = InventoryItem("hammer", 10.49, 12)
>>> item.related_items
[]
PEP 557
Python 3.6
$ pip install dataclasses
Eric V. Smith
-
NamedTuple-ish
-
Real defaults
-
Mutability
@dataclass
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class InventoryItem:
... name: str
... unit_price: float
... quantity: int = 0
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
-
__init__
-
__repr__
-
__str__
-
Explicit __hash__
-
Lots of metadata
>>> from dataclasses import dataclass, field
>>> from typing import List
>>>
>>> @dataclass
... class InventoryItem:
... name: str
... unit_price: float
... quantity: int = 0
... related_items: List = field(default_factory=list)
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.related_items
[]
Real defaults
__post_init__
>>> from dataclasses import dataclass, field
>>> from typing import List
>>>
>>> @dataclass
... class InventoryItem:
... name: str
... unit_price: float
... quantity: int = 0
... related_items: List = field(default_factory=list)
... total_cost: float = field(init=False)
...
... def __post_init__(self):
... self.total_cost = self.unit_price * self.quantity
...
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
>>> from dataclasses import dataclass, field
>>> from typing import List
>>>
>>> @dataclass(frozen=True)
... class InventoryItem:
... name: str
... unit_price: float
... quantity: int = 0
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.unit_price += 1
Traceback (most recent call last):
...
FrozenInstanceError: cannot assign to field 'unit_price'
Freeze!
frozen + __post_init__
>>> from dataclasses import dataclass, field
>>> from typing import List
>>>
>>> @dataclass(frozen=True)
... class InventoryItem:
... name: str
... unit_price: float
... quantity: int = 0
... related_items: List = field(default_factory=list)
... total_cost: float = field(init=False)
...
... def __post_init__(self):
... total_cost = self.unit_price * self.quantity
... object.__setattr__(self, "total_cost", total_cost)
...
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
Hash is key
>>> from dataclasses import dataclass, field
>>> from typing import List
>>>
>>> @dataclass(unsafe_hash=True)
... class InventoryItem:
... name: str
... unit_price: float
... related_items: List = field(default_factory=list,
... hash=False)
...
>>> item1 = InventoryItem("hammer", 10.4)
>>> item2 = InventoryItem("hammer", 10.4)
>>> item3 = InventoryItem("spanner", 8.9)
>>> {item1, item2, item3}
{InventoryItem(name='hammer', ...),
InventoryItem(name='spanner', ...)}
@dataclass + mypy = ❤️
from dataclasses import dataclass, field
@dataclass
class InventoryItem:
name: str
unit_price: float
quantity: int = 0
related_items: List = field(default_factory=list)
item = InventoryItem("what", "are", "you", "doing?")
wtf.py
$ mypy wtf.py
wtf.py:11: error: Argument 2 to "InventoryItem" has incompatible type "str"; expected "float"
wtf.py:11: error: Argument 3 to "InventoryItem" has incompatible type "str"; expected "int"
wtf.py:11: error: Argument 4 to "InventoryItem" has incompatible type "str"; expected "List[Any]"
namedtuple returns
>>> from dataclasses import field, make_dataclass
>>> from typing import List
>>>
>>> make_dataclass("InventoryItem",
... [("name", str),
... ("unit_price", float),
... ("quantity", int, 0),
... ("related_items", List,
... field(default_factory=list))])
...
types.InventoryItem
>>> item = _("hammers", 10.49, 12)
>>> item.unit_price
10.49
>>> item.related_items
[]
>>> from dataclasses import asdict, astuple
>>>
>>> item = InventoryItem("hammers", 10.49, 12)
>>>
>>> asdict(item)
{'name': 'hammers', 'unit_price': 10.49, 'quantity': 12,
'related_items': []}
>>>
>>> astuple(item)
('hammers', 10.49, 12, [])
Let's go back in time...
(again)
attrs
Python 2.7 and 3.4+
$ pip install attrs
Hynek Schlawack
hynek.me 🇪🇺 🇩🇪
@attr.s
>>> import attr
>>>
>>> @attr.s
... class InventoryItem:
... name: str = attr.ib()
... unit_price: float = attr.ib()
... quantity: int = attr.ib(default=0)
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem("hammers", 10.49, 12)
>>> item.total_cost
125.88
-
__init__
-
__repr__
-
__str__
-
__eq__
-
__hash__
-
__ne__
-
__lt__
-
__le__
-
__gt__
-
__ge__
>>> @attr.s
... class InventoryItem:
... price: float = attr.ib()
...
... @price.validator
... def check(self, attribute, value):
... if value > 9000:
... raise ValueError("Dude? That's too expensive!")
...
>>> @attr.s
... class InventoryItem:
... price: float = attr.ib(
... validators=attr.validators.instance_of(float)
... )
...
>>> @attr.s
... class InventoryItem:
... price: Any = attr.ib(converter=float)
...
>>> InventoryItem("10.49")
InventoryItem(price=10.49)
So...
NoSQL?
The MongoDB example
>>> from pymongo import MongoClient
>>>
>>> client = MongoClient()
>>> database = client.amazing
>>>
>>> item_dict = database.inventory_items.find_one()
>>> item_dict
{'name': 'hammers', 'price': 10.49, 'quantity': 10}
-
Real OOP types
-
Properties
-
Dot notation
dict
>>> class InventoryItem(dict):
...
... @property
... def total_cost(self) -> float:
... return self["unit_price"] * self['quantity']
...
>>> item = InventoryItem(item_dict)
>>> item.total_cost
125.88
types.SimpleNamespace
>>> from types import SimpleNamespace
>>>
>>> class InventoryItem(SimpleNamespace):
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem(**item_dict)
>>> item.total_cost
125.88
>>> from copy import copy
>>>
>>> copy(item.__dict__)
{'name': 'hammers', 'price': 10.49, 'quantity': 10}
box
>>> from box import Box
>>>
>>> class InventoryItem(Box):
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem(item_dict)
>>> item.name
'hammer'
>>> item.total_cost
125.88
>>> item.to_dict()
{'name': 'hammer', 'unit_price': 10.49, 'quantity': 12}
51K instantiations + item access
Thingy
Python 2.7 and 3.4+
$ pip install Thingy
Thingy
>>> class InventoryItem(Thingy):
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> item = InventoryItem(item_dict)
>>> item.total_cost
125.88
>>> item.view()
{'name': 'hammer', 'unit_price': 10.49, 'quantity': 12}
???
>>> class InventoryItem(Thingy):
...
... @property
... def total_cost(self) -> float:
... return self.unit_price * self.quantity
...
>>> InventoryItem.add_view("with total", include="total_cost")
>>>
>>> item = InventoryItem(item_dict)
>>> item.view("with total")
{'name': 'hammer', 'unit_price': 10.49, 'quantity': 12,
'total_cost': 125.88}
Also availabe in
MongoDB flavor
Self-Q&A
Does @dataclass deprecate
Thingy?
Does it deprecate
named tuples?
(whatever module they're from)
Does it deprecate
SQLAlchemy?
Does it deprecate
Marshmallow?
Does it deprecate...
class itself?
Thank you!
PEP 557 versus the world
By Guillaume Gelin
PEP 557 versus the world
Python 3.7 will ship with a new module called "dataclasses", which has been defined in PEP 557. What is this module? What are the problems that PEP 557 authors try to solve? What was the chosen design, and why? How does it compare against the tools that already exist?
- 1,566