Python 类型:使用类变量的值作为(mixin)方法的返回类型

Ste*_*fke 5 python type-hinting mypy

概括

如何使用类变量的值(这是一个类对象)作为 Python 类型 / mypy 的(mixin)方法的返回类型?

这是一个最小的示例,您将在下面找到真正的、更复杂的用例:

from typing import Generic, Type, TypeVar


T = TypeVar('T')


class Base(Generic[T]):
    return_type: Type[T]
    value: T  # This attribute is only needed for this minimal example


class Mixin:
    def get(self: Base[T]) -> T:  # mypy: The erased type of self "Base" is not a supertype of its class "Mixin"
        return self.return_type(self.value)  # mypy: Too many arguments for "object"


class Concrete(Mixin, Base[int]):
    return_type = int

    def __init__(self):
        self.value = 3


c = Concrete()
x: str = c.get()  # mypy: expected incompatible types (str vs int) error :)
Run Code Online (Sandbox Code Playgroud)

Base当设置为 的超类时,我可以摆脱第二个错误Mixin,但这并不是我真正想要的。不过,我现在有了想法,如何正确定义return_type: Type[T].

我已经阅读了 python 打字文档以及 mypy 文档,但什么也没找到。网络搜索也没有产生有用的结果。

我真正想做的事

我目前正在编写一个 REST 客户端,其架构类似于python-gitlab

  • 用户使用ApiClient知道 API URL 并执行所有 HTTP 请求的类。

  • API 的端点由 REST 管理器类表示,这些管理器类是ApiClient. 根据端点的功能,REST 管理器可以列出端点的对象、获取单个对象或创建、更新和删除对象。

  • RestManager 返回并接收“哑”数据类(例如,attrspydantic模型)

  • 具体的REST 管理器子类化了一个RestManager基类和用于HTTP 操作的各种mixins,例如,GetMixin用于通过ID 获取单个对象的a。

  • 具体的 REST 管理器有一个类变量,用于保存它将返回的对象的类。

  • 在mixin类中,我想表达“此方法返回对象类的实例,子类restmanager定义为类变量”。

用法示例:

client = ApiClient('https://example.com/myapi/v1')
item = client.items.get(42)
assert isinstance(item, Item)
Run Code Online (Sandbox Code Playgroud)

执行:

from typing import ClassVar, Type, TypeVar


T = TypeVar(T)


class Item:
    """Data class that represents objects of the "items" endpoint"""
    pass


class ApiClient:
    """Main object that the user works with."""
    def __init__(self, url: str):
        self.url = url
        # There is one manager instance for each endpoint of the API
        self.items = ItemManager(self) 
        # self.cats = CatManager(self)

    def http_get(self, path: str) -> 'Response':
        ...  # Request the proper url and return a response object


class RestManager:
    """Base class for REST managers."""
    _path: ClassVar[str]
    _obj_cls: ClassVar[Type[T]]  # Concrete subclasses set this with an object class, e.g., "Item"

    def __init__(self, client: ApiClient):
        self.client = client

    @property
    def path(self) -> str:
        return self._path


class GetMixin:
    """Mixin for getting a single object by ID"""
    def get(self: RestManager, id: int) -> T:  # Return type is the value the subclass' "_obj_cls" attribute
        response = self.client.http_get(f'{self.path}/{id}')
        return self._obj_cls(**response.json())


class ItemsManager(GetMixin, RestManager):
    """Concrete manager for "Item" objects."""
    _path = '/items'
    _obj_cls = Item  # This is the return type of ItemsManager.get()


client = ApiClient()
item = client.items.get(42)
assert isinstance(item, Item)
Run Code Online (Sandbox Code Playgroud)

Ste*_*fke 0

我找到了一个有效的解决方案。这不是最优的,因为 Mixin 类需要继承自RestManager. 但 mypy 可以成功推导出预期的返回类型。

该代码需要 Pyhton 3.10。在 3.11 中,您可以assert_type直接从typing. 对于旧版本,您需要使用typing.Type[T]而不是type[t].

from typing import ClassVar, Generic, TypeVar

from typing_extensions import assert_type


T = TypeVar("T")


class Item:
    """Data class that represents objects of the "items" endpoint"""


class ApiClient:
    """Main object that the user works with."""

    def __init__(self, url: str):
        self.url = url
        # There is one manager instance for each endpoint of the API
        self.items = ItemsManager(self)
        # self.cats = CatManager(self)

    def http_get(self, path: str) -> "Response":
        ...  # Request the proper url and return a response object


class RestManager(Generic[T]):
    """Base class for REST managers."""

    _path: ClassVar[str]
    _obj_cls: type[T]

    def __init__(self, client: ApiClient):
        self.client = client

    @property
    def path(self) -> str:
        return self._path


class GetMixin(RestManager, Generic[T]):
    """Mixin for getting a single object by ID"""

    def get(self, iid: int) -> T:
        response = self.client.http_get(f"{self.path}/{iid}")
        return self._obj_cls(**response.json())


class ItemsManager(GetMixin[Item], RestManager[Item]):
    """Concrete manager for "Item" objects."""

    _path = "/items"
    _obj_cls = Item


def main() -> None:
    client = ApiClient("api")
    item = client.items.get(42)
    assert_type(item, Item)
    assert isinstance(item, Item)


if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)