Skip to content

Instantly share code, notes, and snippets.

@ryxcommar
Last active August 22, 2020 20:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryxcommar/a1a617bd9018e4f216a861cc56a68db9 to your computer and use it in GitHub Desktop.
Save ryxcommar/a1a617bd9018e4f216a861cc56a68db9 to your computer and use it in GitHub Desktop.
Make classes look a little bit more like the functions they're wrapping.
"""This module makes a class look like a function. The function is stored in
``self._func,`` and (most of) the function's properties are exposed in the
class's top-level. The biggest issue with the mixin is that ``__call__`` hides
the signature of the original function; I'm not sure how to address that.
"""
def _create_getter_and_setter(name: str):
def getter(self):
return getattr(self._func, name)
getter.__name__ = name
prop = property(getter)
def setter(self, value):
setattr(self._func, name, value)
setter.__name__ = name
prop = prop.setter(setter)
return prop
class FunctionMixin:
"""Mixin for making classes look like functions.
This class isn't too fancy: if you store random non-standard attributes
inside your function then they are not directly accessible at the top-level
of the the subclass. The attributes this mixin provides are pre-defined.
"""
def __init__(self, func: callable):
self._func = func
for name in [
'__annotations__',
'__closure__',
'__code__',
'__defaults__',
'__kwdefaults__',
'__name__'
]:
locals()[name] = _create_getter_and_setter(name)
@property
def __funcdoc__(self):
return self._func.__doc__
@__funcdoc__.setter
def __funcdoc__(self, value):
self._func.__doc__ = value
def __call__(self, *args, **kwargs):
return self._func(*args, **kwargs)
if __name__ == '__main__':
# Demo!
class MyClass(FunctionMixin):
"""This is a class docstring."""
pass
def multiply_by_two(x: int, *, add_this: int = 0):
"""This is a function docstring."""
return x * 2 + add_this
wrapper = MyClass(multiply_by_two)
assert wrapper.__name__ == 'multiply_by_two'
assert wrapper._func.__name__ == 'multiply_by_two'
assert wrapper.__annotations__ == {'x': int, 'add_this': int}
assert wrapper.__kwdefaults__ == {'add_this': 0}
wrapper.__name__ = 'new_name'
assert wrapper.__name__ == 'new_name'
assert wrapper._func.__name__ == 'new_name'
# So here's the deal with __doc__. First of all, there's no way to preserve
# __doc__ within a mixin after it's subclassed. Second of all, even if we
# could preserve it, it's unclear whether this would be good "default"
# behavior. It's nice for factory functions where classes are generated
# dynamically, but bad elsewhere. For that reason, I create a new property
# called __funcdoc__:
assert wrapper.__doc__ == 'This is a class docstring.'
assert wrapper.__funcdoc__ == 'This is a function docstring.'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment