Is it possible to use functools.singledispatch with composite/nested/container types in Python?

1.2k Views Asked by At

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?

0

There are 0 best solutions below