Dataclasses and property decorator

前端 未结 10 1628
没有蜡笔的小新
没有蜡笔的小新 2020-12-15 04:43

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 datacla

相关标签:
10条回答
  • 2020-12-15 04:51

    Here's another way which allows you to have fields without a leading underscore:

    from dataclasses import dataclass
    
    
    @dataclass
    class Person:
        name: str = property
    
        @name
        def name(self) -> str:
            return self._name
    
        @name.setter
        def name(self, value) -> None:
            self._name = value
    
        def __post_init__(self) -> None:
            if isinstance(self.name, property):
                self.name = 'Default'
    

    The result is:

    print(Person().name)  # Prints: 'Default'
    print(Person('Joel').name)  # Prints: 'Joel'
    print(repr(Person('Jane')))  # Prints: Person(name='Jane')
    
    0 讨论(0)
  • 2020-12-15 04:55

    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
    
    0 讨论(0)
  • 2020-12-15 05:00

    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()
    
    0 讨论(0)
  • 2020-12-15 05:01

    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
    
    0 讨论(0)
  • 2020-12-15 05:06

    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.

    0 讨论(0)
  • 2020-12-15 05:08

    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}).

    0 讨论(0)
提交回复
热议问题