classification
Title: dataclass defaults and property don't work together
Type: behavior Stage:
Components: Library (Lib) Versions: Python 3.9, Python 3.8, Python 3.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: eric.smith Nosy List: Michael Robellard, UnHumbleBen, eric.smith, iivanyuk, juanpa.arrivillaga
Priority: normal Keywords:

Created on 2020-01-07 17:35 by Michael Robellard, last changed 2021-06-06 01:45 by UnHumbleBen.

Messages (11)
msg359528 - (view) Author: Michael Robellard (Michael Robellard) Date: 2020-01-07 17:35
I ran into a strange issue while trying to use a dataclass together with a property.

I have it down to a minumum to reproduce it:

import dataclasses

@dataclasses.dataclass
class FileObject:
    _uploaded_by: str = dataclasses.field(default=None, init=False)
    uploaded_by: str = None

    def save(self):
        print(self.uploaded_by)

    @property
    def uploaded_by(self):
        return self._uploaded_by

    @uploaded_by.setter
    def uploaded_by(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

p = FileObject()
p.save()
This outputs:

Setter Called with Value  <property object at 0x7faeb00150b0>
<property object at 0x7faeb00150b0>
I would expect to get None instead

Here is the StackOverflow Question where I started this:
https://stackoverflow.com/questions/59623952/weird-issue-when-using-dataclass-and-property-together
msg359564 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2020-01-08 00:32
Your code basically becomes similar to this:

sentinel = object()

class FileObject:
    _uploaded_by: str = None
    uploaded_by = None

    def __init__(self, uploaded_by=sentinel):
        if uploaded_by is sentinel:
            self.uploaded_by = FileObject.uploaded_by
        else:
            self.uploaded_by = uploaded_by

    def save(self):
        print(self.uploaded_by)

    @property
    def uploaded_by(self):
        return self._uploaded_by

    @uploaded_by.setter
    def uploaded_by(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

Which has the same problem. I'll have to give it some thought.
msg359757 - (view) Author: Juan Arrivillaga (juanpa.arrivillaga) Date: 2020-01-10 21:12
So, after glancing at the source code:
https://github.com/python/cpython/blob/ce54519aa09772f4173b8c17410ed77e403f3ebf/Lib/dataclasses.py#L869

During this processing of fields, couldn't you just special case property/descriptor objects?
msg359764 - (view) Author: Juan Arrivillaga (juanpa.arrivillaga) Date: 2020-01-10 22:24
Actually, couldn't the following be a workaround, just set the property on the class after the class definition:


import dataclasses
import typing
@dataclasses.dataclass
class FileObject:
    uploaded_by:typing.Optional[None]=None

    def _uploaded_by_getter(self):
        return self._uploaded_by

    def _uploaded_by_setter(self, uploaded_by):
        print('Setter Called with Value ', uploaded_by)
        self._uploaded_by = uploaded_by

FileObject.uploaded_by = property(
    FileObject._uploaded_by_getter,
    FileObject._uploaded_by_setter
)
p = FileObject()
print(p)
print(p.uploaded_by)
msg359774 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2020-01-11 02:23
> During this processing of fields, couldn't you just special case property/descriptor objects?

What if you want the field to be a descriptor?

I think the best way of handling this would be to use some sentinel value for the default, and if found look up the value on the instance, not the class.

But I'm a little worried this might break something else.
msg366546 - (view) Author: Juan Arrivillaga (juanpa.arrivillaga) Date: 2020-04-15 20:00
But when would you want to have a descriptor as an instance attribute? Descriptors must be in the class dictionary to work:

https://docs.python.org/3/reference/datamodel.html#implementing-descriptors

I suppose, you could want some container class of descriptor objects, but that seems like an extremely narrow use-case, compared to the normal and common use-case of descriptors acting like descriptors. I think special-casing descriptors make sense because they act in a special way.
msg371820 - (view) Author: Ivan Ivanyuk (iivanyuk) Date: 2020-06-18 16:08
Was there some solution in progress here? We would like to use dataclasses and seems this problem currently limits their usefulness to us.

We recently came upon the same behaviour https://mail.python.org/pipermail/python-list/2020-June/897502.html and I was wondering if it was possible to make it work without changing the property decorator behaviour. Is there a way at all to preserve the default value on the class with @property even before dataclass starts processing it? 

 An example from that mail thread to workaround this:

 from dataclasses import dataclass, field
 
 def set_property():
     Container.x = property(Container.get_x, Container.set_x)
     return 30
 
 @dataclass
 class Container:
     x: int = field(default_factory=set_property)
 
     def get_x(self) -> int:
         return self._x
 
     def set_x(self, z: int):
         if z > 1:
             self._x = z
         else:
             raise ValueError

set_property can also be made a class method and referenced like this:
 x: int = field(default_factory=lambda: Container.set_property())

Is it possible that this kind of behaviour can be made one of standard flows for the field() function and dataclasses module can generate a function like this and set it on the class during processing?
 Or maybe it's better to extend @property decorator to update property object with default value which can be used later by the dataclass?
msg395191 - (view) Author: Benjamin Lee (UnHumbleBen) Date: 2021-06-06 00:25
Would this issue not be trivially resolved if there was a way to specify alias in the dataclasses field? I.e.:

_uploaded_by: str = dataclasses.field(alias="uploaded_by", default=None, init=False)

Ultimately, the main goal is to make it so that the generated __init__ constructor does

self._uploaded_by = uploaded_by

but with current implementation, there is no aliasing so the default __init__ constructor is always:

self._uploaded_by = _uploaded_by
msg395192 - (view) Author: Eric V. Smith (eric.smith) * (Python committer) Date: 2021-06-06 01:08
> _uploaded_by: str = dataclasses.field(alias="uploaded_by", default=None, init=False)

That's an interesting idea. I'll play around with it. I'm not sure "alias" feels quite right, as it only applies to __init__ (if I'm understanding it correctly).
msg395195 - (view) Author: Michael Robellard (Michael Robellard) Date: 2021-06-06 01:33
The sample I uploaded doesn't do any processing, but the use case originally had some logic inside the property getter/setter, would the alias idea allow for that? The purpose of the property is to add some logic to compute the value if it has not already been computed, however if it is computed don't recompute it because it is expensive to recompute.
msg395196 - (view) Author: Benjamin Lee (UnHumbleBen) Date: 2021-06-06 01:45
> I'm not sure "alias" feels quite right, as it only applies to __init__ (if I'm understanding it correctly).

Maybe `init_alias` might be a better name. In any case, this would support private variables in dataclasses.
History
Date User Action Args
2021-06-06 01:45:14UnHumbleBensetmessages: + msg395196
2021-06-06 01:33:02Michael Robellardsetmessages: + msg395195
2021-06-06 01:08:57eric.smithsetmessages: + msg395192
2021-06-06 00:25:00UnHumbleBensetnosy: + UnHumbleBen
messages: + msg395191
2020-06-18 16:08:07iivanyuksetnosy: + iivanyuk
messages: + msg371820
2020-04-15 20:00:22juanpa.arrivillagasetmessages: + msg366546
2020-01-11 02:23:39eric.smithsetmessages: + msg359774
2020-01-10 22:24:53juanpa.arrivillagasetmessages: + msg359764
2020-01-10 21:12:14juanpa.arrivillagasetnosy: + juanpa.arrivillaga
messages: + msg359757
2020-01-08 00:32:39eric.smithsetmessages: + msg359564
2020-01-07 19:40:37eric.smithsetassignee: eric.smith
2020-01-07 18:19:06xtreaksetnosy: + eric.smith
2020-01-07 17:35:37Michael Robellardcreate