Created
March 13, 2022 21:08
-
-
Save tmr232/c7214d62607ba1fc0e0a2e82d7e041cd to your computer and use it in GitHub Desktop.
This is a proof-of-concept for function overloading in Python based on metaclasses and `__prepare__()`.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This is a proof-of-concept for function overloading in Python | |
based on metaclasses and `__prepare__()`. | |
This code is intended as a fun experiment and an educational resource. | |
I highly recommend avoiding the overload patterns presented here in | |
real projects, as they will be very confusing to readers. | |
Additionally, this code does not handle edge cases and is very likely | |
to fail or behave unexpectedly. | |
""" | |
from functools import singledispatchmethod | |
def _default(*args, **kwargs): | |
"""Raises TypeError() | |
This is the default implementation for all overload sets. | |
Naturally, if the types don't match any overload - we want | |
to raise a TypeError. | |
An alternative implementation can have other defaults.""" | |
raise TypeError() | |
class OverloadingNamespace(dict): | |
"""Class namespace for overloading | |
Here we abuse the fact that metaclasses can provide the class namespace object | |
for class creation. | |
Instead of a normal dict, we make sure we keep declarations in the same name. | |
This allows us to do function overloading. | |
In this case, we use singledispatchmethod for overloading. | |
We can write our own implementation instead, and add more bells and | |
whistles, but this is just a proof-of-concept, so we take the easy solution. | |
Note that this is a _very_ simplistic implementation. It's just s proof of concept. | |
As such, as don't do any real type-checking and don't cover and edge-cases. | |
See https://docs.python.org/3/reference/datamodel.html#preparing-the-class-namespace | |
and https://snarky.ca/unravelling-pythons-classes/ for more info. | |
""" | |
def __setitem__(self, key, value): | |
# If the key already exists, this is a function overload! | |
if key in self: | |
# First, make sure we have a valid "overload set" | |
overload_set = self[key] | |
if not isinstance(overload_set, singledispatchmethod): | |
overload_set = singledispatchmethod(_default) | |
overload_set.register(self[key]) | |
# Then register the new value to it | |
overload_set.register(value) | |
super().__setitem__(key, overload_set) | |
else: | |
super().__setitem__(key, value) | |
class OverloadingMetaclass(type): | |
"""Meteclass for function overloads | |
As mentioned before - we enable function overloading by using a special | |
namespace object during class creation. To do that, we use a metaclass | |
and the __prepare__ method. This is the class that does the trick. | |
""" | |
@classmethod | |
def __prepare__(metacls, name, bases): | |
# Return our custom namespace object | |
return OverloadingNamespace() | |
def __new__(cls, name, bases, classdict): | |
# Convert the custom object to a regular dict, to avoid unwanted shenanigans. | |
return type.__new__(cls, name, bases, dict(classdict)) | |
class WithOverloads(metaclass=OverloadingMetaclass): | |
"""Base class for allowing method overloading | |
Here we createa regular class using our metaclass. | |
This allows us to then use the metaclass without the verbose | |
`metaclass=MetaClass` syntax. | |
Using this class as a baseclass allows any class to have overloaded methods. | |
""" | |
class OverloadSet(metaclass=OverloadingMetaclass): | |
"""Base class for creating overload sets of free functions | |
Here we abuse yet another mechanism. | |
Overriding the `__new__()` method allows us to replace class instantiation | |
with any function call we wish. | |
This means that when a subclass of this class is "instantiated", it will instead | |
call its overloaded `_` method and return the resulting value. | |
See https://docs.python.org/3/reference/datamodel.html#object.__new__ for more info. | |
""" | |
def __new__(cls, *args, **kwargs): | |
return cls._(*args, **kwargs) | |
class Thing(WithOverloads): | |
def f(self, x: int): | |
print(f"{x} is an int") | |
def f(self, x: str): | |
print(f"{x} is a string") | |
def f(self, x: list): | |
print(f"{x} is a list") | |
class f(OverloadSet): | |
def _(x: int): | |
return f"{x} is an int" | |
def _(x: str): | |
return f"{x} is a string" | |
def _(x: list): | |
return f"{x} is a list" | |
def main(): | |
t = Thing() | |
t.f(1) | |
t.f("a") | |
t.f([1]) | |
print(f(5)) | |
print(f(["f"])) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment