Decorating an instance method of a class with a decorating function

41 Views Asked by At

I am using Python 3.10. Consider this toy example of a cache that caches the very first call to an instance method and then returns the cached value on subsequent calls:

import functools

def cache(func):
    @functools.wraps(func)  # for __name__
    def wrapper(*args, **kwargs):
        if not wrapper.cache:
            print("caching...")
            wrapper.cache = func(*args, **kwargs)
        return wrapper.cache
    wrapper.cache = None
    return wrapper

class Power:
    def __init__(self, exponent):
        self.exponent = exponent
    @cache
    def of(self, base):
        return base ** self.exponent

# test
>>> cube = Power(3)
>>> cube.of(2)
caching...
8
>>> cube.of.cache
8
>>> cube.of.__dict__
{'__wrapped__': <function __main__.Power.of(self, base)>, 'cache': 8}
>>> cube.of.cache = None
...
AttributeError: 'method' object has no attribute 'cache'

I have two questions:

1.) The accepted answer here says that the @cache decorator runs when the Power class is constructed and it will be passed an unbound method (of in my case). I guess this claim is true only when you decorate an instance method with a class decorator. There it is an issue that you would need a reference of the cube object to be stored in the decorating class instance construction, but that cube instance is not defined yet. I am having trouble reconciling this claim with the fact that my example works; the decorated of method is passed a tuple with the first element being the cube instance and the second the base=2 parameter

2.) I can access the .cache attribute but why can't I reset it? It gives AttributeError.

2

There are 2 best solutions below

1
quamrana On

What happens is that the decorating mechanism assigns the result of cache(of) to Power.of. So it is Power which has the attribute of (your cache decorator).

You can write: Power.of.cache = None to get what you want:

cube = Power(3)
print(cube.of(2))
print(cube.of(2))

Power.of.cache = None
print(cube.of(2))

Output:

caching...
8
8
caching...
8
3
matszwecja On

Since 2) is easier to answer, I'll start with that one. It is actually directly answered in the documentation for methods:

Like function objects, bound method objects support getting arbitrary attributes. However, since method attributes are actually stored on the underlying function object (method.__func__), setting method attributes on bound methods is disallowed.

As, can be easily verified,

cube.of.__func__.cache = None

does in fact work with your example.

Now in regards to 1), you do not need an instance to be able to run the decorator - "calling the decorator" does not mean calling the function that is being decorated. It refers to the process of taking this function, wrapping it with some additional functionality and returning in place of the original function. It will only be called when you actually call decorated function, and that's when arguments need to be defined and will be eventually passed to of (or ommited if cache is already defined).