Create standalone functions from class methods ("uncurry"?)
Question:
I have a class with many methods, which I’d like to be able to call as ‘standalone’ functions as well.
An example to clarify:
Starting point
A class Person
, and a dictionary of Person
instances. The class methods can be called on each of the instances, as seen in the last two lines.
from typing import Callable
class Person:
def __init__(self, age: int, profession: str):
self._age = age
self._pro = profession
def describe(self) -> str:
"""Describes the person in a sentance.
Parameters
----------
None
Returns
-------
str
"""
return f"This person is {self._age} years old and is a {self._pro}."
def getolder(self, years: int = 1) -> None:
"""Increases the age of the person.
Parameters
----------
years : int, optional
Number of years to increase age by, by default 1
Returns
-------
None
"""
self._age += years
# (and many more)
colleagues = {
"john": Person(39, "accountant"),
"jill": Person(40, "boss"),
"jack": Person(25, "intern"),
}
colleagues["john"].describe() #'This person is 39 years old and is a accountant.'
colleagues["john"].getolder(4)
Goal and current solution
Now, I’d like to abstract the dictionary away, and create functions describe
and getolder
in such a way, that I can call describe('john')
and getolder('john', 4)
instead.
I use the following function, which works:
def uncurry(original: Callable) -> Callable:
def uncurried(name, *args, **kwargs):
return original(colleagues[name], *args, **kwargs)
# Add parameter to docstring.
doc = original.__doc__
search = "Parametersn ----------n"
insert = " name : strn Name of the person.n"
pos = doc.find(search)
if pos == -1:
raise ValueError("Unexpected docstring format.")
pos += len(search)
uncurried.__doc__ = doc[:pos] + insert + doc[pos:]
return uncurried
describe = uncurry(Person.describe)
getolder = uncurry(Person.getolder)
describe("john") #'This person is 43 years old and is a accountant.'
getolder("john", 4)
Issues
The current solution has the following issues, see below.
-
The signatures of the describe
and getolder
functions do not show the parameters.
-
The original docstring is missing the name
parameter, which I add in a rather hacky way, and which also is not perfect (e.g., if the original function has no parameters).
If there is a better solution, I’d love to hear it.
>>> help(describe)
Signature: describe(name, *args, **kwargs)
Docstring:
Describes the person in a sentance.
Parameters
----------
name : str
Name of the person.
None
Returns
-------
str
File: c:usersruud.wijtvlietruudpythondevbelvysbelvys<ipython-input-24-bbc7c1f4a60a>
Type: function
Answers:
If you don’t mind adding a dependency, python-forge
seems to be a nice option.
I’m not aware of a nicer option for the docstring though, as they are just strings. Maybe a regex can make it a little more tidy.
import forge
def uncurry(f):
@forge.compose(
forge.copy(f, exclude=['self']),
forge.insert(forge.arg('name', type=str), index=0)
)
def wrapper(name, *args, **kwargs):
return forge.callwith(f, {'self': colleagues[name], **kwargs}, args)
wrapper.__name__ = f.__name__
wrapper.__qualname__ = f.__name__ # Not `f.__qualname__`, that would keep the `Person` bit
(wrapper.__doc__, subs) = re.subn(
r'(Parameterss+-+)(s*None)?',
(r'g<1>n'
r' name : strn'
r' Name of the person.n'),
f.__doc__,
count=1
)
if subs != 1:
raise ValueError("Unexpected docstring format.")
return wrapper
This gives
assert forge.repr_callable(describe) == 'describe(name: str) -> str'
assert forge.repr_callable(getolder) == 'getolder(name: str, years: int = 1) -> None'
The docstrings are as expected as well.
Without dependency
It is also possible to do this without adding python-forge
, but it is not nearly as neat
def uncurry(f):
def wrapper(name, *args, **kwargs):
return f(colleagues[name], *args, **kwargs)
signature = inspect.signature(f)
name_param = inspect.Parameter('name', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str)
other_params = [p for p in signature.parameters.values() if p.name != 'self']
wrapper.__signature__ = signature.replace(parameters=[name_param] + other_params)
wrapper.__annotations__ = {k: v for k, v in f.__annotations__.items() if k != 'self'}
wrapper.__annotations__['name'] = str
wrapper.__name__ = f.__name__
wrapper.__qualname__ = f.__name__ # Not `f.__qualname__`, that would keep the `Person` bit
(wrapper.__doc__, subs) = re.subn(
r'(Parameterss+-+)(s*None)?',
(r'g<1>n'
r' name : strn'
r' Name of the person.n'),
f.__doc__,
count=1
)
if subs != 1:
raise ValueError("Unexpected docstring format.")
return wrapper
I have a class with many methods, which I’d like to be able to call as ‘standalone’ functions as well.
An example to clarify:
Starting point
A class Person
, and a dictionary of Person
instances. The class methods can be called on each of the instances, as seen in the last two lines.
from typing import Callable
class Person:
def __init__(self, age: int, profession: str):
self._age = age
self._pro = profession
def describe(self) -> str:
"""Describes the person in a sentance.
Parameters
----------
None
Returns
-------
str
"""
return f"This person is {self._age} years old and is a {self._pro}."
def getolder(self, years: int = 1) -> None:
"""Increases the age of the person.
Parameters
----------
years : int, optional
Number of years to increase age by, by default 1
Returns
-------
None
"""
self._age += years
# (and many more)
colleagues = {
"john": Person(39, "accountant"),
"jill": Person(40, "boss"),
"jack": Person(25, "intern"),
}
colleagues["john"].describe() #'This person is 39 years old and is a accountant.'
colleagues["john"].getolder(4)
Goal and current solution
Now, I’d like to abstract the dictionary away, and create functions describe
and getolder
in such a way, that I can call describe('john')
and getolder('john', 4)
instead.
I use the following function, which works:
def uncurry(original: Callable) -> Callable:
def uncurried(name, *args, **kwargs):
return original(colleagues[name], *args, **kwargs)
# Add parameter to docstring.
doc = original.__doc__
search = "Parametersn ----------n"
insert = " name : strn Name of the person.n"
pos = doc.find(search)
if pos == -1:
raise ValueError("Unexpected docstring format.")
pos += len(search)
uncurried.__doc__ = doc[:pos] + insert + doc[pos:]
return uncurried
describe = uncurry(Person.describe)
getolder = uncurry(Person.getolder)
describe("john") #'This person is 43 years old and is a accountant.'
getolder("john", 4)
Issues
The current solution has the following issues, see below.
-
The signatures of the
describe
andgetolder
functions do not show the parameters. -
The original docstring is missing the
name
parameter, which I add in a rather hacky way, and which also is not perfect (e.g., if the original function has no parameters).
If there is a better solution, I’d love to hear it.
>>> help(describe)
Signature: describe(name, *args, **kwargs)
Docstring:
Describes the person in a sentance.
Parameters
----------
name : str
Name of the person.
None
Returns
-------
str
File: c:usersruud.wijtvlietruudpythondevbelvysbelvys<ipython-input-24-bbc7c1f4a60a>
Type: function
If you don’t mind adding a dependency, python-forge
seems to be a nice option.
I’m not aware of a nicer option for the docstring though, as they are just strings. Maybe a regex can make it a little more tidy.
import forge
def uncurry(f):
@forge.compose(
forge.copy(f, exclude=['self']),
forge.insert(forge.arg('name', type=str), index=0)
)
def wrapper(name, *args, **kwargs):
return forge.callwith(f, {'self': colleagues[name], **kwargs}, args)
wrapper.__name__ = f.__name__
wrapper.__qualname__ = f.__name__ # Not `f.__qualname__`, that would keep the `Person` bit
(wrapper.__doc__, subs) = re.subn(
r'(Parameterss+-+)(s*None)?',
(r'g<1>n'
r' name : strn'
r' Name of the person.n'),
f.__doc__,
count=1
)
if subs != 1:
raise ValueError("Unexpected docstring format.")
return wrapper
This gives
assert forge.repr_callable(describe) == 'describe(name: str) -> str'
assert forge.repr_callable(getolder) == 'getolder(name: str, years: int = 1) -> None'
The docstrings are as expected as well.
Without dependency
It is also possible to do this without adding python-forge
, but it is not nearly as neat
def uncurry(f):
def wrapper(name, *args, **kwargs):
return f(colleagues[name], *args, **kwargs)
signature = inspect.signature(f)
name_param = inspect.Parameter('name', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=str)
other_params = [p for p in signature.parameters.values() if p.name != 'self']
wrapper.__signature__ = signature.replace(parameters=[name_param] + other_params)
wrapper.__annotations__ = {k: v for k, v in f.__annotations__.items() if k != 'self'}
wrapper.__annotations__['name'] = str
wrapper.__name__ = f.__name__
wrapper.__qualname__ = f.__name__ # Not `f.__qualname__`, that would keep the `Person` bit
(wrapper.__doc__, subs) = re.subn(
r'(Parameterss+-+)(s*None)?',
(r'g<1>n'
r' name : strn'
r' Name of the person.n'),
f.__doc__,
count=1
)
if subs != 1:
raise ValueError("Unexpected docstring format.")
return wrapper