Python 3.10 方法中使用装饰器的类型提示

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

我正在尝试将

typing.Concatenate
typing.ParamSpec
一起使用来键入提示类的方法要使用的装饰器。装饰器仅接收标志,并且仅当类具有该标志作为成员时才运行。代码如下所示:

import enum
from typing import Callable, ParamSpec, Concatenate

P = ParamSpec("P")
Wrappable = Callable[Concatenate["Foo", P], None]

class Flag(enum.Enum):
    FLAG_1 = enum.auto()
    FLAG_2 = enum.auto()


def requires_flags(*flags: Flag) -> Callable[[Wrappable], Wrappable]:
    def wrap(func: Wrappable) -> Wrappable:
        def wrapped_f(foo: "Foo", *args: P.args, **kwargs: P.kwargs) -> None:
            if set(flags).issubset(foo.flags):
                func(foo, *args, **kwargs)
        return wrapped_f
    return wrap


class Foo:
    def __init__(self, flags: set[Flag] | None = None) -> None:
        self.flags: set[Flag] = flags or set()
        super().__init__()

    @requires_flags(Flag.FLAG_1)
    def some_conditional_method(self, some_int: int):
        print(f"Number given: {some_int}")


Foo({Flag.FLAG_1}).some_conditional_method(1)  # prints "Number given: 1"
Foo({Flag.FLAG_2}).some_conditional_method(2)  # does not print anything

这里使用

Concatenate
的要点是,修饰函数的第一个参数必须是
Foo
的实例,它与 Foo 的方法一致(第一个参数是
self
,是
Foo
的实例) )。装饰函数的其余参数可以是任何东西,因此允许
*args
**kwargs

mypy 无法执行上述代码,并显示以下内容:

error: Argument 1 has incompatible type "Callable[[Foo, int], Any]"; expected "Callable[[Foo, VarArg(Any), KwArg(Any)], None]"  [arg-type]
note: This is likely because "some_conditional_method of Foo" has named arguments: "self". Consider marking them positional-only

有一个问题是,在调用站点,我没有明确传递

Foo
的实例作为第一个参数(因为我将其作为方法调用)。有没有一种方法可以让我严格正确地输入这个内容?包装器本身是否需要以某种方式在类中定义,以便它可以直接访问
self

请注意,如果第 5 行更新为

Wrappable = Callable[P, None]
,则 mypy 通过,但这并不严格,因为我试图在类型中强制执行它只能用于
Foo
的方法(或接收
Foo
作为第一个参数的自由函数)。

类似地,如果我将

some_conditional_method
更新为自由函数而不是
Foo
上的方法,那么 mypy 也会通过(这与下面链接的 SO 问题一致)。在这种情况下,它实现了我所追求的严格性,但我真的希望能够将其应用于方法,而不仅仅是自由函数(事实上,它根本不需要应用于自由函数)。

这个问题在某种程度上是对装饰器的Python 3类型提示的扩展,但在方法中需要使用装饰器有细微的差别。

要明确的是,这个问题和那个问题之间的区别在于以下内容(如链接问题中所述)完美运行:

@requires_flags(Flag.FLAG_1)
def some_conditional_free_function(foo: Foo, some_int: int):
    print(f"Number given: {some_int}")

some_conditional_free_function(Foo({Flag.FLAG_1}), 1)  # prints "Number given: 1"
python decorator mypy typing
1个回答
0
投票

问题是由于 mypy 在应用于方法时要求装饰器签名匹配的方式引起的。要获得

Foo
方法所需的严格类型,请将
@classmethod
装饰器与
@requires_flags
装饰器结合使用,如下所示:

import enum
from typing import Callable, ParamSpec, Concatenate, Type

P = ParamSpec("P")
Wrappable = Callable[Concatenate["Foo", P], None]

class Flag(enum.Enum):
    FLAG_1 = enum.auto()
    FLAG_2 = enum.auto()

def requires_flags(*flags: Flag) -> Callable[[Wrappable], Wrappable]:
    def wrap(func: Wrappable) -> Wrappable:
        def wrapped_f(foo: "Foo", *args: P.args, **kwargs: P.kwargs) -> None:
            if set(flags).issubset(foo.flags):
                func(foo, *args, **kwargs)
        return wrapped_f
    return wrap

class Foo:
    def __init__(self, flags: set[Flag] | None = None) -> None:
        self.flags: set[Flag] = flags or set()
        super().__init__()

    @classmethod
    @requires_flags(Flag.FLAG_1)
    def some_conditional_method(cls: Type["Foo"], foo: "Foo", some_int: int):
        print(f"Number given: {some_int}")

Foo({Flag.FLAG_1}).some_conditional_method(Foo({Flag.FLAG_1}), 1)  # prints "Number given: 1"
Foo({Flag.FLAG_2}).some_conditional_method(Foo({Flag.FLAG_2}), 2)  # does not print anything
© www.soinside.com 2019 - 2024. All rights reserved.