@singledispatch prefers exact match over registration order

Here’s a minimal example:

from abc import ABC, ABCMeta

class LiteralType(ABCMeta):

	_value: object

	def __instancecheck__(self, instance: object) -> bool:
		return isinstance(instance, type(self._value)) and instance == self._value

class Literal1(ABC, metaclass = LiteralType):
	_value = 1
from functools import singledispatch

@singledispatch
def f(_: object):
	print('Default')

@f.register
def _(_: Literal1):
	print('Literal 1')

@f.register
def _(_: int):
	print('Int')
f(1)  # Always prints 'Int' regardless of the registration order.

print(isinstance(1, Literal1))  # True

The source code makes it quite clear that @singledispatch prefers exact matches:

# functools.py, line 831
try:
	impl = dispatch_cache[cls]
except KeyError:
	try:
		impl = registry[cls]
	except KeyError:
		impl = _find_impl(cls, registry)
	dispatch_cache[cls] = impl
return impl

To quote PEP 443:

Where there is no registered implementation for a specific type, its method resolution order is used to find a more generic implementation.

So… this is probably expected. I feel like it should honor my registration order, however. Should a new keyword argument be introduced to allow changing the behaviour?

I presume that by “registration order” you mean the order of the @f.register calls in the code or rather the order in which the calls are executed at runtime. If that is the case then I think that it is important that the dispatch mechanism be defined in such a way that the order is deliberately not allowed to have any effect.

If the @f.register calls are in different modules then the order in which the statements are executed is determined by the order in which the modules are imported. Adding a call to import foo in an unrelated module might affect that import order which could then cause single dispatch functions in far away code to return different results.

4 Likes

I think that this problem should be resolved in concert with formalizing typing’s resolution of @overload. That way, there will be a clear mapping between writing a function using one definition and multiple overloads versus writing the same function using multiple dispatch.

But in that case wouldn’t ordering have to matter for more complex cases?

I don’t see why ordering needs to matter for single/multiple dispatch but I presume that you say this because it does matter for @overload. I will just reiterate that the semantics of @overload are not a good fit for runtime dispatch and I think that trying to merge those two things is a bad idea. In principle it could have been possible to define @overload in such a way that it was compatible with runtime dispatch but it was deliberately not designed that way as noted in PEP 484 that introduced it:

In the future we may come up with a satisfactory multiple dispatch design, but we don’t want such a design to be constrained by the overloading syntax defined for type hints in stub files. It is also possible that both features will develop independent from each other (since overloading in the type checker has different use cases and requirements than multiple dispatch at runtime – e.g. the latter is unlikely to support generic types).

2 Likes