Como ejemplo de juguete, usemos la secuencia de Fibonacci:
def fib(n: int) -> int: if n < 2: return 1 return fib(n - 2) + fib(n - 1)Por supuesto, esto colgará la computadora si intentamos:
print(fib(100)) Entonces decidimos agregar memorización. Para mantener clara la lógica de fib , decidimos no cambiar fib y en su lugar agregar memoization a través de un decorador:
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)Ahora no hay problema si nosotros:
print(fib(100)) 573147844013817084101 Sin embargo, mypy se queja de que "Callable[[int], int]" has no attribute "memory" , lo cual tiene sentido, y por lo general querría esta queja si intentara acceder a una propiedad que no forma parte del tipo declarado. .
Entonces, ¿cómo deberíamos usar la typing para indicar que wrapper , mientras que Callable , también tiene la propiedad memory ?
Para describir algo como "un invocable con un atributo de memoria" , podría definir un protocolo (Python 3.8+ o versiones anteriores con typing_extensions ):
from typing import Protocol class Wrapper(Protocol): memory: dict[int, int] def __call__(self, n: int) -> int: ... En uso, el verificador de tipos sabe que un Wrapper es válido como Callable[[int], int] y permite return wrapper así como la asignación a wrapper.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 Desafortunadamente, esto requiere wrapper = cast(Wrapper, _wrapper) , que no es de tipo seguro; wrapper = cast(Wrapper, "foo") también funcionaría bien.
No use un atributo de función para almacenar el caché y no tendrá este problema. Ya está definiendo un cierre (el wrapper mantiene una referencia al invocable original), así que almacene el caché en el cierre también.
from typing import Callable from functools import wraps def remember(f: Callable[[int], int]) -> Callable[[int], int]: cache: dict[int, int] = {} @wraps(f) def wrapper(n: int) -> int: if n not in cache: cache[n] = f(n) return cache[n] return wrapper @remember def fib(n: int) -> int: if n < 2: return 1 return fib(n - 2) + fib(n - 1)Partiendo de la respuesta de jonrsharpe (que funciona, sugiere lo siguiente y acepté), podemos evitar la necesidad de una conversión no segura de tipos de la siguiente manera:
from typing import Callable from functools import wraps class Remember: def __init__(self) -> None: self.memory = dict[int, int]() def __call__(self, f: Callable[[int], int]) -> Callable[[int], int]: @wraps(f) def wrapper(n: int) -> int: if n not in self.memory: self.memory[n] = f(n) return self.memory[n] return wrapper @Remember() def fib(n: int) -> int: if n < 2: return 1 return fib(n - 2) + fib(n - 1) print(fib(100))