如何注释一个函数产生一个数据类?

How to annotate that a function produces a dataclass?

假设你想像这样包装 dataclass 装饰器:

from dataclasses import dataclass

def something_else(klass):
    return klass

def my_dataclass(klass):
    return something_else(dataclass(klass))

my_dataclass and/or something_else 应该如何注解表明return类型是数据类? 请参阅以下示例,了解内置 @dataclass 如何工作,但自定义 @my_dataclass 不工作:


@dataclass
class TestA:
    a: int
    b: str

TestA(0, "") # fine


@my_dataclass
class TestB:
    a: int
    b: str

TestB(0, "") # error: Too many arguments for "TestB" (from mypy)

问题是 mypy 理解 metaclass decorator 及其魔力 关于__init__,但是不明白 dataclass function:

@dataclass
class Test:
    a: int


# Mypy: Revealed type is "def (self: Test, a: builtins.int)"
reveal_type(Test.__init__)


class Test2:
    a: int

#Mypy: Revealed type is "def (self: builtins.object)"
reveal_type(dataclass(Test2).__init__)

如您所见,Test2__init__ 方法不接受任何参数。

PEP 681 之前没有可行的方法来做到这一点。

A dataclass 描述的不是类型而是转换。 Python 的类型系统无法表达其实际效果 – @dataclassMyPy Plugin which inspects the code, not just the types. This is triggered on specific decorators 在不了解其实现的情况下处理。

dataclass_makers: Final = {
    'dataclass',
    'dataclasses.dataclass',
}

虽然可以提供自定义 MyPy 插件,但这通常超出了大多数项目的范围。 PEP 681 (Python 3.11) 添加了一个通用的“这个装饰器的行为类似于 @dataclass”-标记,可用于从注释到字段的所有转换器。

PEP 681 可通过 typing_extensions.

用于早期 Python 版本

执行数据classes

对于纯类型替代方案,定义您的自定义装饰器以获取数据class并修改它。一个数据class可以通过它的__dataclass_fields__字段来识别。

from typing import Protocol, Any, TypeVar, Type
import dataclasses

class DataClass(Protocol):
    __dataclass_fields__: dict[str, Any]

DC = TypeVar("DC", bound=DataClass)

def my_dataclass(klass: Type[DC]) -> Type[DC]:
    ...

这允许类型检查器理解并验证需要dataclass class。

@my_dataclass
@dataclass
class TestB:
    a: int
    b: str

TestB(0, "")  # note: Revealed type is "so_test.TestB"

@my_dataclass
class TestC:  # error: Value of type variable "DC" of "my_dataclass" cannot be "TestC"
    a: int
    b: str

自定义 dataclass-like 装饰器

PEP 681 dataclass_transform 装饰器是 其他 装饰器的标记,表明它们的行为“像”@dataclass。为了匹配 @dataclass 的行为,必须使用 field_specifiers 来指示字段以相同的方式表示。

from typing import dataclass_transform, TypeVar, Type
import dataclasses

T = TypeVar("T")

@dataclass_transform(
    field_specifiers=(dataclasses.Field, dataclasses.field),
)
def my_dataclass(klass: Type[T]) -> Type[T]:
    return something_else(dataclasses.dataclass(klass))

自定义数据class装饰器可以将所有关键字都作为@dataclassdataclass_transform 可用于标记它们各自的默认值,即使装饰器本身不接受为关键字。