Welcome, guest | Sign In | My Account | Store | Cart

This is a decorator wrapper class to delay decorating the base function until it is actually invoked. This can be useful when decorating ordinary functions if some decorator parameters depend on data that is only known at function invocation. It can also be used (and was written) to ensure that a decorated method of a class gets decorated once per instance instead of once per class; the use case that prompted this was the need to memoize a generator (see the Memoized Generator recipe), but the implementation is general.

Python, 85 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from new import instancemethod


class DelayedDecorator(object):
    """Wrapper that delays decorating a function until it is invoked.
    
    This class allows a decorator to be used with both ordinary functions and
    methods of classes. It wraps the function passed to it with the decorator
    passed to it, but with some special handling:
      
      - If the wrapped function is an ordinary function, it will be decorated
        the first time it is called.
      
      - If the wrapped function is a method of a class, it will be decorated
        separately the first time it is called on each instance of the class.
        It will also be decorated separately the first time it is called as
        an unbound method of the class itself (though this use case should
        be rare).
    """
    
    def __init__(self, deco, func):
        # The base decorated function (which may be modified, see below)
        self._func = func
        # The decorator that will be applied
        self._deco = deco
        # Variable to monitor calling as an ordinary function
        self.__decofunc = None
        # Variable to monitor calling as an unbound method
        self.__clsfunc = None
    
    def _decorated(self, cls=None, instance=None):
        """Return the decorated function.
        
        This method is for internal use only; it can be implemented by
        subclasses to modify the actual decorated function before it is
        returned. The ``cls`` and ``instance`` parameters are supplied so
        this method can tell how it was invoked. If it is not overridden,
        the base function stored when this class was instantiated will
        be decorated by the decorator passed when this class was instantiated,
        and then returned.
        
        Note that factoring out this method, in addition to allowing
        subclasses to modify the decorated function, ensures that the
        right thing is done automatically when the decorated function
        itself is a higher-order function (e.g., a generator function).
        Since this method is called every time the decorated function
        is accessed, a new instance of whatever it returns will be
        created (e.g., a new generator will be realized), which is
        exactly the expected semantics.
        """
        return self._deco(self._func)
    
    def __call__(self, *args, **kwargs):
        """Direct function call syntax support.
        
        This makes an instance of this class work just like the underlying
        decorated function when called directly as an ordinary function.
        An internal reference to the decorated function is stored so that
        future direct calls will get the stored function.
        """
        if not self.__decofunc:
            self.__decofunc = self._decorated()
        return self.__decofunc(*args, **kwargs)
    
    def __get__(self, instance, cls):
        """Descriptor protocol support.
        
        This makes an instance of this class function correctly when it
        is used to decorate a method on a user-defined class. If called
        as a bound method, we store the decorated function in the instance
        dictionary, so we will not be called again for that instance. If
        called as an unbound method, we store a reference to the decorated
        function internally and use it on future unbound method calls.
        """
        if instance:
            deco = instancemethod(self._decorated(cls, instance), instance, cls)
            # This prevents us from being called again for this instance
            setattr(instance, self._func.__name__, deco)
        elif cls:
            if not self.__clsfunc:
                self.__clsfunc = instancemethod(self._decorated(cls), None, cls)
            deco = self.__clsfunc
        else:
            raise ValueError("Must supply instance or class to descriptor.")
        return deco

The use case that prompted writing this class was the desire to "memoize" a generator using the Memoize Generator decorator. The obvious Pythonic way to do this is to write a decorator that can be applied to a method on the class, and make the method a generator function which the decorator then turns into a memoized generator. However, if the decorator is implemented in the usual way, this does not work properly; the memoization is done at the class level, when what is really desired is to do it at the instance level. In other words, the usual decorator implementation would make the decorated method a normal member of the class, but that would result in the memoized generator becoming common to all instances of the class. Since each instance represents a different generator, this is not what is needed.

The solution is to delay applying the decorator until the decorated function is actually called. For an ordinary function, this does not really change anything except that it delays invoking the decorator (which can be useful in itself); but for a method, it means the decorator wrapper can now use the descriptor protocol to be invoked each time the method is called on a new instance of the class. Then the decorator can be applied separately for each instance. As a side effect, the decorator is also applied the first time the method is called as an unbound method on the class itself; this use case should be very rare, but it is supported for consistency with the behavior of normal methods.