4

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?

jrbergen
  • 660
  • 5
  • 16
  • No, because fundamentally `list[int]` is not a "real," type. – juanpa.arrivillaga Jul 14 '21 at 15:53
  • Could you expand on that? Why would that prevent such an implementation for `singledispatch`, compared to how static typecheckers infer composite types? `singledispatch` seems to make use of the annotations, so why couldn't it be implemented to infer nested types in a similar fashion? – jrbergen Jul 14 '21 at 15:55
  • 1
    Because statics type checkers use static analysis, which is absolutely not what the single dispatch decorator is doing. It is essentially using a runtime `isinstance` check. Python is fundamentally a dynamically typed language, and container types are never restricted at runtime – juanpa.arrivillaga Jul 14 '21 at 15:57
  • Ah right, thanks. I guess an implementation is possible using something like the second example in the question internally, but this could result in significant overhead for large containers and it may also not be clear immediately that it only works if all container elements are of the same type. – jrbergen Jul 14 '21 at 16:01
  • yes, I highly suggest *never* doing anything like that and instead just use `mypy` – juanpa.arrivillaga Jul 14 '21 at 16:53
  • @juanpa.arrivillaga I don't understand how `mypy` is relevant here. I'm not using it for static type analysis. I'd like dispatch to be compatible w/ a subset of container instances. E.g. only homogeneous containers with max 1 level of nesting. Whether that is a bad idea / reduces maintainability/readability is a separate point though and I suppose context-dependent. – jrbergen Jul 14 '21 at 18:05
  • 1
    No, I'm saying you shouldn't add runtime checks that verify a list is of homogenous type, i.e. `all(isinstance(x, str) for x in mylist)`, this adds absurd linear time check for simply passing a list this function. Instead, you *should use mypy in lieu of this*. And note, I don't think pattern matching would help you here. – juanpa.arrivillaga Jul 14 '21 at 18:39
  • Ah I misunderstood. Agreed. Thanks again. For the use case I had in mind the performance hit would be negligible though. – jrbergen Jul 14 '21 at 18:52

0 Answers0