Skip to content

apfel.core.dispatch

Dynamic dispatch facilities for Python.

This addresses the lack of runtime-available, inheritance-free interfaces in Python. It partially resembles the extension and trait.

Rationale

Why not functools.singledispatch?

  • Runtime behavior based on type hints can be confusing.
  • Its method variant functools.singledispatchmethod dispatches methods based on the first non-self argument, which is very different from the single dispatch found in other object-oriented programming languages.

Why not abc.ABC?

  • It does not enable virtual-like or dyn-like dispatch. An abstractmethod in an abstract class will not forward the call to the concrete implementation based on the real type. i.e., there is no facilities for implementation selection based on concrete types.
  • Although ABCMeta.register can be used to register non-child classes, it cannot inject implementations to the registered classes. Some classes do not allow monkey-patching and the implementations must be stored elsewhere, registering them to the ABC is logical error, as the implementations are not part of the concrete class.

Using ABCDispatch defined in this module,

  • All ABC features work out of the box. Supports static type checking (inheritance-based usages only), non-inheritance based runtime type checking, and @abstractmethod.
  • Abstract methods defined in an ABCDispatch can dynamically forward the call to the concrete implementation based on the real type, and the implementations can be externally provided. For example, an ABCDispatch can be used to extend built-in classes.

Usage

First, define a class that subclasses ABCDispatch. This class will be semantically similar to Rust traits. All interface functions defined in this method shall be decorated with @abc.abstractmethod. Class methods and static methods can also be defined with the same decorator.

class Interface(ABCDispatch):
@abstractmethod
def method(self, f): ...
@classmethod
@abstractmethod
def class_method(cls, f): ...
@staticmethod
@abstractmethod
def static_method(f): ...

Then, we can implement the “interface” for a class by using the @impl decorator. The implementor class is passed as the first argument to the decorator. The implementation is written in the class syntax, with each interface method implemented as a method of the class. The class name doesn’t matter, but it should subclass the implementor class. This syntax provides you the ability to reference concrete attributes and methods defined on the implementor class.

@impl(Interface)
class _(Implementor):
def method(self, f): """Concrete implementation goes here"""
@classmethod
def class_method(cls, f): """Concrete implementation goes here"""
@staticmethod
def static_method(f): """Concrete implementation goes here"""

In this case, Implementor.method does not exist during runtime so you may not call it, and calling .method on an instance of Implementor will also fail. But you can call Interface.method on an instance of Implementor and it will dispatch to the concrete implementation defined in the @impl construct.

c = Implementor()
Interface.method(c, f) # Calls the implementation within the @impl

If you have control to the source code, you can add Interface to the base classes of Implementor, so Implementor.method(instance) and instance.method() will work as expected, Interface.method(instance, f) shall also work.

Class methods and static methods shall select a concrete implementation explicitly, as the concrete class is not inferrable from the function’s call arguments.

Interface.dispatch(Implementor).class_method(f)
Interface.dispatch(Implementor).static_method(f)

The older generic syntax, e.g. Interface.class_method[Implementor](f), is also supported.

It’s also possible to define a dispatchable function using the @dispatched decorator. Then a method impl_for will be on the function, which can be used to register implementations.

@dispatched
def f(x): ...
@f.impl_for(int)
def _(x: int):
"""Implementation for int goes here"""

By default ABCDispatch and @dispatched defines a single dispatch. This means the runtime implementation selection is based on the concrete type of first argument of the function, or of the receiver in the case of methods.

API#

Classes

NameDescription
ABCDispatchAn ABC that enables single dispatch for its abstract methods.
ABCDispatchMetaNo description.
ABCDispatchProxyA proxy for selecting implementations of an ABCDispatch.
DispatchFunctionNo description.
DispatchRegistryDefault dispatch registry using single dispatch.
DispatchRegistryForClassMethodNo description.
DispatchRegistryForStaticMethodNo description.
IABCDispatchA protocol for ABCs that supports dispatching regardless of dispatch flavor.
IDispatchRegistryA protocol for dispatch registry.

Functions

