Skip to content

Instantly share code, notes, and snippets.

@tmr232
Created March 13, 2022 21:08
Show Gist options
  • Save tmr232/c7214d62607ba1fc0e0a2e82d7e041cd to your computer and use it in GitHub Desktop.
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 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