Added PEP 557: Data Classes
This commit is contained in:
parent
4ab747322b
commit
6be5d81449
|
@ -0,0 +1,611 @@
|
||||||
|
PEP: 557
|
||||||
|
Title: Data Classes
|
||||||
|
Author: Eric V. Smith <eric@trueblade.com>
|
||||||
|
Status: Draft
|
||||||
|
Type: Standards Track
|
||||||
|
Content-Type: text/x-rst
|
||||||
|
Created: 02-Jun-2017
|
||||||
|
Python-Version: 3.7
|
||||||
|
Post-History: 08-Sep-2017
|
||||||
|
|
||||||
|
Notice for Reviewers
|
||||||
|
====================
|
||||||
|
|
||||||
|
This PEP and the initial implementation were drafted in a separate
|
||||||
|
repo: https://github.com/ericvsmith/dataclasses. Before commenting in
|
||||||
|
a public forum please at least read the `discussion`_ listed at the
|
||||||
|
end of this PEP.
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
========
|
||||||
|
|
||||||
|
This PEP describes an addition to the standard library called Data
|
||||||
|
Classes. Although they use a very different mechanism, Data Classes
|
||||||
|
can be thought of as "mutable namedtuples with defaults".
|
||||||
|
|
||||||
|
A class decorator is provided which inspects a class definition for
|
||||||
|
variables with type annotations as defined in PEP 526, "Syntax for
|
||||||
|
Variable Annotations". In this document, such variables are called
|
||||||
|
fields. Using these fields, the decorator adds generated method
|
||||||
|
definitions to the class to support instance initialization, a repr,
|
||||||
|
and comparisons methods. Such a class is called a Data Class, but
|
||||||
|
there's really nothing special about the class: it is the same class
|
||||||
|
but with the generated methods added.
|
||||||
|
|
||||||
|
As an example::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InventoryItem:
|
||||||
|
name: str
|
||||||
|
unit_price: float
|
||||||
|
quantity_on_hand: int = 0
|
||||||
|
|
||||||
|
def total_cost(self) -> float:
|
||||||
|
return self.unit_price * self.quantity_on_hand
|
||||||
|
|
||||||
|
The ``@dataclass`` decorator will add the equivalent of these methods
|
||||||
|
to the InventoryItem class::
|
||||||
|
|
||||||
|
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 __repr__(self):
|
||||||
|
return f'InventoryItem(name={self.name!r},unit_price={self.unit_price!r},quantity_on_hand={self.quantity_on_hand!r})'
|
||||||
|
def __eq__(self, other):
|
||||||
|
if other.__class__ is self.__class__:
|
||||||
|
return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand)
|
||||||
|
return NotImplemented
|
||||||
|
def __ne__(self, other):
|
||||||
|
if other.__class__ is self.__class__:
|
||||||
|
return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand)
|
||||||
|
return NotImplemented
|
||||||
|
def __lt__(self, other):
|
||||||
|
if other.__class__ is self.__class__:
|
||||||
|
return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand)
|
||||||
|
return NotImplemented
|
||||||
|
def __le__(self, other):
|
||||||
|
if other.__class__ is self.__class__:
|
||||||
|
return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand)
|
||||||
|
return NotImplemented
|
||||||
|
def __gt__(self, other):
|
||||||
|
if other.__class__ is self.__class__:
|
||||||
|
return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand)
|
||||||
|
return NotImplemented
|
||||||
|
def __ge__(self, other):
|
||||||
|
if other.__class__ is self.__class__:
|
||||||
|
return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand)
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
Data Classes saves you from writing and maintaining these functions.
|
||||||
|
|
||||||
|
Rationale
|
||||||
|
=========
|
||||||
|
|
||||||
|
There have been numerous attempts to define classes which exist
|
||||||
|
primarily to store values which are accessible by attribute lookup.
|
||||||
|
Some examples include:
|
||||||
|
|
||||||
|
- collection.namedtuple in the standard library.
|
||||||
|
|
||||||
|
- typing.NamedTuple in the standard library.
|
||||||
|
|
||||||
|
- The popular attrs [#]_ project.
|
||||||
|
|
||||||
|
- Many example online recipes [#]_, packages [#]_, and questions [#]_.
|
||||||
|
David Beazley used a form of data classes as the motivating example
|
||||||
|
in a PyCon 2013 metaclass talk [#]_.
|
||||||
|
|
||||||
|
So, why is this PEP needed?
|
||||||
|
|
||||||
|
With the addition of PEP 526, Python has a concise way to specify the
|
||||||
|
type of class members. This PEP leverages that syntax to provide a
|
||||||
|
simple, unobtrusive way to describe Data Classes. With one exception,
|
||||||
|
the specified attribute type annotation is completely ignored by Data
|
||||||
|
Classes.
|
||||||
|
|
||||||
|
No base classes or metaclasses are used by Data Classes. Users of
|
||||||
|
these classes are free to use inheritance and metaclasses without any
|
||||||
|
interference from Data Classes. The decorated classes are truly
|
||||||
|
"normal" Python classes. The Data Class decorator should not
|
||||||
|
interfere with any usage of the class.
|
||||||
|
|
||||||
|
Data Classes are not, and are not intended to be, a replacement
|
||||||
|
mechanism for all of the above libraries. But being in the standard
|
||||||
|
library will allow many of the simpler use cases to instead leverage
|
||||||
|
Data Classes. Many of the libraries listed have different feature
|
||||||
|
sets, and will of course continue to exist and prosper.
|
||||||
|
|
||||||
|
Where is it not appropriate to use Data Classes?
|
||||||
|
|
||||||
|
- Compatibility with tuples is required.
|
||||||
|
|
||||||
|
- True immutability is required.
|
||||||
|
|
||||||
|
- Type validation beyond that provided by PEPs 484 and 526 is
|
||||||
|
required, or value validation is required.
|
||||||
|
|
||||||
|
XXX Motivation for each dataclass() and field() parameter
|
||||||
|
|
||||||
|
Specification
|
||||||
|
=============
|
||||||
|
|
||||||
|
All of the functions described in this PEP will live in a module named
|
||||||
|
``dataclasses``.
|
||||||
|
|
||||||
|
A function ``dataclass`` which is typically used as a class decorator
|
||||||
|
is provided to post-process classes and add generated member
|
||||||
|
functions, described below.
|
||||||
|
|
||||||
|
The ``dataclass`` decorator examines the class to find ``field``'s. A
|
||||||
|
``field`` is defined as any variable identified in
|
||||||
|
``__annotations__``. That is, a variable that is decorated with a
|
||||||
|
type annotation. With a single exception described below, none of the
|
||||||
|
Data Class machinery examines the type specified in the annotation.
|
||||||
|
|
||||||
|
Note that ``__annotations__`` is guaranateed to be an ordered mapping,
|
||||||
|
in class declaration order. The order of the fields in all of the
|
||||||
|
generated methods is the order in which they appear in the class.
|
||||||
|
|
||||||
|
The ``dataclass`` decorator is typically used with no parameters and
|
||||||
|
no parenthesis. However, it also supports the following logical
|
||||||
|
signature::
|
||||||
|
|
||||||
|
def dataclass(*, init=True, repr=True, hash=None, cmp=True, frozen=False)
|
||||||
|
|
||||||
|
If ``dataclass`` is used just as a simple decorator with no
|
||||||
|
parameters, it acts as if it has the default values documented in this
|
||||||
|
signature. That is, these three uses of ``@dataclass`` are equivalent::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
...
|
||||||
|
|
||||||
|
@dataclass()
|
||||||
|
class C:
|
||||||
|
...
|
||||||
|
|
||||||
|
@dataclass(init=True, repr=True, hash=None, cmp=True, frozen=False)
|
||||||
|
class C:
|
||||||
|
...
|
||||||
|
|
||||||
|
The parameters to ``dataclass`` are:
|
||||||
|
|
||||||
|
- ``init``: If true, a ``__init__`` method will be generated.
|
||||||
|
|
||||||
|
- ``repr``: If true, a ``__repr__`` function will be generated. The
|
||||||
|
generated repr string will have the class name and the name and repr
|
||||||
|
of each field, in the order they are defined in the class. Fields
|
||||||
|
that are marked as being excluded from the repr are not included.
|
||||||
|
For example:
|
||||||
|
``InventoryItem(name='widget',unit_price=3.0,quantity_on_hand=10)``.
|
||||||
|
|
||||||
|
- ``cmp``: If true, ``__eq__``, ``__ne__``, ``__lt__``, ``__le__``,
|
||||||
|
``__gt__``, and ``__ge__`` methods will be generated. These compare
|
||||||
|
the class as if it were a tuple of its fields, in order. Both
|
||||||
|
instances in the comparison must be of the identical type.
|
||||||
|
|
||||||
|
- ``hash``: Either a bool or ``None``. If ``None`` (the default), the
|
||||||
|
``__hash__`` method is generated according to how cmp and frozen are
|
||||||
|
set.
|
||||||
|
|
||||||
|
If ``cmp`` and ``hash`` are both true, Data Classes will generate a
|
||||||
|
``__hash__`` for you. If ``cmp`` is true and ``frozen`` is false,
|
||||||
|
``__hash__`` will be set to ``None``, marking it unhashable (which
|
||||||
|
it is). If cmp is false, ``__hash__`` will be left untouched
|
||||||
|
meaning the ``__hash__`` method of the superclass will be used (if
|
||||||
|
superclass is object, this means it will fall back to id-based
|
||||||
|
hashing).
|
||||||
|
|
||||||
|
Although not recommended, you force Data Classes to create a
|
||||||
|
``__hash__`` method ``hash=True``. This might be the case if your
|
||||||
|
class is logically immutable but can none the less be mutated. This
|
||||||
|
is a specialized use case and should be considered carefully.
|
||||||
|
|
||||||
|
See the Python documentation [#]_ for more information.
|
||||||
|
|
||||||
|
- ``frozen``: If true, assigning to fields will generate an exception.
|
||||||
|
This emulates read-only frozen instances. See the discussion below.
|
||||||
|
|
||||||
|
``field``'s may optionally specify a default value, using normal
|
||||||
|
Python syntax::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
int a # 'a' has no default value
|
||||||
|
int b = 0 # assign a default value for 'b'
|
||||||
|
|
||||||
|
For common and simple use cases, no other functionality is required.
|
||||||
|
There are, however, some Data Class features that require additional
|
||||||
|
per-field information. To satisfy this need for additional
|
||||||
|
information, you can replace the default field value with a call to
|
||||||
|
the provided ``field()`` function. The signature of ``field()`` is::
|
||||||
|
|
||||||
|
def field(*, default=_MISSING, default_factory=_MISSING, repr=True,
|
||||||
|
hash=None, init=True, cmp=True)
|
||||||
|
|
||||||
|
The ``_MISSING`` value is a sentinel object used to detect if the
|
||||||
|
``default`` and ``default_factory`` parameters are provided. Users
|
||||||
|
should never use ``_MISSING`` or depend on its value. This sentinel
|
||||||
|
is used because ``None`` is a valid value for ``default``.
|
||||||
|
|
||||||
|
The parameters to ``field()`` are:
|
||||||
|
|
||||||
|
- ``default``: If provided, this will be the default value for this
|
||||||
|
field. This is needed because the ``field`` call itself replaces
|
||||||
|
the normal position of the default value.
|
||||||
|
|
||||||
|
- ``default_factory``: If provided, a zero-argument callable that will
|
||||||
|
be called when a default value is needed for this field. Among
|
||||||
|
other purposes, this can be used to specify fields with mutable
|
||||||
|
default values, discussed below. It is an error to specify both
|
||||||
|
``default`` and ``default_factory``.
|
||||||
|
|
||||||
|
- ``init``: If true, this field is included as a parameter to the
|
||||||
|
generated ``__init__`` function.
|
||||||
|
|
||||||
|
- ``repr``: If true, this field is included in the string returned by
|
||||||
|
the generated ``__repr__`` function.
|
||||||
|
|
||||||
|
- ``cmp``: If true, this field is included in the generated comparison
|
||||||
|
methods (``__eq__`` et al).
|
||||||
|
|
||||||
|
- ``hash``: This can be a bool or ``None``. If true, this field is
|
||||||
|
included in the generated ``__hash__`` method. If ``None`` (the
|
||||||
|
default), use the value of ``cmp``: this would normally be the
|
||||||
|
expected behavior. A field needs to be considered in the hash if
|
||||||
|
it's used for comparisons. Setting this value to anything other
|
||||||
|
than ``None`` is discouraged.
|
||||||
|
|
||||||
|
``Field`` objects
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
``Field`` objects describe each defined field. These objects are
|
||||||
|
created internally, and are returned by the ``fields()`` module-level
|
||||||
|
method (see below). Users should never instantiate a ``Field``
|
||||||
|
object directly. Its attributes are:
|
||||||
|
|
||||||
|
- ``name``: The name of the field.
|
||||||
|
|
||||||
|
- ``type``: The type of the field.
|
||||||
|
|
||||||
|
- ``default``, ``default_factory``, ``init``, ``repr``, ``hash``, and
|
||||||
|
``cmp`` have the identical meaning as they do in the ``field()``
|
||||||
|
declaration.
|
||||||
|
|
||||||
|
post-init processing
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The generated ``__init__`` code will call a method named
|
||||||
|
``__dataclass_post_init__``, if it is defined on the class. It will
|
||||||
|
be called as ``self.__dataclass_post_init__()``.
|
||||||
|
|
||||||
|
Among other uses, this allows for initializing field values that
|
||||||
|
depend on one or more other fields.
|
||||||
|
|
||||||
|
Class variables
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The one place where ``dataclass`` actually inspects the type of a
|
||||||
|
field is to determine if a field is a class variable. It does this by
|
||||||
|
seeing if the type of the field is given as of type
|
||||||
|
``typing.ClassVar``. If a field is a ``ClassVar``, it is excluded
|
||||||
|
from consideration as a field and is ignored by the Data Class
|
||||||
|
mechanisms.
|
||||||
|
|
||||||
|
Frozen instances
|
||||||
|
----------------
|
||||||
|
|
||||||
|
It is not possible to create truly immutable Python objects. However,
|
||||||
|
by passing ``frozen=True`` to the ``@dataclass`` decorator you can
|
||||||
|
emulate immutability. In that case, Data Classes will add
|
||||||
|
``__setattr__`` and ``__delattr__`` member functions to the class.
|
||||||
|
These functions will raise a ``FrozenInstanceError`` when invoked.
|
||||||
|
|
||||||
|
There is a tiny performance penalty when using ``frozen=True``:
|
||||||
|
``__init__`` cannot use simple assignment to initialize fields, and
|
||||||
|
must use ``object.__setattr__``.
|
||||||
|
|
||||||
|
Mutable default values
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Python stores the default field values in class attributes.
|
||||||
|
Consider this example, not using Data Classes::
|
||||||
|
|
||||||
|
class C:
|
||||||
|
x = []
|
||||||
|
def __init__(self, x=x):
|
||||||
|
self.x = x
|
||||||
|
|
||||||
|
assert C().x is C().x
|
||||||
|
assert C().x is not C([]).x
|
||||||
|
|
||||||
|
That is, two instances of class ``C`` that do not not specify a value
|
||||||
|
for ``x`` when creating a class instance will share the same copy of
|
||||||
|
the list. Because Data Classes just use normal Python class creation,
|
||||||
|
they also share this problem. There is no general way for Data
|
||||||
|
Classes to detect this condition. Instead, Data Classes will raise a
|
||||||
|
``TypeError`` if it detects a default parameter of type ``list``,
|
||||||
|
``dict``, or ``set``. This is a partial solution, but it does protect
|
||||||
|
against many common errors. See `How to support mutable default
|
||||||
|
values`_ in the Discussion section for more details.
|
||||||
|
|
||||||
|
Using default factory functions is a way to create new instances of
|
||||||
|
mutable types as default values for fields::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
x: list = field(default_factory=list)
|
||||||
|
|
||||||
|
assert C().x is not C().x
|
||||||
|
|
||||||
|
Inheritance
|
||||||
|
-----------
|
||||||
|
|
||||||
|
When the Data Class is being created by the ``@dataclass`` decorator,
|
||||||
|
it looks through all of the class's base classes in reverse MRO (that
|
||||||
|
is, starting at ``object``) and, for each Data Class that it finds,
|
||||||
|
adds the fields from that base class to an ordered mapping of fields.
|
||||||
|
After all of the base classes, it adds its own fields to the ordered
|
||||||
|
mapping. Because the fields are in insertion order, derived classes
|
||||||
|
override base classes. An example::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Base:
|
||||||
|
x: float = 15.0
|
||||||
|
y: int = 0
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C(Base):
|
||||||
|
z: int = 10
|
||||||
|
x: int = 15
|
||||||
|
|
||||||
|
The final list of fields is, in order, ``x``, ``y``, ``z``. The final
|
||||||
|
type of ``x`` is ``int``, as specified in class ``C``.
|
||||||
|
|
||||||
|
Default factory functions
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
If a field specifies a ``default_factory``, it is called with zero
|
||||||
|
arguments when a default value for the field is needed. For example,
|
||||||
|
to create a new instance of a list, use::
|
||||||
|
|
||||||
|
l: list = field(default_factory=list)
|
||||||
|
|
||||||
|
If a field is excluded from ``__init__`` (using ``init=False``) and
|
||||||
|
the field also specifies ``default_factory``, then the default factory
|
||||||
|
function will always be called from the generated ``__init__``
|
||||||
|
function. This happens because there is no other way to give the
|
||||||
|
field a default value.
|
||||||
|
|
||||||
|
Module level helper functions
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
- ``fields(class_or_instance)``: Returns a list of ``Field`` objects
|
||||||
|
that define the fields for this Data Class. Accepts either a Data
|
||||||
|
Class, or an instance of a Data Class.
|
||||||
|
|
||||||
|
- ``asdict(instance)``: todo: recursion, class factories, etc.
|
||||||
|
|
||||||
|
- ``astuple(instance)``: todo: recursion, class factories, etc.
|
||||||
|
|
||||||
|
.. _discussion:
|
||||||
|
|
||||||
|
Discussion
|
||||||
|
==========
|
||||||
|
|
||||||
|
python-ideas discussion
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
This discussion started on python-ideas [#]_ and was moved to a GitHub
|
||||||
|
repo [#]_ for further discussion. As part of this discussion, we made
|
||||||
|
the decision to use PEP 526 syntax to drive the discovery of fields.
|
||||||
|
|
||||||
|
Support for automatically setting ``__slots__``?
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
At least for the initial release, ``__slots__`` will not be supported.
|
||||||
|
``__slots__`` needs to be added at class creation time. The Data
|
||||||
|
Class decorator is called after the class is created, so in order to
|
||||||
|
add ``__slots__`` the decorator would have to create a new class, set
|
||||||
|
``__slots__``, and return it. Because this behavior is somewhat
|
||||||
|
surprising, the initial version of Data Classes will not support
|
||||||
|
automatically setting ``__slots__``. There are a number of
|
||||||
|
workarounds:
|
||||||
|
|
||||||
|
- Manually add ``__slots__`` in the class definition.
|
||||||
|
|
||||||
|
- Write a function (which could be used as a decorator) that
|
||||||
|
inspects the class using ``fields()`` and creates a new class with
|
||||||
|
``__slots__`` set.
|
||||||
|
|
||||||
|
For more discussion, see [#]_.
|
||||||
|
|
||||||
|
Should post-init take params?
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
The post-init function ``__dataclass_post_init__`` takes no
|
||||||
|
parameters. This was deemed to be simpler than trying to find a
|
||||||
|
mechanism to optionally pass a parameter to the
|
||||||
|
``__dataclass_post_init__`` function.
|
||||||
|
|
||||||
|
|
||||||
|
Why not just use namedtuple
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
- Any namedtuple can be compared to any other with the same number of
|
||||||
|
fields. For example: ``Point3D(2017, 6, 2) == Date(2017, 6, 2)``.
|
||||||
|
With Data Classes, this would return False.
|
||||||
|
|
||||||
|
- A namedtuple can be compared to a tuple. For example ``Point2D(1,
|
||||||
|
10) == (1, 10)``. With Data Classes, this would return False.
|
||||||
|
|
||||||
|
- Instances are always iterable, which can make it difficult to add
|
||||||
|
fields. If a library defines::
|
||||||
|
|
||||||
|
Time = namedtuple('Time', ['hour', 'minute'])
|
||||||
|
def get_time():
|
||||||
|
return Time(12, 0)
|
||||||
|
|
||||||
|
Then if a user uses this code as::
|
||||||
|
|
||||||
|
hour, minute = get_time()
|
||||||
|
|
||||||
|
then it would not be possible to add a ``second`` field to ``Time``
|
||||||
|
without breaking the user's code.
|
||||||
|
|
||||||
|
- No option for mutable instances.
|
||||||
|
|
||||||
|
- Cannot specify default values.
|
||||||
|
|
||||||
|
- Cannot control which fields are used for ``__init__``, ``__repr__``,
|
||||||
|
etc.
|
||||||
|
|
||||||
|
Why not just use typing.NamedTuple
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
For classes with statically defined fields, it does support similar
|
||||||
|
syntax to Data Classes, using type annotations. This produces a
|
||||||
|
namedtuple, so it shares ``namedtuple``'s benefits and some of its
|
||||||
|
downsides.
|
||||||
|
|
||||||
|
Why not just use attrs
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- attrs moves faster than could be accommodated if it were moved in to
|
||||||
|
the standard library.
|
||||||
|
|
||||||
|
- attrs supports additional features not being proposed here:
|
||||||
|
validators, converters, metadata, etc. Data Classes makes a
|
||||||
|
tradeoff to achieve simplicity by not implementing these
|
||||||
|
features.
|
||||||
|
|
||||||
|
For more discussion, see [#]_.
|
||||||
|
|
||||||
|
Dynamic creation of classes
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
An earlier version of this PEP and the sample implementation provided
|
||||||
|
a ``make_class`` function that dynamically created Data Classes. This
|
||||||
|
functionality was later dropped, although it might be added at a later
|
||||||
|
time as a helper function. The ``@dataclass`` decorator does not care
|
||||||
|
how classes are created, so they could be either statically defined or
|
||||||
|
dynamically defined. For this Data Class::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
x: int
|
||||||
|
y: int = field(init=False, default=0)
|
||||||
|
|
||||||
|
Here is one way of dynamically creating the same Data Class::
|
||||||
|
|
||||||
|
cls_dict = {'__annotations__': OrderedDict(x=int, y=int),
|
||||||
|
'y': field(init=False, default=0),
|
||||||
|
}
|
||||||
|
C = dataclass(type('C', (object,), cls_dict))
|
||||||
|
|
||||||
|
How to support mutable default values
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
One proposal was to automatically copy defaults, so that if a literal
|
||||||
|
list ``[]`` was a default value, each instance would get a new list.
|
||||||
|
There were undesirable side effects of this decision, so the final
|
||||||
|
decision is to disallow the 3 known built-in mutable types: list,
|
||||||
|
dict, and set. For a complete discussion of this and other options,
|
||||||
|
see [#]_.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
========
|
||||||
|
|
||||||
|
This code exists in a closed source project::
|
||||||
|
|
||||||
|
class Application:
|
||||||
|
def __init__(self, name, requirements, constraints=None, path='', executable_links=None, executables_dir=()):
|
||||||
|
self.name = name
|
||||||
|
self.requirements = requirements
|
||||||
|
self.constraints = {} if constraints is None else constraints
|
||||||
|
self.path = path
|
||||||
|
self.executable_links = [] if executable_links is None else executable_links
|
||||||
|
self.executables_dir = executables_dir
|
||||||
|
self.additional_items = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'Application({self.name!r},{self.requirements!r},{self.constraints!r},{self.path!r},{self.executable_links!r},{self.executables_dir!r},{self.additional_items!r})'
|
||||||
|
|
||||||
|
This can be replaced by::
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Application:
|
||||||
|
name: Str
|
||||||
|
requirements: List
|
||||||
|
constraints: List[str] = field(default_factory=list)
|
||||||
|
path: Str = ''
|
||||||
|
executable_links: List[str] = field(default_factory=list)
|
||||||
|
executable_dir: Tuple[str] = ()
|
||||||
|
additional_items: List[str] = field(init=False, default_factory=list)
|
||||||
|
|
||||||
|
The Data Class version is more declarative, has less code, supports
|
||||||
|
``typing``, and includes the other generated functions.
|
||||||
|
|
||||||
|
Acknowledgements
|
||||||
|
================
|
||||||
|
|
||||||
|
The following people provided invaluable input during the development
|
||||||
|
of this PEP and code: Ivan Levkivskyi, Guido van Rossum, Hynek
|
||||||
|
Schlawack, Raymond Hettinger, and Lisa Roach. I thank them for their
|
||||||
|
time and expertise.
|
||||||
|
|
||||||
|
A special mention must be made about the attrs project. It was a true
|
||||||
|
inspiration for this PEP, and I respect the design decisions they
|
||||||
|
made.
|
||||||
|
|
||||||
|
References
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. [#] attrs project on github
|
||||||
|
(https://github.com/python-attrs/attrs)
|
||||||
|
|
||||||
|
.. [#] DictDotLookup recipe
|
||||||
|
(http://code.activestate.com/recipes/576586-dot-style-nested-lookups-over-dictionary-based-dat/)
|
||||||
|
|
||||||
|
.. [#] attrdict package
|
||||||
|
(https://pypi.python.org/pypi/attrdict)
|
||||||
|
|
||||||
|
.. [#] StackOverflow question about data container classes
|
||||||
|
(https://stackoverflow.com/questions/3357581/using-python-class-as-a-data-container)
|
||||||
|
|
||||||
|
.. [#] David Beazley metaclass talk featuring data classes
|
||||||
|
(https://www.youtube.com/watch?v=sPiWg5jSoZI)
|
||||||
|
|
||||||
|
.. [#] Python documentation for __hash__
|
||||||
|
(https://docs.python.org/3/reference/datamodel.html#object.__hash__)
|
||||||
|
|
||||||
|
.. [#] Start of python-ideas discussion
|
||||||
|
(https://mail.python.org/pipermail/python-ideas/2017-May/045618.html)
|
||||||
|
|
||||||
|
.. [#] GitHub repo where discussions and initial development took place
|
||||||
|
(https://github.com/ericvsmith/dataclasses)
|
||||||
|
|
||||||
|
.. [#] Support __slots__?
|
||||||
|
(https://github.com/ericvsmith/dataclasses/issues/28)
|
||||||
|
|
||||||
|
.. [#] why not just attrs?
|
||||||
|
(https://github.com/ericvsmith/dataclasses/issues/19)
|
||||||
|
|
||||||
|
.. [#] Copying mutable defaults
|
||||||
|
(https://github.com/ericvsmith/dataclasses/issues/3)
|
||||||
|
|
||||||
|
Copyright
|
||||||
|
=========
|
||||||
|
|
||||||
|
This document has been placed in the public domain.
|
||||||
|
|
||||||
|
|
||||||
|
..
|
||||||
|
Local Variables:
|
||||||
|
mode: indented-text
|
||||||
|
indent-tabs-mode: nil
|
||||||
|
sentence-end-double-space: t
|
||||||
|
fill-column: 70
|
||||||
|
coding: utf-8
|
||||||
|
End:
|
Loading…
Reference in New Issue