Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconciling Dataclasses And Properties In Python - Florimond Manca #387

Open
utterances-bot opened this issue Nov 3, 2022 · 4 comments
Open
Labels
comments Comments from utterances

Comments

@utterances-bot
Copy link

Reconciling Dataclasses And Properties In Python - Florimond Manca

I love Python dataclasses, but combining them with properties is not obvious. This is a problem solving report — and a practical introduction to dataclasses!

https://florimond.dev/en/posts/2018/10/reconciling-dataclasses-and-properties-in-python/

Copy link

Appreciate your effort to bring this detailed analysis, It really help us to take deep dive.
But still, have a question about @wheels.setter if we provide a setter to a protected/private variable then didn't we lose the intent of having one(protected/private )?

@florimondmanca florimondmanca added the comments Comments from utterances label Nov 5, 2022
Copy link

ruslaniv commented Dec 4, 2022

Great article!
Could you maybe extend this article and write about setting a default value to an attribute using dataclasses and properties (because of this: python/cpython#94067)? Right now I'm setting a default value in a parent class and then subclassing it but maybe there is a more elegant solution?

Copy link
Owner

florimondmanca commented Dec 4, 2022

@ruslaniv It seems you can set _wheels: int = field(..., default=...).

from dataclasses import dataclass, field

@dataclass
class Vehicle:
    wheels: int
    _wheels: int = field(init=False, repr=False, default=4)

    @property
    def wheels(self) -> int:
        print("getting wheels")
        return self._wheels

    @wheels.setter
    def wheels(self, wheels: int) -> None:
        print("setting wheels to", wheels)
        self._wheels = wheels

But this wouldn't work: the wheels setter receives the wheels property on init, instead of 4.

>>> Vehicle()
setting wheels to <property object at 0x1045ff100>
getting wheels
Vehicle(wheels=<property object at 0x1045ff100>)

After some digging into the source code of the dataclasses module, this is because of these lines:

# dataclasses.py, L730:732
    # If the default value isn't derived from Field, then it's only a
    # normal default value.  Convert it to a Field().
    default = getattr(cls, a_name, MISSING)

When processing the class, getattr(Vehicle, "wheels", MISSING) returns the wheels property. This value is later used as the default and passed to the setter via self.wheels = _wheels_dflt in the generated __init__.

We can't set a default using wheels: int = 4, because the later definition of the property would override it.

One solution that would work is ignoring that initial property value...

    @wheels.setter
    def wheels(self, wheels: int) -> None:
        if isinstance(wheels, property):
            return
        print("setting wheels to", wheels)
        self._wheels = wheels

Output:

>>> Vehicle()
getting wheels
Vehicle(wheels=4)

This could be moved to a minimal dataclass_property implementation, like so...

class dataclass_property(property):
    def __set__(self, obj, value):
        if isinstance(value, property):
            # dataclasses tries to set a default and uses the
            # getattr(cls, name). But the real default will come
            # from: `_attr = field(..., default=...)`.
            return
        super().__set__(obj, value)

Usage would be to replace @property with @dataclass_property.

I won't comment on the level of hackiness, but any usage of the approach described in this blog post was already in the "NO WARRANTY PROVIDED" zone. :-)

Copy link

Thanks for this walk through and the short and sweet "recipe" at the end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
comments Comments from utterances
Projects
None yet
Development

No branches or pull requests

5 participants