Using Python decorators to gather information on available method and store that data in the class or instance

Viewed 145

I found a lot of questions regarding decorators and classes, and access to the class instance that is being decorated. As far as I could understand, the decorator generator can not access the object instance because, at that time, no object was yet created.

Nevertheless, it seems that if a decorator uses a class reference (I mean, receives cls), then I am able to access to some class field:

     class Foo:
          _tags = {}
      
          @classmethod
          def _tag(cls, tag_name):
              def decorator(func):
                  def tmp(*args, **kwargs):
                      return func(*args, **kwargs)
     
                  cls._tags[tag_name] = func.__name__
                  return tmp
     
              return decorator

This works as a charm for subclasses, where I can write:

 @Foo._tag("something")
 def somethingelse():
     pass

But this stores the information in the father class (as expected), as we are talking about class variables.

I wonder if there is a way to have a similar behavior, but guaranteeing that each subclass has its own instance of the _tags property.

I can foresee some workarounds, like parsing the source code, or inspecting the functions, during __init__. But I was looking for a simpler way, if there is such a way.

Edit 1: to make it clearer, I want to decorate methods from the subclass. Thus, this is not a possibility:

     Class Bar (Foo):
        @Bar._tag("method")
        def method(self):
           pass

as the processor/compiler did not complete the creation of the class yet. Otherwise, the solution proposed of a two level dictionary with class/method would work good enough for me.

WORKAROUND

My decorator:

    @classmethod
    def TAG(cls, name):
        def decorator(func):
            def tmp(*args, **kwargs):
                return func(*args, **kwargs)

            tmp._has_alias = name
            return tmp
        return decorator

During __init__, I get this info, and store:

        self._tags = {getattr(self, name).has_alias: name for name in dir(self) if callable(getattr(self, name)) and hasattr(getattr(self, name), "_has_alias")}
1 Answers

A quick workaround could be to treat _tags as a dictionary of dictionaries, with the top level key being the name of the subclass, and its associated dictionary containing the callables wrapped by its inherited version of _tag:

from collections import defaultdict
class Foo:
  _tags = defaultdict(dict)
  @classmethod
  def _tag(cls, tag_name):
    def decorator(func):
      def tmp(*args, **kwargs):
         return func(*args, **kwargs)
      cls._tags[cls.__name__][tag_name] = func.__name__ #storing tag_name under the name of the subclass
      return tmp
    return decorator

Now, when creating subclasses and wrapping functions, _tags will store the values under the subclass name by which _tag was called:

class A(Foo):
  pass

class B(Foo):
  pass

@A._tag('test_a')
def a():
  pass

@B._tag('test_b')
def b():
  pass

print(dict(Foo._tags))

Output:

{'A': {'test_a': 'a'}, 'B': {'test_b': 'b'}}
Related