NameDescription
add_implAn imperative interface for adding implementations to a dispatchable interface.
dispatchNo description.
dispatchedA decorator for creating a single-dispatchable function.
implDecorator for registering an implementation using the class syntax.

ABCDispatchclass#

class ABCDispatch

An ABC that enables single dispatch for its abstract methods. This behaves similarly to abc.ABC but with the added feature of dynamic concrete implementation selection under single dispatch. Stick to built-in abc.abstractmethod to define abstract methods.

See the usage section for more information.

from abc import abstractmethod
from apfel.core.dispatch import ABCDispatch
class A(ABCDispatch):
@abstractmethod
def hi(self):
print(self)
@classmethod
@abstractmethod
def hey(cls):
print(cls)
a: A = some_instance()
A.hi(a)

__dispatch_methods__attributeclass attribute#

__dispatch_methods__: set[str]

dispatchmethodclassmethod#

def dispatch(implementor: type[T], /) -> Self

ABCDispatchMetaclass#

class ABCDispatchMeta(ABCMeta)

Bases: ABCMeta

__new__method#

def __new__(name, bases, namespace, **kwargs)

ABCDispatchProxyclass#

class ABCDispatchProxy(interface: type[I], implementor: type[T], /)

A proxy for selecting implementations of an ABCDispatch.

implementorattributeinstance attribute#

implementor: type[T] = implementor if isinstance(implementor, type) else type(implementor)

interfaceattributeinstance attribute#

interface: type[I] = interface

__getattr__method#

def __getattr__(name: str) -> Any

__init__method#

def __init__(interface: type[I], implementor: type[T], /) -> None

DispatchFunctionclass#

class DispatchFunction

__dispatch__property#

__dispatch__: DispatchRegistry[P, [type], R]

__call__method#

def __call__(*args: P.args, **kwargs: P.kwargs) -> R

impl_formethod#

def impl_for(cls: type) -> Callable[[Callable[P, R]], Callable[P, R]]

DispatchRegistryclass#

class DispatchRegistry(func: Callable[P, R] | None = None, *, enclosing_class: type | None = None, decorate_assignments: Sequence[str] | None = None)

Bases: IDispatchRegistry

Default dispatch registry using single dispatch.

decorate_assignmentsattributeinstance attribute#

decorate_assignments: Sequence[str] = decorate_assignments or WRAPPER_ASSIGNMENTS

What attributes to copy from the original function to the decorated function when calling the make_dispatch_func method.

enclosing_classattributeinstance attribute#

enclosing_class: type | None = enclosing_class

The class that the dispatched function is a method of. If it is not a method, this should be None.

functionattributeinstance attribute#

function: Callable[P, R] = func or unimplemented

The fallback function or method.

registryattributeinstance attribute#

registry: dict[type, Callable[P, R]] = {}

A mapping from types to functions or methods.

__init__method#

def __init__(func: Callable[P, R] | None = None, *, enclosing_class: type | None = None, decorate_assignments: Sequence[str] | None = None) -> None

add_implmethod#

def add_impl(func, *args, **kwargs)

decide_implmethod#

def decide_impl(*args, **kwargs)

get_implmethod#

def get_impl(*args, **kwargs)

make_dispatch_funcmethod#

def make_dispatch_func(func)

DispatchRegistryForClassMethodclass#

class DispatchRegistryForClassMethod(DispatchRegistry)

Bases: DispatchRegistry

add_implmethod#

def add_impl(func, *args, **kwargs)

decide_implmethod#

def decide_impl(*args, **kwargs)

make_dispatch_funcmethod#

def make_dispatch_func(func)

DispatchRegistryForStaticMethodclass#

class DispatchRegistryForStaticMethod(DispatchRegistryForClassMethod)

Bases: DispatchRegistryForClassMethod

add_implmethod#

def add_impl(func, *args, **kwargs)

make_dispatch_funcmethod#

def make_dispatch_func(func)

IABCDispatchclass#

class IABCDispatch(Protocol)

Bases: Protocol

A protocol for ABCs that supports dispatching regardless of dispatch flavor.

