0

In my undertstanding, decorator class should contain __call__ or __new__ method. But cached_property in cpython repo doesn't follow the rules. Can anyone explain it for me?

class cached_property:
    def __init__(self, func):
        xxx

    def __set_name__(self, owner, name):
        xxx
    def __get__(self, instance, owner=None):
        xxx

    __class_getitem__ = classmethod(GenericAlias)
syheliel
  • 151
  • 1
  • 7
  • I omit the function body in order to post successfully, please follow the cpython link to see full code. – syheliel Dec 08 '22 at 13:11
  • Post an excerpt of the code instead of just a link. – SimonC Dec 09 '22 at 20:40
  • decorators can be callables is general so functions, classes (will do init or new) or class instances (those need call) and should return a function/callable again. – Daraan Dec 11 '22 at 18:58

1 Answers1

1

Do all decorator classes need __call__?

decorator class should contain __call__ or __new__ method

Not all decorator classes need to implement __call__. It's only required when we want to call the decorated object with ().

  • A decorator class that takes a callable to produce a callable has to implement __call__. In this example, __call__ is implemented because we want to do data.calculate().

    # Decorator to call and cache the function immediately
    class PreCompute:
        def __init__(self, func):
            self.value = func()
    
        def __call__(self, *args, **kwds):
            return self.value
    
    class Data:
        @PreCompute
        def calculate():
            print("Data.calculate called")
            return 42
    
    data = Data()
    # This actually calls PreCompute's __call__
    print(data.calculate())
    

    The definition of class Data here is roughly desugared to something like this, so when calling data.calculate() we're actually calling the __call__ function from class PreCompute.

    class Data:
        def calculate():
            print("Data.calculate called")
            return 42
        calculate = PreCompute(calculate)
    
  • A decorator class that takes a callable but does not produce a callable does not have to implement __call__. For example, we can modify the class Precompute decorator to the following code, which allows us to access data.calculate as if it's an attribute. For more information about what __get__ does, see Descriptor HowTo Guide from Python docs.

    class PreCompute:
      def __init__(self, func):
          self.value = func()
    
      def __get__(self, instance, owner):
          return self.value
    
    class Data:
        @PreCompute
        def calculate():
            print("Data.calculate called")
            return 42
    
    data = Data()
    # Access .calculate like an attribute
    print(data.calculate)
    

What about __new__?

I'm not sure how OP got the impression that decorator classes must define either __call__ or __new__. I've seen __new__ being defined for use cases like @singleton decorator for classes, but as discussed in the previous section about __call__, this is also not strictly required. The only function we must define is an __init__ that receives the object to be decorated.

How does @functools.cached_property work, then?

Now going back to the question, notice from the documentation of @functools.cached_property that it "transform a method of a class into a property", which is to be accessed without the parentheses (). Therefore, class cached_property implements __get__ but not __call__, which is similar to the second example above.

kotatsuyaki
  • 1,441
  • 3
  • 10
  • 17