Topic Introduction
Has anyone ever considered adding protocols to Python that fit the more functional style? For example, we already have the built-in len
which simply calls the __len__
magic method on implementers of the “Sized” protocol. Let’s say I want to create a new protocol, “Renderable”, for a UI components. Currently I have two options:
Structural Protocols (PEP 544)
I could create a Protocol
from PEP 544 that requires all classes to have a render
method. This works, but has a couple downsides
- If I have a component that implements multiple protocols, then the names of the members must be distinct
- I must call the object method
inst.render(...)
rather thanrender(inst, ...)
. This creates a strange asymmetry between built-in protocols that are based on magic methods, and user-created protocols. - There cannot be a default implementation of the method
Functools Singledispatch
I could instead create a render
function using functools.singledispatch
. This addresses my concerns from above, but has its own problems.
- The implementation of the function lives outside of the class body, but is often dependent on the class implementation. For example, you might want to reference “private” variables prefaced with an underscore. Doing this outside of the class body is considered bad practice. Overall, we have coupling between the class and function implementation
- There is no way to type-hint that the argument to some other function implements the protocol in the same way that PEP 544 provides.
- If we want to ensure at runtime that a class implements a protocol with multiple members, we must check that an implementation exists for every member.
Proposal
I believe there should be a version of protocols that take the best of both approaches.
A protocol is a function or collection of functions that a class must implement corresponding methods for.
@generic
def render(self):
# default implementation
Renderable = Protocol(render)
class UIComponent:
@render.implementation
def _render(self):
# class-specific implementation
render(UIComponent())
isinstance(UIComponent(), Renderable) # True
Here the Renderable
protocol has only one member, but we could have many members, and the idea is that protocols could be combined.
This addresses my concerns from above:
- We do not rely on the name of the methods like magic methods or PEP 544 protocols, instead we use decorators.
- These protocols mirror built-in ones in that we can call using a functional approach.
- We can have a default implementation easily and transparently.
- The implementation lives directly within the class body, and is explicit in which protocol it is implementing.
- We should be able to easily type hint and perform runtime type checks on the protocol
- I suspect that static type checkers like MyPy might have some issues handling this type of thing out of the box, but I am unfamiliar with how they are implemented.