Search code examples
python-3.xcustom-data-attributepython-decoratorsdefault-parameters

Why is applying a Python decorator with and without parenthesis different?


I have a self-written class-based Python decorator. As far as I can see there is a difference, when I apply the decorator to a method in a class. Usually, decorators like @classmethod, @staticmethod, @property or @unique are applied without parenthesis. These decorators expect no parameters and are (mostly?) written as function-based decorators.

So in contrast to these examples, my decorator is class-based and expects an optional parameter while applying.

My decorator:

class DocumentMemberAttribute(Attribute):
  def __init__(self, value=True):
    super().__init__()
    self.value = value

The Attributes class (the real decorator):

class Attribute:
  __AttributesMemberName__ = "__pyattr__"

  def __call__(self, func):
    self._AppendAttribute(func, self)
    return func

  @staticmethod
  def _AppendAttribute(func, attribute):
    # inherit attributes and append myself or create a new attributes list
    if (Attribute.__AttributesMemberName__ in func.__dict__):
      func.__dict__[Attribute.__AttributesMemberName__].append(attribute)
    else:
      func.__setattr__(Attribute.__AttributesMemberName__, [attribute])

  def __str__(self):
    return self.__name__

Example class:

class MyClass:
  def __init__(self, message=""):
    super().__init__()
    self.message = message

  @DocumentMemberAttribute
  def Method1(self):
    return "foo"

  @DocumentMemberAttribute()
  def Method2(self):
    return "bar"

  @DocumentMemberAttribute(False)
  def Method3(self):
    return "spam"

  @DocumentMemberAttribute(True)
  def Method4(self):
    return "egg"

The attached information is used in a custom autodoc-skip-member handler to decide if a method shall be documented or skipped. This is similar to the docit extension.

So when we look at the generated documentation (Sphinx), we can see these results:

class MyClass(message="")

Method1 = <lib.SphinxExtensions.DocumentMemberAttribute object at 0x0000000004AD9E80>

Method2()

Method4()

What can we see here:

  • Method1 has no parenthesis indicating a function/method, hence it's regarded a class field and Sphinx uses __str__ (or __repr__?) to document the initial value
  • Method3 is not documented as intended.

So my questions:

  • Why is there a difference in the behavior?
  • MUST I apply class-based attributes with parenthesis?
  • How should a user know how to use these 2 kinds of decorators? (I can document it for myself, but others might remember just the name und forget to add the parenthesis, because other decorators don't require them.)

pyAttributes is a set of attributes (real attributes, no Python attributes) written by me. They behave almost like in .NET


Solution

  • As @Ryan pointed out, the string behind the @ sign is an expression, which gets translated to a function call. The parameter of that call is the object, to which the decorator was applied.

    Example 1 - function-based decorators:

    I'll use the enum.unique decorator. It is written as a function.

    from enum import Enum, unique
    
    @unique
    class MyEnum(Enum):
      foo = 0
      bar = 1
    

    Gets translated to

    from enum import Enum, unique
    
    class MyEnum(Enum):
      foo = 0
      bar = 1
    
    MyEnum = unique(MyEnum)
    

    Example 2 - class-based decorators:

    I'll use the decorator from the question. It is written as a class.

    class MyClass:
      @DocumentMemberAttribute()
      def Method1():
        pass
    
      @DocumentMemberAttribute(True)
      def Method2():
        pass
    

    Gets translated to

    class MyClass:
      def Method1():
        pass
    
      Method1 = DocumentMemberAttribute()(Method1)
    
      def Method2():
        pass
    
      Method2 = DocumentMemberAttribute(True)(Method2)
    

    Please note the empty parenthesis before passing Method1 as an argument to the class' __call__ method. These are the initializer's (__init__) parenthesis. So these parenthesis get filled for Method2 when passing True as an argument.

    So in conclusion:

    • function-based decorators are applied without parenthesis
    • class-based decorators are applied with parenthesis

    Note for PyCharm users:

    Look at the colored @ sign:

    • blue -> function-based decorator
    • red -> class-based decorator