问题
I've been reading up on Python 3.7's dataclass as an alternative to namedtuples (what I typically use when having to group data in a structure). I was wondering if dataclass is compatible with the property decorator to define getter and setter functions for the data elements of the dataclass. If so, is this described somewhere? Or are there examples available?
回答1:
It sure does work:
from dataclasses import dataclass
@dataclass
class Test:
_name: str="schbell"
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, v: str) -> None:
self._name = v
t = Test()
print(t.name) # schbell
t.name = "flirp"
print(t.name) # flirp
print(t) # Test(_name='flirp')
In fact, why should it not? In the end, what you get is just a good old class, derived from type:
print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>
Maybe that's why properties are nowhere mentioned specifically. However, the PEP-557's Abstract mentions the general usability of well-known Python class features:
Because Data Classes use normal class definition syntax, you are free to use inheritance, metaclasses, docstrings, user-defined methods, class factories, and other Python class features.
回答2:
Currently, the best way I found was to overwrite the dataclass fields by property in a separate child class.
from dataclasses import dataclass, field
@dataclass
class _A:
x: int = 0
class A(_A):
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
The class behave like a regular dataclass. And will correctly define the __repr__
and __init__
field (A(x=4)
instead of A(_x=4)
. The drawback is that the properties cannot be read-only.
This blog post, tries to overwrite the wheels dataclass attribute by the property
of the same name.
However, the @property
overwrite the default field
, which leads to unexpected behavior.
from dataclasses import dataclass, field
@dataclass
class A:
x: int
# same as: `x = property(x) # Overwrite any field() info`
@property
def x(self) -> int:
return self._x
@x.setter
def x(self, value: int):
self._x = value
A() # `A(x=<property object at 0x7f0cf64e5fb0>)` Oups
print(A.__dataclass_fields__) # {'x': Field(name='x',type=<class 'int'>,default=<property object at 0x>,init=True,repr=True}
One way solve this, while avoiding inheritance would be to overwrite the field outside the class definition, after the dataclass metaclass has been called.
@dataclass
class A:
x: int
def x_getter(self):
return self._x
def x_setter(self, value):
self._x = value
A.x = property(x_getter)
A.x = A.x.setter(x_setter)
print(A(x=1))
print(A()) # missing 1 required positional argument: 'x'
It should probably possible to overwrite this automatically by creating some custom metaclass and setting some field(metadata={'setter': _x_setter, 'getter': _x_getter})
.
回答3:
Some wrapping could be good:
# Copyright 2019 Xu Siyuan
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass, field
MISSING = object()
__all__ = ['property_field', 'property_dataclass']
class property_field:
def __init__(self, fget=None, fset=None, fdel=None, doc=None, **kwargs):
self.field = field(**kwargs)
self.property = property(fget, fset, fdel, doc)
def getter(self, fget):
self.property = self.property.getter(fget)
return self
def setter(self, fset):
self.property = self.property.setter(fset)
return self
def deleter(self, fdel):
self.property = self.property.deleter(fdel)
return self
def property_dataclass(cls=MISSING, / , **kwargs):
if cls is MISSING:
return lambda cls: property_dataclass(cls, **kwargs)
remembers = {}
for k in dir(cls):
if isinstance(getattr(cls, k), property_field):
remembers[k] = getattr(cls, k).property
setattr(cls, k, getattr(cls, k).field)
result = dataclass(**kwargs)(cls)
for k, p in remembers.items():
setattr(result, k, p)
return result
You can use it like this:
@property_dataclass
class B:
x: int = property_field(default_factory=int)
@x.getter
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
回答4:
TWO VERSIONS THAT SUPPORT DEFAULT VALUES
Most published approaches don't provide a readable way to set a default value for the property, which is quite an important part of dataclass. Here are two possible ways to do that.
The first way is based on the approach referenced by @JorenV. It defines the default value in _name = field()
and utilises the observation that if no initial value is specified, then the setter is passed the property object itself:
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False, default='baz')
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
if type(value) is property:
# initial value not specified, use default
value = Test._name
self._name = value
def main():
obj = Test(name='foo')
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'bar'
print(obj) # displays: Test(name='bar')
obj = Test()
print(obj) # displays: Test(name='baz')
if __name__ == '__main__':
main()
The second way is based on the same approach as @Conchylicultor: bypassing the dataclass machinery by overwriting the field outside the class definition.
Personally I think this way is cleaner and more readable than the first because it follows the normal dataclass idiom to define the default value and requires no 'magic' in the setter.
Even so I'd prefer everything to be self-contained... perhaps some clever person can find a way to incorporate the field update in dataclass.__post_init__()
or similar?
from dataclasses import dataclass
@dataclass
class Test:
name: str = 'foo'
@property
def _name(self):
return self._my_str_rev[::-1]
@_name.setter
def _name(self, value):
self._my_str_rev = value[::-1]
# --- has to be called at module level ---
Test.name = Test._name
def main():
obj = Test()
print(obj) # displays: Test(name='foo')
obj = Test()
obj.name = 'baz'
print(obj) # displays: Test(name='baz')
obj = Test(name='bar')
print(obj) # displays: Test(name='bar')
if __name__ == '__main__':
main()
回答5:
Following a very thorough post about data classes and properties that can be found here the TL;DR version which solves some very ugly cases where you have to call MyClass(_my_var=2)
and strange __repr__
outputs:
from dataclasses import field, dataclass
@dataclass
class Vehicle:
wheels: int
_wheels: int = field(init=False, repr=False)
def __init__(self, wheels: int):
self._wheels = wheels
@property
def wheels(self) -> int:
return self._wheels
@wheels.setter
def wheels(self, wheels: int):
self._wheels = wheels
回答6:
From the ideas from above, I created a class decorator function resolve_abc_prop
that creates a new class containing the getter and setter functions as suggested
by @shmee.
def resolve_abc_prop(cls):
def gen_abstract_properties():
""" search for abstract properties in super classes """
for class_obj in cls.__mro__:
for key, value in class_obj.__dict__.items():
if isinstance(value, property) and value.__isabstractmethod__:
yield key, value
abstract_prop = dict(gen_abstract_properties())
def gen_get_set_properties():
""" for each matching data and abstract property pair,
create a getter and setter method """
for class_obj in cls.__mro__:
if '__dataclass_fields__' in class_obj.__dict__:
for key, value in class_obj.__dict__['__dataclass_fields__'].items():
if key in abstract_prop:
def get_func(self, key=key):
return getattr(self, f'__{key}')
def set_func(self, val, key=key):
return setattr(self, f'__{key}', val)
yield key, property(get_func, set_func)
get_set_properties = dict(gen_get_set_properties())
new_cls = type(
cls.__name__,
cls.__mro__,
{**cls.__dict__, **get_set_properties},
)
return new_cls
Here we define a data class AData
and a mixin AOpMixin
implementing operations
on the data.
from dataclasses import dataclass, field, replace
from abc import ABC, abstractmethod
class AOpMixin(ABC):
@property
@abstractmethod
def x(self) -> int:
...
def __add__(self, val):
return replace(self, x=self.x + val)
Finally, the decorator resolve_abc_prop
is then used to create a new class
with the data from AData
and the operations from AOpMixin
.
@resolve_abc_prop
@dataclass
class A(AOpMixin):
x: int
A(x=4) + 2 # A(x=6)
EDIT #1: I created a python package that makes it possible to overwrite abstract properties with a dataclass: dataclass-abc
回答7:
An @property
is typically used to store a seemingly public argument (e.g. name
) into a private attribute (e.g. _name
) through getters and setters, while dataclasses generate the __init__()
method for you.
The problem is that this generated __init__()
method should interface through the public argument name
, while internally setting the private attribute _name
.
This is not done automatically by dataclasses.
In order to have the same interface (through name
) for setting values and creation of the object, the following strategy can be used (Based on this blogpost, which also provides more explanation):
from dataclasses import dataclass, field
@dataclass
class Test:
name: str
_name: str = field(init=False, repr=False)
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
This can now be used as one would expect from a dataclass with a data member name
:
my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)
The above implementation does the following things:
- The
name
class member will be used as the public interface, but it actually does not really store anything - The
_name
class member stores the actual content. The assignment withfield(init=False, repr=False)
makes sure that the@dataclass
decorator ignores it when constructing the__init__()
and__repr__()
methods. - The getter/setter for
name
actually returns/sets the content of_name
- The initializer generated through the
@dataclass
will use the setter that we just defined. It will not initialize_name
explicitly, because we told it not to do so.
回答8:
Here's what I did to define the field as a property in __post_init__
. This is a total hack, but it works with dataclasses
dict-based initialization and even with marshmallow_dataclasses.
from dataclasses import dataclass, field, asdict
@dataclass
class Test:
name: str = "schbell"
_name: str = field(init=False, repr=False)
def __post_init__(self):
# Just so that we don't create the property a second time.
if not isinstance(getattr(Test, "name", False), property):
self._name = self.name
Test.name = property(Test._get_name, Test._set_name)
def _get_name(self):
return self._name
def _set_name(self, val):
self._name = val
if __name__ == "__main__":
t1 = Test()
print(t1)
print(t1.name)
t1.name = "not-schbell"
print(asdict(t1))
t2 = Test("llebhcs")
print(t2)
print(t2.name)
print(asdict(t2))
This would print:
Test(name='schbell')
schbell
{'name': 'not-schbell', '_name': 'not-schbell'}
Test(name='llebhcs')
llebhcs
{'name': 'llebhcs', '_name': 'llebhcs'}
I actually started off from this blog post mentioned somewhere in this SO, but ran into the issue that the dataclass field was being set to type property
because the decorator is applied to the class. That is,
@dataclass
class Test:
name: str = field(default='something')
_name: str = field(init=False, repr=False)
@property
def name():
return self._name
@name.setter
def name(self, val):
self._name = val
would make name
to be of type property
and not str
. So, the setter will actually receive property
object as the argument instead of the field default.
来源:https://stackoverflow.com/questions/51079503/python-dataclass-and-property-decorator