Anatoly Makarevich
Anatoly Makarevich

Reputation: 147

Convert lazy evaluation into decorator (Python)

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

Answers (2)

Anatoly Makarevich
Anatoly Makarevich

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

Graipher
Graipher

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

Related Questions