Python 3.11 + Mypy 泛型类型提示,可以接受类型和类型定义

问题描述 投票:0回答:2

我正在尝试编写一个带有泛型的 arg getter,它根据泛型类型输入将结果转换为正确的类型:

T = TypeVar("T")
R = TypeVar("R")
P = ParamSpec("P")
GenericCallable = Callable[P, R]

def get_arg(
    name_or_pos: str | int,  # 0-based index
    typ: type[T],
    args: tuple[Any, ...] = (),
    kwargs: dict[str, Any] = {},
    fallback: T | None = None,
    throw: bool = False,
) -> T:
    found: Any = None
    if isinstance(name_or_pos, str):
        name = name_or_pos
        found = kwargs.get(name, None)
        if found is None:
            if throw:
                raise RuntimeError(
                    f"Could not find keyword argument '{name}' of type '{typ!r}' in"
                    f" {kwargs!r}"
                )
            found = fallback
    elif isinstance(name_or_pos, int):
        pos = name_or_pos
        if pos >= len(args):
            if throw:
                raise RuntimeError(
                    f"Could not find argument at (0-based) pos={pos}, not enough"
                    f" arg(s) (only have {len(args)}: {', '.join(*args)}"
                )
            found = fallback  # type: ignore

        found = args[pos]  # type: ignore

    if found is None:
        found = fallback

    return cast(T, found)

给出以下测试用例:

def test_get_arg_for_functions():
    def func1(a: str, b: str) -> str:
        return f"{a} {b}"

    def func2(a: str, b: str, c: str) -> str:
        return f"{a} {b} {c}"

    class Class1:
        def __init__(self, x: int, y: str) -> None:
            self.x = x
            self.y = y

    kwargs = {"func1": func1, "func2": func2, "class1": Class1(1, "a")}

    got_func1: Callable[..., str] = get_arg("func1", type(func1), kwargs=kwargs)
    assert got_func1("a", "b") == "a b"

    got_class1_instance = get_arg("class1", Class1, kwargs=kwargs)
    assert got_class1_instance.x == 1
    assert got_class1_instance.y == "a"

    Func1Type = GenericCallable[[str, str], str]
    got_func1_from_type = get_arg("func1", Func1Type, kwargs=kwargs)
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    assert got_func1_from_type("a", "b") == "a b"

Mypy 抱怨以下内容,对于

got_func1_from_type

Argument 2 to "get_arg" has incompatible type "<typing special form>"; expected "type[Never]"

(我相信)这是因为它期待

type[T]
并返回
T
,但这里我传递了一个类型定义。

所以我尝试用

type: T
代替并返回
T
(或者,为了避免混淆,我们称其为
D

def get_arg(
    ...
    typ: D,
    ...
) -> D:
    ...

现在,Mypy 可以让我将类型定义传递给泛型

typ
:

Func1Type = GenericCallable[[str, str], str]
    got_func1_from_type = get_arg_def("func1", Func1Type, kwargs=kwargs)
    assert got_func1_from_type("a", "b") == "a b"
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

但是

got_func1_from_type
不可调用,因为它是
type[(str, str) -> str]
并且Mypy抱怨:

Cannot instantiate type "GenericCallable[(str, str), str]"

这是有道理的,但我不知道该怎么办。我尝试做一些聪明的事情,例如:

D = TypeVar("D", bound=type)
# or
D = TypeVar("D", bound=AnyType)
# or
D = TypeVar("D", bound=type[AnyType])

...但这些都将类型定义绑定到

type[TypeDef]
而不是
actual instance of type D

有没有一种方法可以强制转换或声明它返回正确类型的实际实例,同时允许它适用于所有这些用例?

P = ParamSpec("P")
R = TypeVar("R")
GenericCallable = Callable[P, R]

a_str = get_arg("a", str, kwargs=kwargs) # a_str: str

FuncType = GenericCallable[..., str]
a_func = get_arg("func", FuncType, kwargs=kwargs) # a_func: FuncType

an_instance = get_arg("class", MyClass, kwargs=kwargs) # an_instance: MyClass

(P.S.,我用

@overload
尝试了一堆奇特的东西,但无法让它比工会发挥得更好)

python mypy python-typing python-3.11
2个回答
0
投票

有点激进的想法(这可能取决于你的用例) - 但为什么 kwargs 不能只是一个

TypedDict
?我一直在研究各种代码结构,但我不断发现的一件事是,在每次使用
get_arg
时,参数的类型与每个键相关联都是已知的。如果类型已知,为什么我们不能使用更结构化的字典,并且类型检查器可以告诉您是否缺少某个项目?

from typing import Callable, TypedDict

def func1(a: str, b: str) -> str:
    return f"{a} {b}"

def func2(a: str, b: str, c: str) -> str:
    return f"{a} {b} {c}"

class Class1:
    def __init__(self, x: int, y: str) -> None:
        self.x = x
        self.y = y

class MyDictType(TypedDict):
    func1: Callable[[str,str], str]
    func2: Callable[[str,str, str], str]
    class1: Class1

def test_get_arg_for_functions():
    kwargs: MyDictType = {"func1": func1, "func2": func2, "class1": Class1(1, "a")}

    got_func1 = kwargs["func1"]
    assert got_func1("a", "b") == "a b"

    got_class1_instance = kwargs["class1"]
    assert got_class1_instance.x == 1
    assert got_class1_instance.y == "a"

    got_func1_from_type = kwargs["func1"]
    
    assert got_func1_from_type("a", "b") == "a b"

感谢这有点偏离主题,但认为它可能对你仍然有用


0
投票

看起来类型检查器不喜欢这样:

import typing_extensions as t

F = t.TypeVar("F")

def get(Type: type[F], /) -> F: ...
>>> FuncType = t.Callable[[int, str], bytes]
>>> get(FuncType)  # mypy: Argument 1 to "get" has incompatible type "<typing special form>"; expected "type[Never]"  [arg-type]

他们对于是否接受回调

typing.Protocol
而不是
collections.abc.Callable
也不一致。您可以在类型检查器游乐场中看到一些实验:

我认为 Pyright 的实现更正确,如果你读过 PEP 484 - 类对象的类型

有时您想讨论类对象,特别是从给定类继承的类对象。这可以拼写为

Type[C]
,其中
C
是一个类。

FuncType
中的
FuncType = t.Callable[[int, str], bytes]
不是一个类,因此它与
type[F]
不兼容。然而,
typing.Protocol
子类确实是一个类,因此应该可以分配给
type[F]

我不太同意在

collections.abc.Callable
的地方拒绝打字结构
type[F]
有用,尤其是在这种情况下 - 但那是另一个故事了。


一个解决方法(也在这些游乐场中给出)是稍微撒谎并使用

typing.Generic

import typing_extensions as t

P = t.ParamSpec("P")
R = t.TypeVar("R", covariant=True)

class GenericCallable(t.Generic[P, R]):
    def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> R: ...
>>> Func1Type = GenericCallable[[str, str], str]
>>> reveal_type(get_arg("func1", Func1Type, kwargs=kwargs))  # mypy: Revealed type is "GenericCallable[[builtins.str, builtins.str], builtins.str]"
© www.soinside.com 2019 - 2024. All rights reserved.