我正在尝试编写一个示例,其中我想在类型检查期间使用冻结数据类实例并将其与普通数据类交换,以避免支付冻结数据类的实例化成本。
目标是确保实例在类型检查器中是不可变的,并在运行时使用常规数据类。这是片段:
from dataclasses import dataclass
from typing import TYPE_CHECKING
from functools import partial
if TYPE_CHECKING:
frozen = partial(dataclass, frozen=True)
else:
frozen = dataclass
@frozen
class Foo:
x: int
y: int
foo = Foo(1, 2) # mypy complains about the number of arguments
foo.x = 3 # instead, mypy should complain here
这在运行时按预期工作,但运行 mypy 会引发此错误。 Pyright 也给了我同样的错误:
foo.py:49: error: Too many arguments for "Foo" [call-arg]
在此片段中,类型检查器可以捕获突变错误:
@dataclass(frozen=True)
class Foo:
x: int
y: int
foo = Foo(1, 2)
foo.x = 3 # mypy correctly catches the error here
所以,我猜测类型检查器不喜欢我使用别名
frozen = dataclass
或 frozen = partial(...)
。如何正确注释它,以便类型检查器了解它是一个数据类实例,并且不会抱怨参数计数不匹配?
P.S:这只是一个练习。我知道打开
dataclass(frozen=True)
更容易,而且在这种情况下我不应该关心性能。在阅读了 Tin Tvrtković 的关于使 attr 类实例在编译时冻结的博客帖子后,我受到启发尝试此操作。
@dataclass_transform
与 frozen_default = True
一起使用:
if TYPE_CHECKING:
T = TypeVar('T')
@dataclass_transform(frozen_default = True)
def frozen(cls: type[T]) -> type[T]:
...
else:
frozen = dataclass
frozen_default
是在 Python 3.12 中添加的。然而,由于 @dataclass_transform
故意接受所有关键字参数,所以 3.11 (完全相同)和 lower (使用 typing_extensions
)就可以了。
reveal_type(Foo) # mypy => (x: int, y: int) -> Foo
# pyright => type[Foo]
foo = Foo(1, 2) # mypy + pyright => fine
foo.x = 3 # mypy => error: Property "x" defined in "Foo" is read-only
# pyright => error: Cannot assign member "x" for type "Foo"; "Foo" is frozen