Looking at the original issues [1], [2], it sounds as if there was initially an idea to have @typing.overload
enable actual multiple dispatch, before it became informational only. This idea seems to resurface occasionally [3].
So, why isn’t there something like @functools.multidispatch
that uses @typing.overload
to actually do multiple dispatch? Since libraries for multiple dispatch exist, the actual dispatching is presumably not an insurmountable problem. And with @typing.overload
there is already a standard mechanism to describe the individual specialisations.
To illustrate that point, now that Python 3.11 has added the ability to return the overloads of a given function, the following is entirely possible with a bit of DIY:
from typing import Any, overload
@overload
def concat(a: list, b: list) -> list:
return a + b
@overload
def concat(a: list, b: object) -> list:
return a + [b]
@overload
def concat(a: object, b: list) -> list:
return [a] + b
# decorator that does multiple dispatch with a stub function
@multipledispatch
def concat(a: Any, b: Any) -> Any:
...
print(concat([1], [2])) # [1, 2]
print(concat([1], 2)) # [1, 2]
print(concat(1, [2])) # [1, 2]
print(concat(1, 2)) # TypeError: No overload variant ...
As you can see, the idea is roughly that the overloads have all necessary type information for multiple dispatch anyway, so one only has to add the specific implementations. While this turns the usual pattern of only having stubs for @typing.overload
exactly on its head, it does yield somewhat type-checked multiple dispatch right now at no extra cost.[1]
Toy implementation
Below is a very naive implementation of the dispatcher, just to run the example. It would probably be better to reuse the @singledispatch
mechanism.
from functools import wraps
from inspect import signature
from typing import Any, Callable, TypeVar, cast, get_overloads, get_type_hints
F = TypeVar('F', bound=Callable[..., Any])
def _dispatch(classes, registry):
match = next((types for types in registry.keys()
if all(issubclass(c, t) for c, t in zip(classes, types))),
None)
return registry.get(match)
def multipledispatch(func: F) -> F:
registry = {}
sig = signature(func)
par = list(sig.parameters)
for overload in get_overloads(func):
ann = get_type_hints(overload)
types = tuple(map(ann.get, par))
registry[types] = overload
@wraps(func)
def wrapper(*args, **kwargs):
ba = sig.bind(*args, **kwargs)
ba.apply_defaults()
classes = tuple(ba.arguments[name].__class__ for name in par)
overload = _dispatch(classes, registry)
if not overload:
given = '", "'.join(cls.__name__ for cls in classes)
raise TypeError(f'No overload variant of "{func.__name__}" '
f'matches argument types "{given}"')
return overload(*args, **kwargs)
return cast(F, wrapper)
-
Somewhat, because there is probably no good way to implement support for generic types. ↩︎