在 Python 里,使用 type hint 定义一个数据模型似乎都采用了 dataclass
类似的方式,也就是下面这样的代码。这种代码确实是可以的,但是会出现一个问题,当你直接使用 Data.value
时,它会出现本不该出现的类型推导行为——Data.value
会被视作一个 str
类型的变量。而实际上它的类型应当是 field
函数返回的 Field
,当然由于 dataclass
的屏蔽了这种访问,所以你运行 print(Data.value)
会抛出一个错误。如果不屏蔽,那么它的类型就应当是一个 dataclasses.Field
。
from dataclasses import dataclass, field
@dataclass
class Data:
value: str = field()
这个问题的根源是在类定义处直接写出的变量是类变量,尽管你可能想定义一个仅实例可用的变量,但由于历史原因,你这么写它确实就是一个类变量。dataclass
作为标准库,各个类型推导或检查器当然可以把它当作特例来处理(但我试了 pylance 没有把它当特例),而如果我们自己写一个定义模型用的库,各个类型检查器显然不会为了我们做特别的优化。pydantic 的解决方案是为 mypy 编写插件。
而我的解决方案是直接使用 typing 的规则,而不是破坏规则。
from __future__ import annotations
from typing import TypeVar, Generic, Type, overload
FieldType = TypeVar("FieldType")
class Field(Generic[FieldType]):
def __init__(self, type_: Type[FieldType]) -> None:
self.type_ = type_
@overload
def __get__(self, instance: None, cls: type = None) -> Field:
...
@overload
def __get__(self, instance: object, cls: type = None) -> FieldType:
...
def __get__(self, instance, cls):
if instance is None: # Call from class
return self
try:
return instance.__dict__[self._name]
except KeyError:
raise AttributeError(f"{instance} has no attribute '{self._name}'") from None
def __set__(self, instance: object, value: FieldType) -> None:
if not isinstance(value, self.type_):
raise TypeError(
f"{instance.__class__.__qualname__}.{self._name} expects {self.type_} type, but gives {type(value)}"
)
instance.__dict__[self._name] = value
def __delete__(self, instance: object) -> None:
try:
del instance.__dict__[self._name]
except KeyError:
raise AttributeError(f"{instance} has no attribute '{self._name}'") from None
def __set_name__(self, owner: object, name: str) -> None:
self._name = name
试着用这个 Field
来定义一个数据模型,并且使用 mypy 等类型检查器来检查,你会发现一切都很清晰,当你通过类直接调用 .value
时,类型检查器会告诉你它是一个 Field
对象,当你通过对象调用或赋值时,类型检查器能清楚的知道它是 int
类型,并且在你错误的赋值时,给出类型检查错误。这一切,都不需要你编写指定类型检查程序的插件。
class Data:
value = Field(int)
Data.value
Data().value
Data().value = "1"
这个秘诀就在于两个 overload
之中,当我们通过类直接调用 .value
时,根据 Python 的规则,一个良好的类型检查器会知道此时 instance
是 None
,所以它会得出此时返回类型为 Field
的结论,而通过对象访问时则返回 FieldType
。