Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
104 views
in Technique[技术] by (71.8m points)

python - Using a class as a decorator?


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The thing is that besides the decorator mechanism, there is the mechanism that Python uses so that functions inside class bodies behave as instance methods: it is the "descriptor protocol". That is actually simple: all function objects have a __get__ method (but not __set__ or __del__) method, which make of them "non data descriptors". When Python retrieves the attribute from an instance, __get__ is called with the instance as a parameter - the __get__ method them have to return a callable that will work as the method, and has to know which was the instance called:


# example only - DO NOT DO THIS but for learning purposes,
#  due to concurrency problems:

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        
        return self.func(self.instance, *args, **kwargs)

    def __get__(self, instance, owner):
        self.instance = instance
        return self

class A:
    @D
    def f(self, x):
        print(self, x)

a=A()
a.f(1)

This will print "<main.A object at 0x...> 1"

However, as it is easily perceivable this only allows the decorated method to be called in a single instance at once - even non parallel code that owns more than an instance of "A" could have the method called with the wrong instance in mind. That is, this sequence:


In [127]: a1 = A()                

In [128]: a2 = A()                

In [129]: f1 =  a1.f              

In [130]: f2 = a2.f               

In [131]: f1() 

will end up calling "a2.f()" not "a1.f()"

To avoid this, what you have to return is a callable from __get__ that won't need to retrieve the instance as a class attribute. One way to do that is to create a partial callable and include that - however, note that since this is a necessary step, there is no need for the decorator class itself to have the "run wrapper + original code" function in the __call__ method - it could have any name:

from functools import partial

class D:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, _instance=None, **kwargs):
        if _instance:
            return self.func(_instance, *args, **kwargs)
        else: 
            return self.func(*args, **kwargs)
        

    def __get__(self, instance, owner):
        return partial(self.__call__, _instance=instance)
      

class A:
    @D
    def f(self, x):
        print(self, x)

a=A()
a.f(1)

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...