Edit 2022-08-30: perhaps with the introduction of variadic generics (PEP-646) in Python 3.11, dispatch with composite types may become possible.
I wonder whether the following can be achieved, and if so, without requiring much extra code:
from __future__ import annotations
from functools import singledispatch
@singledispatch
def somefunc(value):
print(f"Type {type(value).__qualname__!r} "
f"is not registered for dispatch.")
@somefunc.register
def _(value: list[int]):
print(f"Dispatched type list[int]!")
@somefunc.register
def _(value: list[str]):
print(f"Dispatched type list[str]!")
somefunc('123')
somefunc([123])
somefunc(list('123'))
And get output:
Type 'str' is not registered for dispatch.
Dispatched type list[int]!
Dispatched type list[str]!
Running this snippet with python 3.9.6, however, instead results in an error at line 742 of functools.py:
TypeError: issubclass() argument 2 cannot be
a parameterized generic
As singledispatch
does work for user defined classes, one way to make this work is to typecheck the elements in the passed list, wrap the passed list into a class representing e.g. list[str]
and have the dispatched function call itself again with the new instance as argument:
from __future__ import annotations
from functools import singledispatch
class ListOfStrs(list):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class ListOfInts(list):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@singledispatch
def somefunc(value):
print(f"Type {type(value).__qualname__!r} "
f"is not registered for dispatch.")
@somefunc.register
def _(value: list):
if value and all(isinstance(subval, int)
for subval in value):
somefunc(ListOfInts(value))
elif value and all(isinstance(subval, str)
for subval in value):
somefunc(ListOfStrs(value))
else:
print(
f"Dispatched a list whose elements ",
f"are not all of a registered type."
)
@somefunc.register
def _(value: ListOfStrs):
print(f"Dispatched type 'list[str]'!")
@somefunc.register
def _(value: ListOfInts):
print(f"Dispatched type 'list[int]'!")
somefunc('123')
somefunc([1, 2, 3])
somefunc(list('123'))
somefunc([{1}, {2}, {3}])
Which, as expected, results in:
Type 'str' is not registered for dispatch.
Dispatched type 'list[int]'!
Dispatched type 'list[str]'!
Dispatched a list whose elements are
not all of a registered type.
However, besides increasing extensibility, one of the reasons to use singledispatch
in the first place is to circumvent verbose typechecks, which some consider an anti-pattern. And of course for this solution specifically, you'd need to define the wrapper classes which litter the code (there may be a better way of achieving this that I currently don't see, but the first point still stands).
One could reason one typecheck for 'list' is avoided here, but that reduces complexity just by just one if/else clause.
So I wouldn't actually use the last case.
Does anyone know how to get this behaviour as elegantly as can be done with non-composite types?
I suppose this could be done elegantly using pattern matching in 3.10. So maybe I should wait for its launch and maturation if this is currently not feasible?