Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How should we type a callable with additional properties?

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?

like image 249
Richard Ambler Avatar asked Jan 02 '22 13:01

Richard Ambler


People also ask

What is a callable in Python?

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!

Should you use typing in Python?

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.

What is typing module in Python?

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.


2 Answers

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.

like image 99
jonrsharpe Avatar answered Oct 24 '22 03:10

jonrsharpe


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)
like image 24
chepner Avatar answered Oct 24 '22 02:10

chepner