Reputation: 147
I am looking to convert a lot of boilerplate code into using decorators, but I am getting stuck figuring out how to do it.
My current code looks something like this:
import time # for demonstration
class C(object):
def large_function(self, optional_param=[]):
"""Large remote query that takes some time"""
time.sleep(3)
# usually run without optional_param
return ['val'] + optional_param
@property
def shorthand(self):
"""Docstr..."""
if not hasattr(self, "_shorthand"):
setattr(self, "_shorthand", self.large_function())
return self._shorthand
This works like I want it to, but obviously is annoying to write many of these. A shorter example that seems to do the same:
import time # for demonstration
def lazy(self, name, func):
attr_name = "_" + name
if not hasattr(self, attr_name):
setattr(self, attr_name, func())
return getattr(self, attr_name)
class C(object):
def large_function(self, optional_param=[]):
"""Large remote query that takes some time"""
time.sleep(3)
# usually run without optional_param
return ['val'] + optional_param
@property
def shorthand(self):
"""Docstr..."""
return lazy(self, 'shorthand', self.large_function)
However, this still seems very verbose. Optimally, I'd like to have:
class C(object):
@add_lazy_property('shorthand')
def large_function(self, optional_param=[]):
"""Large remote query that takes some time"""
time.sleep(3)
# usually run without optional_param
return ['val'] + optional_param
c = C()
print(c.shorthand)
print(c.large_function(['add_param'])
Unfortunately, the lazy decorators I've found completely mask the underlying function. I tried several setattr()
for functions, but it quickly got confusing. Any way to do this with one decorator 'add_lazy_property'? Bonus if I can add my own docstring or at least copy the function's docstring.
Upvotes: 2
Views: 1829
Reputation: 147
I made a modification to this answer by Graipher to allow calling with both renaming the stored _value and customizing the function call (so you don't have to lambda-wrap it).
from collections import Callable
def lazy_property(method_or_name=None, *args, **kwargs):
"""Defines a lazy named property.
If method_or_name is Callable, immediately wraps it.
Otherwise, returns a wrapper with a custom name.
*args and **kwargs are passed onto the wrapped function."""
name = method_or_name
is_callable = isinstance(name, Callable) # Check if property is callable
def method_decorator(method): # Actual work
if not is_callable: internal_name = ("_%s" % name)
else: internal_name = "_" + method.__name__
def wrapper(self):
if not hasattr(self, internal_name):
setattr(self, internal_name, method(self, *args, **kwargs))
return getattr(self, internal_name)
return property(wrapper, doc=method.__doc__)
if is_callable: return method_decorator(name) # Allows lazy_property(method)
return method_decorator # Allows lazy_property("name")(method)
To demonstrate:
import time
class C(object):
def large_function(self, optional_param=[]):
"""Large remote query that takes some time"""
time.sleep(3)
# usually run without optional_param
return ['val'] + optional_param
short1 = lazy_property(large_function)
short2 = lazy_property("short2")(large_function)
short3 = lazy_property("short3", optional_param=["foo"])(large_function)
pass
c = C()
print(c.short1)
print(c.short2)
print(c.short3)
print(c.__dict__)
This is all the functionality I currently require, and it seems flexible enough. The method_or_name
variable was chosen to be unlikely to coincide with any kwargs
use (instead of just name
).
Upvotes: 2
Reputation: 7186
The closest I could get is the following:
def lazy_property(name):
internal_name = "_" + name
def method_decorator(method):
def wrapper(self, *args, **kwargs):
if not hasattr(self, internal_name):
setattr(self, internal_name, method(self, *args, **kwargs))
return getattr(self, internal_name)
return property(wrapper, doc=method.__doc__)
return method_decorator
class C(object):
def large_function(self, optional_param=[]):
"""Large remote query that takes some time"""
time.sleep(3)
# usually run without optional_param
return ['val'] + optional_param
shorthand = lazy_property("shorthand")(large_function)
You still need that one extra line, unfortunately. The problem is that the outer two functions of that decorator don't know anything about the class or instance and so there is no way to bind the result to a member of that class or instance.
The outer call (with the name) is not necessarily needed, if you don't care about the internal name being the same as the property (here I take the method name as a base):
def lazy_property(method):
internal_name = "_" + method.__name__
def wrapper(self, *args, **kwargs):
if not hasattr(self, internal_name):
setattr(self, internal_name, method(self, *args, **kwargs))
return getattr(self, internal_name)
return property(wrapper, doc=method.__doc__)
class C(object):
def large_function(self, optional_param=[]):
"""Large remote query that takes some time"""
time.sleep(3)
# usually run without optional_param
return ['val'] + optional_param
shorthand = lazy_property(large_function)
Alternatively you could generate a random name using str(uuid.uuid4())
.
Upvotes: 4