As a toy example, let's use the Fibonacci sequence:
def fib(n: int) -> int:
if n < 2:
return 1
return fib(n - 2) + fib(n - 1)
Of course, this will hang the computer if we try to:
print(fib(100))
So we decide to add memoization. To keep the logic of fib
clear, we decide not to change fib
and instead add memoization via a decorator:
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)
Now there is no problem if we:
print(fib(100))
573147844013817084101
However, mypy
complains that "Callable[[int], int]" has no attribute "memory"
, which makes sense, and usually I would want this complaint if I tried to access a property that is not part of the declared type...
So, how should we use typing
to indicate that wrapper
, while a Callable
, also has the property memory
?
In Python, a callable is a function-like object, meaning it's something that behaves like a function. Just like with a function, you can use parentheses to call a callable. Functions are callables in Python but classes are callables too!
Type hints work best in modern Pythons. Annotations were introduced in Python 3.0, and it's possible to use type comments in Python 2.7. Still, improvements like variable annotations and postponed evaluation of type hints mean that you'll have a better experience doing type checks using Python 3.6 or even Python 3.7.
Introduced since Python 3.5, Python's typing module attempts to provide a way of hinting types to help static type checkers and linters accurately predict errors.
To describe something as "a callable with a memory attribute", you could define a protocol (Python 3.8+, or earlier versions with typing_extensions
):
from typing import Protocol
class Wrapper(Protocol):
memory: dict[int, int]
def __call__(self, n: int) -> int: ...
In use, the type checker knows that a Wrapper
is valid as a Callable[[int], int]
and allows return wrapper
as well as the assignment to 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
Playground
Unfortunately this requires wrapper = cast(Wrapper, _wrapper)
, which is not type safe - wrapper = cast(Wrapper, "foo")
would also check just fine.
Don't use a function attribute to store the cache, and you won't have this problem. You are already defining a closure (wrapper
keeps a reference to the original callable), so store the cache in the closure as well.
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With