为非特定数据类的实例提供类型提示

52

我有一个接受任何dataclass实例的函数,应该如何为其添加适当的类型提示?

在Python文档中没有官方的解释。


这是我目前一直在做的事情,但我认为这不正确。

from typing import Any, NewType

DataClass = NewType('DataClass', Any)
def foo(obj: DataClass):
    ...

另一个想法是使用 Protocol,并结合这些类属性:__dataclass_fields____dataclass_params__


什么?使用@dataclass装饰器和不使用的类之间没有明显差异。Dataclasses不实现任何特殊方法,也没有任何特殊属性。区分“dataclass”和“regular”类毫无意义。 - Aran-Fey
6
该功能将数据类解包成字典,并具有特殊属性“__dataclass_fields__”、“__dataclass_params__”,正如问题所述。对于具名元组也是这样的,尽管它们仅继承自“tuple”,但它们确实具有类型提示。 - moshevi
这些属性是未记录的,因此我建议不要依赖它们的存在。然而,我错了,没有观察到任何区别;像dataclasses.astuple这样的函数只适用于数据类。 - Aran-Fey
那么带有astuple方法的Protocol?听起来不错,但有点不稳定。不确定为什么他们决定使用装饰器而不是通过继承和元类像namedtuple一样创建dataclass - moshevi
astuple 不是一个方法,所以那样做行不通。我认为这不能通过 typing 来完成,因为数据类在技术上不是一种类型。它们不公开基类或特定的公共接口。换句话说,接收到非数据类而不是数据类更接近于 ValueError 而不是 TypeError。 - Aran-Fey
2
没错,我读了源代码,Python实际上实现了一个名为_is_dataclass_instance的函数。它检查是否具有属性__dataclass_fields__,我认为这已经是最好的了。 - moshevi
3个回答

43

尽管它的名字是dataclasses.dataclass,但它并没有公开类接口。它只允许您以方便的方式声明自定义类,使其明显地将被用作数据容器。因此,理论上很难编写仅适用于dataclasses的代码,因为dataclasses实际上只是普通的类。

在实践中,你仍有几个原因希望声明仅适用于dataclasses的函数,下面是如何实现:

from dataclasses import dataclass
from typing import ClassVar, Dict, Protocol


class IsDataclass(Protocol):
    # as already noted in comments, checking for this attribute is currently
    # the most reliable way to ascertain that something is a dataclass
    __dataclass_fields__: ClassVar[Dict] 

def dataclass_only(x: IsDataclass):
    ...  # do something that only makes sense with a dataclass

@dataclass
class Foo:
    pass

class Bar:
    pass

dataclass_only(Foo())  # a static type check should show that this line is fine ..
dataclass_only(Bar())  # .. and this one is not

这种方法也是你在问题中暗示的。如果你想采用这种方法,请注意你需要使用第三方库,例如mypy来为你进行静态类型检查。如果你使用的是Python 3.7 或更早的版本,则需要手动安装typing_extensions,因为Protocol仅成为了Python 3.8标准库的一部分。

还要注意,较旧版本的mypy(>=0.982)错误地期望__dataclass_fields__是一个实例属性,所以协议应该只是__dataclass_fields__: Dict[1]


当我最初编写此帖时,它还包括了The Old Way of Doing Things,即我们没有类型检查器时的方法。我把它保留下来,但不推荐再使用运行时失败处理这种特性:

from dataclasses import is_dataclass

def dataclass_only(x):
    """Do something that only makes sense with a dataclass.
    
    Raises:
        ValueError if something that is not a dataclass is passed.
        
    ... more documentation ...
    """
    if not is_dataclass(x):
        raise ValueError(f"'{x.__class__.__name__}' is not a dataclass!")
    ...

[1]感谢 @Kound 更新并测试了ClassVar的行为。


能否在 @dataclass 注释器后面隐藏 if TYPE_CHECKING,将从一个虚拟类型继承的 monkeypatch 功能实现,以便仅影响类型检查?(即 IAmADataclass = type('IAmADataclass', (), {},当你使用 @dataclass Class Foo 时,它会有效地将其替换为 @dataclass Class Foo(IAmADataclass))。 - user3534080
@user3534080,你在评论中提出了一个复杂的问题。它值得有自己的回答,但简短的答案是,就实际目的而言,你想要的并不是非常有用的。在运行时修补继承意味着静态分析工具(如mypy或IDE使用的工具)无法正确地捕捉它们。 - Arne
看起来链接的 Github 问题已经解决了,这个答案可能需要快速更新。 - Gricey
3
谢谢你提醒我,链接的修复计划将会在0.920版本的mypy中发布,但目前还未发布。我会持续关注并在那个时候更新我的帖子。 - Arne
@Arne 看起来现在发布了 mypy 0.920 版本。 - Cnoor0171
显示剩余2条评论

6

有一个名为is_dataclass的辅助函数可供使用,它是从dataclasses中导出的。

它的基本作用就是:

def is_dataclass(obj):
    """Returns True if obj is a dataclass or an instance of a
    dataclass."""
    cls = obj if isinstance(obj, type) else type(obj)
    return hasattr(cls, _FIELDS) 

使用type获取实例的类型或者如果对象继承了type,获取对象本身的类型。

然后检查变量_FIELDS是否存在于这个对象上,它等于__dataclass_fields__。这基本上相当于其他答案中的方法。

如果要"为数据类定义类型",可以像下面这样操作:


class DataclassProtocol(Protocol):
    __dataclass_fields__: Dict
    __dataclass_params__: Dict
    __post_init__: Optional[Callable]

2

你确实可以使用 Protocol,但我建议将该 Protocol 作为一个 runtime_checkabledataclass 进行修饰:

@runtime_checkable
@dataclasses.dataclass
class DataclassProtocol(Protocol):
    pass

以上结果为:
  • 使用 DataclassProtocol 进行类型提示是可行的,并且对于类型检查器(如 mypy 0.982、PyCharm 2022.2.3 CE)来说有意义
  • isinstance(obj, DataclassProtocol) 等同于 dataclasses.is_dataclass(obj)
  • 因为 dataclasses.is_dataclass(DataclassProtocol),类型检查器对 dataclasses 的特殊处理生效
  • DataclassProtocol 不需要使用内部的 dataclass 字段

第一个点也可以通过之前提到的 Protocol 实现。第二个点则是通过使用 @runtime_checkable 装饰器实现的。 后两个点则依赖于使用 @dataclass 装饰器。

虽然这回答了问题,但我个人希望能将 DataclassProtocol 子类化为 DataclassInstanceProtocol,以便专门针对 not isinstance(obj, type) 进行优化。但直到现在,我还没有找到这个东西。


这个解决方案似乎只适用于数据类的实例。对于类本身,mypy会抱怨:“fun”的第一个参数类型“Type [DC]”不兼容;期望的是“DataclassProtocol”[arg-type]”(通过将数据类“DC”传递给“fun(dc:DataclassProtocol)”进行测试)。 - Noa
类型存根文档似乎建议使用@dataclass:https://typing.readthedocs.io/en/latest/source/stubs.html#decorators。 - wrwrwr

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