Dataclasses and property decorator

前端 未结 10 1629
没有蜡笔的小新
没有蜡笔的小新 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 05:09

    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.

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

    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 with field(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.
    0 讨论(0)
  • 2020-12-15 05:14

    After trying different suggestions from this thread I've come with a little modified version of @Samsara Apathika answer. In short: I removed the "underscore" field variable from the __init__ (so it is available for internal use, but not seen by asdict() or by __dataclass_fields__).

    from dataclasses import dataclass, InitVar, field, asdict
    
    @dataclass
    class D:
        a: float = 10.                # Normal attribut with a default value
        b: InitVar[float] = 20.       # init-only attribute with a default value 
        c: float = field(init=False)  # an attribute that will be defined in __post_init__
        
        def __post_init__(self, b):
            if not isinstance(getattr(D, "a", False), property):
                print('setting `a` to property')
                self._a = self.a
                D.a = property(D._get_a, D._set_a)
            
            print('setting `c`')
            self.c = self.a + b
            self.d = 50.
        
        def _get_a(self):
            print('in the getter')
            return self._a
        
        def _set_a(self, val):
            print('in the setter')
            self._a = val
    
    
    if __name__ == "__main__":
        d1 = D()
        print(asdict(d1))
        print('\n')
        d2 = D()
        print(asdict(d2))
    
    

    Gives:

    setting `a` to property
    setting `c`
    in the getter
    in the getter
    {'a': 10.0, 'c': 30.0}
    
    
    in the setter
    setting `c`
    in the getter
    in the getter
    {'a': 10.0, 'c': 30.0}
    
    0 讨论(0)
  • 2020-12-15 05:18

    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

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