Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Dataclass that inherits from base Dataclass, how do I upgrade a value from base to the new class?

How can I upgrade values from a base dataclass to one that inherits from it?

Example (Python 3.7.2)

from dataclasses import dataclass

@dataclass
class Person:
    name: str 
    smell: str = "good"    

@dataclass
class Friend(Person):

    # ... more fields

    def say_hi(self):        
        print(f'Hi {self.name}')

friend = Friend(name='Alex')
f1.say_hi()

prints "Hi Alex"

random_stranger = Person(name = 'Bob', smell='OK')

return for random_stranger "Person(name='Bob', smell='OK')"

How do I turn the random_stranger into a friend?

Friend(random_stranger)

returns "Friend(name=Person(name='Bob', smell='OK'), smell='good')"

I'd like to get "Friend(name='Bob', smell='OK')" as a result.

Friend(random_stranger.name, random_stranger.smell)

works, but how do I avoid having to copy all fields?

Or is it possible that I can't use the @dataclass decorator on classes that inherit from dataclasses?

like image 953
576i Avatar asked Feb 22 '19 10:02

576i


2 Answers

What you are asking for is realized by the factory method pattern, and can be implemented in python classes straight forwardly using the @classmethod keyword.

Just include a dataclass factory method in your base class definition, like this:

import dataclasses

@dataclasses.dataclass
class Person:
    name: str
    smell: str = "good"

    @classmethod
    def from_instance(cls, instance):
        return cls(**dataclasses.asdict(instance))

Any new dataclass that inherit from this baseclass can now create instances of each other[1] like this:

@dataclasses.dataclass
class Friend(Person):
    def say_hi(self):        
        print(f'Hi {self.name}')

random_stranger = Person(name = 'Bob', smell='OK')
friend = Friend.from_instance(random_stranger)
print(friend.say_hi())
# "Hi Bob"

[1] It won't work if your child classes introduce new fields with no default values, you try to create parent class instances from child class instances, or your parent class has init-only arguments.

like image 163
Arne Avatar answered Nov 01 '22 22:11

Arne


You probably do not want to have the class itself be a mutable property, and instead use something such as an enum to indicate a status such as this. Depending on the requirements, you may consider one of a few patterns:

class RelationshipStatus(Enum):
    STRANGER = 0
    FRIEND = 1
    PARTNER = 2

@dataclass
class Person(metaclass=ABCMeta):
    full_name: str
    smell: str = "good"
    status: RelationshipStatus = RelationshipStatus.STRANGER

@dataclass
class GreetablePerson(Person):
    nickname: str = ""

    @property
    def greet_name(self):
        if self.status == RelationshipStatus.STRANGER:
            return self.full_name
        else:
            return self.nickname

    def say_hi(self):
        print(f"Hi {self.greet_name}")

if __name__ == '__main__':
    random_stranger = GreetablePerson(full_name="Robert Thirstwilder",
                                      nickname="Bobby")
    random_stranger.status = RelationshipStatus.STRANGER
    random_stranger.say_hi()
    random_stranger.status = RelationshipStatus.FRIEND
    random_stranger.say_hi()

You may want, also, to implement this in a trait/mixin style. Instead of creating a GreetablePerson, instead make a class Greetable, also abstract, and make your concrete class inherit both of those.

You may also consider using the excellent, backported, much more flexible attrs package. This would also enable you to create a fresh object with the evolve() function:

friend = attr.evolve(random_stranger, status=RelationshipStatus.FRIEND)
like image 43
Bob Zimmermann Avatar answered Nov 01 '22 23:11

Bob Zimmermann