我们应该如何输入具有附加属性的可调用对象?

Ric*_*ler 7 python python-typing

作为一个玩具示例,让我们使用斐波那契数列:

def fib(n: int) -> int:
  if n < 2:
    return 1
  return fib(n - 2) + fib(n - 1)
Run Code Online (Sandbox Code Playgroud)

当然,如果我们尝试执行以下操作,计算机将会挂起:

print(fib(100))
Run Code Online (Sandbox Code Playgroud)

所以我们决定添加记忆功能。为了保持逻辑fib清晰,我们决定不改变fib,而是通过装饰器添加记忆:

from typing import Callable
from functools import wraps


def remember(f: Callable[[int], int]) -> Callable[[int], int]:
    @wraps(f)
    def wrapper(n: int) -> int:
        if n not in wrapper.memory:
            wrapper.memory[n] = f(n)
        return wrapper.memory[n]

    wrapper.memory = dict[int, int]()
    return wrapper


@remember
def fib(n: int) -> int:
    if n < 2:
        return 1
    return fib(n - 2) + fib(n - 1)

Run Code Online (Sandbox Code Playgroud)

现在如果我们:

print(fib(100))
Run Code Online (Sandbox Code Playgroud)
573147844013817084101
Run Code Online (Sandbox Code Playgroud)

然而,mypy抱怨"Callable[[int], int]" has no attribute "memory",这是有道理的,如果我尝试访问不属于声明类型的属性,通常我会想要这个抱怨......

那么,我们应该如何使用typing来表示wrapper,而 aCallable也具有属性 呢memory?

jon*_*rpe 3

要将某些内容描述为“具有内存属性的可调用对象”,您可以定义一个协议(Python 3.8+,或带有 的早期版本typing_extensions):

from typing import Protocol


class Wrapper(Protocol):
    memory: dict[int, int]
    def __call__(self, n: int) -> int: ...
Run Code Online (Sandbox Code Playgroud)

在使用中,类型检查器知道 a 作为Wrappera 是有效的,并Callable[[int], int]允许对 进行赋值:return wrapperwrapper.memory

from functools import wraps
from typing import Callable, cast


def remember(f: Callable[[int], int]) -> Callable[[int], int]:
    @wraps(f)
    def _wrapper(n: int) -> int:
        if n not in wrapper.memory:
            wrapper.memory[n] = f(n)
        return wrapper.memory[n]
    wrapper = cast(Wrapper, _wrapper)
    wrapper.memory = dict()
    return wrapper
Run Code Online (Sandbox Code Playgroud)

操场

不幸的是,这需要wrapper = cast(Wrapper, _wrapper),这不是类型安全的 -wrapper = cast(Wrapper, "foo")也可以很好地检查。