__dispatch_methods__attributeclass attribute#

__dispatch_methods__: set[str]

IDispatchRegistryclass#

class IDispatchRegistry(Protocol)

Bases: Protocol

A protocol for dispatch registry. A dispatch registry is a collection of implementations of a polymorphic function or method.

add_implmethod#

def add_impl(func: Callable[P, R], *args: K.args, **kwargs: K.kwargs) -> None
def add_impl(self, func: Callable[P, R], *args: K.args, **kwargs: K.kwargs)

Add a new implementation to the registry.

Parameters

NameTypeDescription
funcCallable[P, R]The implementation to be added.
...KThe implementor of the implementation added.

decide_implmethod#

def decide_impl(*args: P.args, **kwargs: P.kwargs) -> Callable[P, R]
def decide_impl(self, *args: P.args, **kwargs: R.args) -> Callable[P, R]

Decide which implementation should be used based on the arguments.

This method should not call the implementation directly, but return a reference to it.

Returns

CallableCallable[P, R]
The implementation to be used.

get_implmethod#

def get_impl(*args: K.args, **kwargs: K.kwargs) -> Callable[P, R]
def get_impl(self, *args: K.args, **kwargs: K.kwargs) -> Callable raise KeyError

Get an implementation from the registry.

Parameters

NameTypeDescription
...KThe implementor of the implementation.

Returns

Callable[P, R]
Callable[Self.P, Self.R]: The implementation.

Raises

KeyError
If no such implementation is found.

make_dispatch_funcmethod#

def make_dispatch_func(func: Callable[P, R]) -> Callable[P, R]
def make_dispatch_func(self, func: Callable[P, R]) -> Callable[P, R]

Make a function or callable that uses this registry to perform dynamic dispatch.

add_implfunction#

def add_impl(interface: IABCDispatch, implementation: Mapping[str, Callable], *impl_for_args, **impl_for_kwargs) -> None

An imperative interface for adding implementations to a dispatchable interface. For a declarative interface, use the @impl decorator.

If the dispatchable interface is an ABCDispatch which does single dispatch, the impl_for_args param should be the type of the implementor class.

# The following imperative code ...
add_impl(Interface, {"method": lambda self, f: f}, Implementor)
# ... is equivalent to the following declarative code:
@impl(Interface)
class _(Implementor):
def method(self, f): return f

Parameters

NameTypeDefaultDescription
interfaceIABCDispatchrequiredThe dispatchable interface.
implementationMapping[str, Callable]requiredA mapping from method names to implementations.
*impl_for_argsAny()Arguments that the dispatch mechanism will use for selecting the implementation.
**impl_for_kwargsAny{}Keyword arguments that the dispatch mechanism will use for selecting the implementation.

dispatchfunction#

def dispatch(func: Callable[P, R], /) -> DispatchFunction[P, R]

dispatchedfunction#

def dispatched(func: Callable[P, R], /) -> DispatchFunction[P, R]

A decorator for creating a single-dispatchable function. It will add an impl_for method to the function, which can be used to register implementations. Calling the function will dispatch to the correct implementation based on the type of the first argument.

This is similar to functools.singledispatch, but does not use type hints for dispatching, and static type unions are not supported.

@dispatched
def show(x):
...
@show.impl_for(int)
def _(x: int):
return f"int: {x}"
@show.impl_for(str)
def _(x: str):
return f"str: {x}"
show(1) # "int: 1"
show("hello") # "str: hello"

implfunction#

def impl(interface: IABCDispatch, /) -> Callable[[type[T]], type[T]]

Decorator for registering an implementation using the class syntax.

See the usage section for more information.

Here Implementor is a concrete class and Interface is an ABCDispatch.

@impl(Interface)
class _(Implementor):
def method(self, f): """Concrete implementation goes here"""
@classmethod
def class_method(cls, f): """Concrete implementation goes here"""
@staticmethod
def static_method(f): """Concrete implementation goes here"""

Parameters

NameTypeDescription
interfaceIABCDispatchThe dispatchable interface.