Python:继承基本Dataclass的Dataclass,如何将值从基本类升级到新类?

18

如何将基础数据类的值升级到继承它的数据类中?

示例(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()

打印出 "Hi Alex"

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

返回随机陌生人 "Person(name='Bob', smell='OK')"。

如何将随机陌生人变成朋友?

Friend(random_stranger)

返回 "Friend(name=Person(name='Bob', smell='OK'), smell='good')"。

我想要的结果是 "Friend(name='Bob', smell='OK')"。

Friend(random_stranger.name, random_stranger.smell)

这个方法可以运行,但我如何避免复制所有字段?

或者说,我不能在继承自dataclasses的类上使用@dataclass装饰器吗?


2
也许更简单的方法是在Person类中创建一个方法 def become_friend(self): return Friend(self.name, self.smell, other_parameters) - sanyassh
你还不熟悉dataclasses吗?但是你尝试过改变类吗?rs.class = Friend。这在为等价性设计的类上很有效(状态机模式)。 - JL Peyret
1
@JLPeyret 那听起来像是一场灾难的好配方。 - Arne
4个回答

19
你所要实现的功能可以通过工厂方法模式来实现,可以在Python类中直接使用@classmethod关键字进行实现。
只需在基类定义中包含一个数据类工厂方法,就像这样:
import dataclasses

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

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

任何继承此基类的新数据类现在都可以像这样创建彼此的实例[1]:
@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]如果您的子类引入了没有默认值的新字段,您试图从子类实例创建父类实例,或者您的父类具有init-only参数,则无法正常工作。


请参见 https://dev59.com/OlQJ5IYBdhLWcg3wAA1S#74184041,了解为什么当字段本身是数据类时,此答案会出现错误。 - Ethereal
这并没有展示出Friend有额外属性的情况,那么会发生什么呢? - undefined

3

您可能不希望将 class 本身作为可变属性,而是使用诸如枚举之类的东西来指示此类状态。根据要求,您可以考虑以下几种模式之一:

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()

你可能也希望以trait/mixin的方式实现这个功能。不需要创建一个名为 GreetablePerson 的类,而是创建一个名为 Greetable 的抽象类,让你的具体类继承它们两个。
你还可以考虑使用优秀的后移 attrs 包,这将使你更加灵活。同时,你可以使用 evolve() 函数创建一个新对象。
friend = attr.evolve(random_stranger, status=RelationshipStatus.FRIEND)

1
Person 类中设置 metaclass=ABCMeta 的目的是什么?Person 类仍然可以直接实例化。 - Yu Chen
你是对的。为了使其无法实例化,必须添加一个 abstractmethod,然后在尝试实例化时会引发异常。我不知道是否同意需要一个 abstractmethod,我可能会将其视为一种遗漏。但是 Python 倾向于对类型错误保持非攻击性,因此这可能是 Python 哲学的问题。我会将它保留下来以表明意图,并且除非它代表“人”的通用接口部分,否则也不会添加 abstractmethod。在这种情况下,也许 say_hi() 会比较合适。 - Bob Zimmermann

3

dataclasses.asdict 是递归的(详见文档), 所以如果字段本身是数据类,则出现在其他答案中的 dataclasses.asdict(instance) 会出错。相反,应该定义:

from dataclasses import fields

def shallow_asdict(instance):
  return {field.name: getattr(instance, field.name) for field in fields(instance)}

使用它来初始化一个Friend对象,以Person对象的字段为基础:

friend = Friend(**shallow_asdict(random_stranger))

assert friend == Friend(name="Bob", smell="OK")

0

vars(stranger) 会返回一个数据类实例 stranger 的所有属性的字典。由于数据类的默认 __init__() 方法接受关键字参数,twin_stranger = Person(**vars(stranger)) 将创建一个新实例,并复制其值。如果您提供了额外的参数,那么这也适用于派生类,例如 stranger_got_friend = Friend(**vars(stranger), city='Rome')

from dataclasses import dataclass


@dataclass
class Person:
    name: str
    smell: str


@dataclass
class Friend(Person):
    city: str

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


friend = Friend(name='Alex', smell='good', city='Berlin')
friend.say_hi()  # Hi Alex
stranger = Person(name='Bob', smell='OK')
stranger_got_friend = Friend(**vars(stranger), city='Rome')
stranger_got_friend.say_hi()  # Hi Bob

2
在数据类中,不应使用 vars 来获取属性,而是应该使用 dataclass.asdictvars 将包括仅初始化和类变量,并且如果使用 __slots__ 存储属性,则无法正常工作,这些都是不支持的坏处。这就是为什么存在 dataclass.asdict 的原因。 - Arne
dataclass.asdict 在我的使用场景中无法正常工作,因为它会递归地转换子数据类(文档中指出:“数据类、字典、列表和元组都会被递归处理。”)。我的数据类通常具有应保持为数据类的子数据类。 - phispi
没错,我能理解它对你的情况不起作用。 - Arne

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接