簡體   English   中英

Python 輸入:使用 class 變量的值作為(mixin)方法的返回類型

[英]Python typing: Use a class variable's value as return type of a (mixin) method

概括

我如何使用 class 變量的值(這是一個 class 對象)作為帶有 Python typing / 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 :)

當將Base設置為Mixin的 super class 時,我可以擺脫第二個錯誤,但這並不是我真正想要的。 不過,我現在知道如何正確定義return_type: Type[T]

我已經閱讀了 python 打字文檔以及 mypy 文檔,但一無所獲。 web 搜索也沒有產生有用的結果。

我真正想做的

我目前正在編寫一個 REST 客戶端,其架構類似於python-gitlab

  • 用戶使用ApiClient class,它知道 API URL 並執行所有 HTTP 請求。

  • API 的端點由作為 ApiClient 屬性的ApiClient管理器類表示。 根據端點的功能,REST 管理器可以列出端點的對象,獲取單個 object,或者創建、更新和刪除 object。

  • RestManager 返回並接收“啞”數據類(例如, attrspydantic模型)

  • 具體的 REST 管理器將RestManager基類 class 和用於 HTTP 操作的各種 mixin 子類化,例如,用於通過 ID 獲取單個GetMixin的 GetMixin。

  • 具體的 REST 管理器有一個 class 變量,該變量保存它將要返回的對象的 class。

  • 在 mixin 類中,我想表達“此方法返回 object class 的實例,子類 restmanager 定義為 class 變量”。

用法示例:

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

執行:

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)

免責聲明:我沒有仔細閱讀您的真實用例,所以我可能是錯的。 以下分析基於您的簡化示例。

我認為mypy不支持這一點。 目前mypy假設(並且正確地如此)方法中的self類型是 class 的子類型。

然而,為了讓你的 mixin 工作,必須對它可以混合的類的種類進行限制。 例如,在您的簡化示例中,class 必須具有return_typevalue屬性。 我的建議是,您也可以將它們添加為您的 mixin class 的注釋,結果如下:

T = TypeVar('T')


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


class Mixin(Generic[T]):
    return_type: Type[T]
    value: T

    def get(self) -> T:  # annotation on `self` can be dropped
        return self.return_type(self.value)

注意最后一行在mypy中仍然是一個錯誤,因為它不能證明self.return_type__init__方法只接受一個參數。 您可以使用typing.Protocol 3.7 中引入的 typing.Protocol 來解決此問題,但這可能有點矯枉過正。 恕我直言,如果您有一些您確信是正確的簡單代碼,有時使用一點# type: ignore最適合您。

我找到了一個有效的解決方案。 這不是最優的,因為 Mixin 類需要繼承自RestManager 但是 mypy 可以成功推導出預期的返回類型。

該代碼需要 Pyhton 3.10。 在 3.11 中,您可以直接從typing中導入assert_type 對於舊版本,您需要使用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()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM