Last active
July 17, 2025 01:07
-
-
Save josiahcarlson/39ed816e80108093d585df94f2dbc8b7 to your computer and use it in GitHub Desktop.
No need for typing any more __dunder__ method names, solving 80-95% of hand-wringing: https://news.ycombinator.com/item?id=44579717
This file contains hidden or 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
""" | |
Copyright 2025 Dr. Josiah Carlson, Phd <[email protected]> | |
Released under the "don't be a jerk" license - which requires that you | |
include this entire header, and that you not be a jerk. Can't do that? | |
You don't have a license. Want to be super cool? Buy me a pizza: | |
https://josiahcarlson.github.io/ | |
Why was this created? There are a lot of complainers about Python's | |
syntax for magic methods. This module allows you to not need to use __add__ | |
to override the + operation, you can instead use:: | |
class MyClass(metaclass=ReDunder): | |
def add(self, other): | |
# your implementation here | |
And your add method (as well as any other matching methods / entries in the | |
class namespace) will be auto-renamed to having double-underscore prefixed | |
and suffixed names, aka dunder names. | |
If you find that we have missed a name, feel free to call _scan_namespace(obj) | |
to discover any other relevant names on the type with existing dunder names. | |
If you wish to limit renames:: | |
class MyClass(metaclass=ReDunder): | |
re_dunder = {"add"} | |
# class MyClass(metaclass=ReDunder, re_dunder={"add"}): # alternative spellling to ^^^ | |
def add(self, other): | |
# your implementation here | |
# method will be renamed to __add__ | |
def sub(self): | |
# don't rename this one! | |
# method will remain sub. | |
Warning: you probably shouldn't use this in production code. For real. Stop. | |
""" | |
import types | |
import typing | |
# pulled from https://docs.python.org/3/reference/datamodel.html on July 16, 2025 | |
dunder = set([ | |
'__path__', '__name__', '__weakref__', '__qualname__', '__hash__', '__class_getitem__', | |
'__annotations__', '__module__', '__class__', '__aenter__', '__objclass__', '__loader__', | |
'__self__', '__dict__', '__file__', '__anext__', '__release_buffer__', '__traceback__', | |
'__func__', '__cached__', '__spec__', '__prepare__', '__buffer__', '__globals__', | |
'__firstlineno__', '__getattr__', '__classcell__', '__format__', '__package__', | |
'__future__', '__getitem__', '__length_hint__', '__doc__', '__init_subclass__', | |
'__set_name__', '__match_args__', '__code__', '__dir__', '__slots__', '__aexit__' | |
]) | |
de_dunder = {x[2:-2] for x in dunder} | |
ld = len(de_dunder) | |
# clean up namespace | |
del dunder | |
def _filter_name(name: str) -> bool: | |
return name.startswith("__") and name.endswith("__") and len(name) > 4 | |
def _scan_namespace(ns: object) -> None: | |
for it in dir(ns): | |
if _filter_name(it): | |
de_dunder.add(it[2:-2]) | |
for name in dir(getattr(ns, it)): | |
if _filter_name(name): | |
de_dunder.add(name[2:-2]) | |
def _init(ex_objects: typing.Iterable) -> None: | |
any(map(_scan_namespace, ex_objects)) | |
# make sure we get some methods from real objects, not just docs | |
if len(de_dunder) == ld: | |
_init([1, 1.1, {}, [], de_dunder, "", b"", _init, types, typing]) | |
assert len(de_dunder) > ld | |
# clean up namespace | |
del ld | |
class ReDunder(type): | |
""" | |
Setting:: | |
re_dunder = {'add', 'sub'} | |
Or using:: | |
class MyClass(metaclass=ReDunder, re_dunder={'add', 'sub'}) | |
In your class will limit the remapping to only those methods listed. | |
""" | |
def __new__(cls: type, name: str, bases: list, namespace: typing.Mapping, **kwds: dict) -> type: | |
only_these = kwds.get("re_dunder") or namespace.get("re_dunder") or de_dunder | |
only_these = set(only_these) if not isinstance(only_these, set) else only_these | |
# rename <operation> -> __<operation>__ | |
# O(len(kwds)) operation to find + rename everything | |
for it in set(namespace) & only_these: | |
namespace[f"__{it}__"] = namespace.pop(it) | |
return super().__new__(cls, name, bases, namespace) | |
# Ta da! That's it. <100 lines, including docs, and now you don't need to be | |
# shackled to the Tyranny of Python's __dunder__ methods. Just De-dunder your | |
# code and use our ReDunder metaclass. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment