简体   繁体   中英

Magic attributes of functions inconsistent

I'm currently working on a project where I have a class with various expensive methods that I'd like to cache. I want to implement the cache myself, both for exercise as well as that it's special in that it specifically aimed at functions where f(f(x)) == x is True (via a dict subclass where d[key] == value and d[value] == key is True ). This goes kinda deep into python at times, and I'm a bit lost at the moment.

The cache should be attached to the class that the method is defined on and thus I needed to extract the class from the function in the decorator that adds the cache to a function. The problem is that it seems like python does indeed do something else as f = dec(f) when decorating f with @dec .

My test code and the beginning of the cache decorator is:

def bidirectional_cache(function):
    """Function decorator for caching
    For functions where f(f(x)) == x is True
    Requires hashable args and doesn't support kwargs
    """
    parent_instance = getattr(function, "__self__", None)
    #print(type(function))
    #print(dir(function))
    if parent_instance is None:
        parent_class = globals()[function.__qualname__.rstrip(f".{function.__name__}")]
    elif type(parent_instance) is type:
        parent_class = parent_instance
    else:
        parent_class = parent_instance.__class__
    print(parent_class)
    ...

class A():
    N = 0
    def __init__(self, n):
        self.n = n

    def __hash__(self):
        return hash(self.n)

    def __add__(self, other):
        return self.__class__(int(self) + int(other))

    def __int__(self):
        return self.n

    @bidirectional_cache
    def test(self):
        return f"n = {self.n}"

    @bidirectional_cache
    @staticmethod
    def test_static(a, b):
        return a + b

    @bidirectional_cache
    @classmethod
    def test_class(cls, b):
        return N + b

When defining A without the cache decorator and then execute the following calls (REPL session) it gives the outputs as expected:

>>> bidirectional_cache(A.test)
<class '__main__.A'>
>>> bidirectional_cache(A.test_static)
<class '__main__.A'>
>>> bidirectional_cache(A.test_class)
<class '__main__.A'>
>>> a = A(5)
>>> bidirectional_cache(a.test)
<class '__main__.A'>
>>> bidirectional_cache(a.test_static)
<class '__main__.A'>
>>> bidirectional_cache(a.test_class)
<class '__main__.A'>

But if I instead run the class definition with the decorator I always have staticmethod objects inside the decorator and it breaks because those don't have a __qualname__ . Calling dir on Ax , where x are all the test methods, gives a completely different output as when dir is called within the decorator.

The question I have is, why is it that @dec receives a function object that's different from what dec(f) receives? Is there any way to retrieve the class a function is defined on within the scope of a decorator or would I always have to manually do Ax = dec(x) ?

The __self__ attribute you are trying to access there is not present when the decorator code is run.

The decorator body is called with the method as a parameter when the class body is run - that is before the class itself is created, not to say instances of that class.

The easiest way to get the instance ( self ) in a method decorator is simply to accept it as a parameter in the wrapper function your decorator uses to replace the original method:

def bidirectional_cache(function):
    """Function decorator for caching
    For functions where f(f(x)) == x is True
    Requires hashable args and doesn't support kwargs
    """
    def wrapper(self, *args, **kw):
        parent_instance = self
        parent_class = parent_instance.__class__
        print(parent_class)
        ...
        result = function(self, *args, **kw)
        ...
        return result
    return wrapper

(For the method name to be preserved, you should decorate the wrapper inner function itself with functools.wraps )

In this model, when the code inside wrapper is run, you have a living instance of your class - and the self parameter is the instance - and you can decide whether or not to call the original function based in whatever you want and have stored in cache from previous calls.

So I just noticed that I did in fact not need to attach the cache to the class, making everything way easier. Details are in my comment under @jsbuenos answer. The final solution looks like this:

class BidirectionalDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(hash(key), value)
        super().__setitem__(value, key)

    def __delitem__(self, key):
        super().__delitem__(self[key])
        super().__delitem__(key)


def bidirectional_cache(function):
    """Function decorator for caching
    For functions where f(f(x)) == x is True
    Requires hashable args and doesn't support kwargs
    """
    cache = BidirectionalDict()
    @wraps(function)
    def wrapped(*args):
        if hash(args) not in cache:
            cache[hash(args)] = function(*args)
        return cache[hash(args)]
    return wrapped

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM