Michael
Michael

Reputation: 1397

Tracking decorated methods of children classes in python

In python, how can I setup a parent class to track methods with a specific decorator for each child seperatly? A quick code snippet of what I am trying to do:

class Parent:
    decorated_func_dict = {} #dictionary that stores name->func for decorated functions
    def get_func_by_decorator_name(self, name):
        #stuff
        pass

class Child1(Parent):
    @func_name("Bob")
    def bob_func(self, *args):
        pass

    @func_name("Tom")
    def func2(self, *args):
        pass

class Child2(Parent):
    @func_name("Bob")
    def func_bob2(self, *args):
        pass

foo = Child1()
bar = Child2()

foo.get_func_by_decorator_name("Bob")
#Returns foo.bob_func

bar.get_func_by_decorator_name("Bob")
#Returns bar.func_bob2

Using Python 3.9.

Upvotes: 1

Views: 48

Answers (1)

Mad Physicist
Mad Physicist

Reputation: 114440

A decorator is not something that makes a function look pretty. It is a callable that ingests an object (not only functions), does some arbitrary operations, and returns a replacement object.

In this case, your decorator should be storing references to function objects in a dictionary somewhere. The problem is that you won't be able to reference the class in which the functions are defined until it is created, which happens well after the decorator is run. You can avoid this by storing the name of the class as well as the name of the function.

The final step here is to properly bind the function objects to methods on the right object. That is something that get_func_by_decorated_name can do for you.

In sum, you can write something like this:

decorated_func_dict = {}

def func_name(cls_name, func_name):
    def decorator(func):
        decorated_func_dict.setdefault(cls_name, {})[func_name] = func
        return func
    return decorator

class Parent:
    def get_func_by_decorator_name(self, name):
        return decorated_func_dict[type(self).__name__][name].__get__(self)

class Child1(Parent):
    @func_name("Child1", "Bob")
    def bob_func(self, *args):
        pass

    @func_name("Child1", "Tom")
    def func2(self, *args):
        pass

class Child2(Parent):
    @func_name("Child2", "Bob")
    def func_bob2(self, *args):
        pass

And indeed you get:

>>> foo.get_func_by_decorator_name("Bob")
<bound method Child1.bob_func of <__main__.Child1 object at 0x000001D58181E070>>
>>> bar.get_func_by_decorator_name("Bob")
<bound method Child2.func_bob2 of <__main__.Child2 object at 0x000001D582041F10>>

Another way to do this is to give your functions a name attribute, which you can then aggregate into a mapping in __init_subclass__ in Parent. This allows you to make an interface a bit closer to what you originally intended:

def func_name(func_name):
    def decorator(func):
        func.special_name = func_name
        return func
    return decorator

class Parent:
    def __init_subclass__(cls):
        cls.decorated_func_dict = {}
        for item in cls.__dict__.values():
            if hasattr(item, 'special_name'):
                cls.decorated_func_dict[item.special_name] = item
                del item.special_name  # optional
    def get_func_by_decorator_name(self, name):
        return self.decorated_func_dict[name].__get__(self)

class Child1(Parent):
    @func_name("Bob")
    def bob_func(self, *args):
        pass

    @func_name("Tom")
    def func2(self, *args):
        pass

class Child2(Parent):
    @func_name("Bob")
    def func_bob2(self, *args):
        pass

The results are identical to the first example.

The easiest way would of course be to get access to the child's namespace before the class is created, e.g. with a metaclass.

Upvotes: 1

Related Questions