Skip to content

Python 数据类的可选属性

问题描述

在使用 Python 数据类(dataclass)时,我们常常会遇到需要定义可选属性的情况。具体来说,我们希望某些属性在某些实例中存在,而在其他实例中不存在,而不是简单地设置为 None 或其他默认值。

考虑以下示例:

python
from dataclasses import dataclass

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: # 这里应该怎么写?
    
kennys_stuff = {
    'knife': True,
    'fork': True
}

print(CampingEquipment(**kennys_stuff))

目标是:当 kennys_stuff 字典中包含 missing_flask_size 键时,实例中应该有这个属性;当不包含时,实例中不应该有这个属性(而不是设置为 None)。

解决方案

1. 使用默认值(推荐方法)

最简单且最常用的方法是为可选属性设置默认值:

python
from dataclasses import dataclass
from typing import Optional

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: Optional[int] = None
    
kennys_stuff = {
    'knife': True,
    'fork': True
}

print(CampingEquipment(**kennys_stuff))
# 输出: CampingEquipment(knife=True, fork=True, missing_flask_size=None)

TIP

这种方法在实践中最为常用,因为它简单且符合 Python 数据类的设计理念。虽然属性仍然存在于实例中,但通过检查其值是否为 None 可以确定用户是否提供了该值。

2. 使用 InitVar 和 post_init

如果需要更精细的控制,可以使用 InitVar 结合 __post_init__ 方法:

python
from dataclasses import dataclass, InitVar
from typing import Optional

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: InitVar[Optional[int]] = None

    def __post_init__(self, missing_flask_size):
        if missing_flask_size is not None:
            self.missing_flask_size = missing_flask_size

kennys_stuff = {
    'knife': True,
    'fork': True
}

ce = CampingEquipment(**kennys_stuff)
print(hasattr(ce, 'missing_flask_size'))  # 输出: False

INFO

使用这种方法,只有当用户明确提供了 missing_flask_size 值时,实例才会拥有该属性。但是请注意,这会破坏数据类的某些功能(如自动生成的 __repr__ 方法)。

3. 使用继承和工厂函数

如果需要严格地区分有无该属性的情况,可以使用继承和工厂函数:

python
from dataclasses import dataclass

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool


@dataclass
class CampingEquipmentWithFlask(CampingEquipment):
    missing_flask_size: int


def equipment(**fields):
    if 'missing_flask_size' in fields:
        return CampingEquipmentWithFlask(**fields)
    return CampingEquipment(**fields)
    

kennys_stuff = {
    'knife': True,
    'fork': True
}

print(equipment(**kennys_stuff))  # 输出: CampingEquipment(knife=True, fork=True)

WARNING

这种方法虽然类型安全,但增加了代码复杂度,可能不适合简单场景。

不推荐的解决方案

使用 field(default=None)

虽然这种方法可以达到类似效果,但它只是设置默认值为 None,而不是真正让属性可选:

python
from dataclasses import dataclass, field

@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: int = field(default=None)

动态删除属性

这种方法破坏了数据类的完整性,不推荐使用:

python
@dataclass
class Element:
    type: str
    src: str
    scale: int 
    duration: float = -1

    def get_data(self) -> None:
        if self.type != "image":
            self.__dict__.pop('scale')
            return self.__dict__
        return self.__dict__

DANGER

动态删除属性会破坏数据类的预期行为,可能导致意外的错误,强烈不推荐使用这种方法。

总结

在实际开发中,应根据具体需求选择合适的方法:

  • 大多数情况:使用 Optional[类型] = None 设置可选属性
  • 需要精细控制:使用 InitVar__post_init__ 方法
  • 要求严格类型区分:使用继承和工厂函数
代码示例对比
python
# 方法1:默认值
@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: Optional[int] = None

# 方法2:InitVar
@dataclass
class CampingEquipment:
    knife: bool
    fork: bool
    missing_flask_size: InitVar[Optional[int]] = None
    
    def __post_init__(self, missing_flask_size):
        if missing_flask_size is not None:
            self.missing_flask_size = missing_flask_size

记住,数据类的设计初衷是简化类的创建,而不是支持动态属性。如果需要完全动态的属性,可以考虑使用字典或 SimpleNamespace