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.singledispatchmethoddispatches methods based on the first non-selfargument, 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 ordyn-like dispatch. Anabstractmethodin 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.registercan 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
ABCDispatchcan dynamically forward the call to the concrete implementation based on the real type, and the implementations can be externally provided. For example, anABCDispatchcan 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 @implIf 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.
@dispatcheddef 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
| Name | Description |
|---|---|
ABCDispatch | An ABC that enables single dispatch for its abstract methods. |
ABCDispatchMeta | No description. |
ABCDispatchProxy | A proxy for selecting implementations of an ABCDispatch. |
DispatchFunction | No description. |
DispatchRegistry | Default dispatch registry using single dispatch. |
DispatchRegistryForClassMethod | No description. |
DispatchRegistryForStaticMethod | No description. |
IABCDispatch | A protocol for ABCs that supports dispatching regardless of dispatch flavor. |
IDispatchRegistry | A protocol for dispatch registry. |
Functions
| Name | Description |
|---|---|
add_impl | An imperative interface for adding implementations to a dispatchable interface. |
dispatch | No description. |
dispatched | A decorator for creating a single-dispatchable function. |
impl | Decorator for registering an implementation using the class syntax. |
ABCDispatchclass#
class ABCDispatchAn 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 abstractmethodfrom 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)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] = interfaceDispatchFunctionclass#
class DispatchFunction__dispatch__property#
__dispatch__: DispatchRegistry[P, [type], 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_ASSIGNMENTSWhat 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_classThe 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 unimplementedThe 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) -> Noneadd_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
DispatchRegistryForStaticMethodclass#
class DispatchRegistryForStaticMethod(DispatchRegistryForClassMethod)IABCDispatchclass#
class IABCDispatch(Protocol)Bases: Protocol
A protocol for ABCs that supports dispatching regardless of dispatch flavor.
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) -> Nonedef add_impl(self, func: Callable[P, R], *args: K.args, **kwargs: K.kwargs)Add a new implementation to the registry.
Parameters
| Name | Type | Description |
|---|---|---|
func | Callable[P, R] | The implementation to be added. |
... | K | The 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 KeyErrorGet an implementation from the registry.
Parameters
| Name | Type | Description |
|---|---|---|
... | K | The 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) -> NoneAn 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 fParameters
| Name | Type | Default | Description |
|---|---|---|---|
interface | IABCDispatch | required | The dispatchable interface. |
implementation | Mapping[str, Callable] | required | A mapping from method names to implementations. |
*impl_for_args | Any | () | Arguments that the dispatch mechanism will use for selecting the implementation. |
**impl_for_kwargs | Any | {} | 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.
@dispatcheddef 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
| Name | Type | Description |
|---|---|---|
interface | IABCDispatch | The dispatchable interface. |
