AberSheeran
Aber Sheeran

Type hint 在定义数据模型时的应用

起笔自
所属文集: 程序杂记
共计 2434 个字符
落笔于

在 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 的规则,一个良好的类型检查器会知道此时 instanceNone,所以它会得出此时返回类型为 Field 的结论,而通过对象访问时则返回 FieldType

如果你觉得本文值得,不妨赏杯茶
基于抢占式 Keepalived 的健康检查设计
WSGI 项目的 Lifespan