Search code examples
pythonclassdecorator

How to automatically "register" methods in a python class as a list class variable?


When defining a Python class, I'd like to use decorators to register some of its methods into a class variable list. Here's an example of incorrect python that outlines what I'm looking for:

class MyClass:

    dangerous_methods = []

    @classmethod
    def dangerous_method(cls, func):
        cls.dangerous_methods.append(func)
        return func

    @MyClass.dangerous_method
    def incinerate(self):
        pass

    def watch_tv(self):
        pass

    @MyClass.dangerous_method
    def stab(self):
        pass

    def print_dangerous_methods(self):
        print(self.dangerous_methods)


obj = MyClass()
obj.print_dangerous_methods()

with the expected output being

[<function MyClass.incinerate at 0x000001A42A629280>, <function MyClass.stab at 0x000001A42A629281>]

Is it possible to do this without torturing Python too much?


Solution

  • All you really want to do is to set dangerous on the methods. Remember that python functions and methods are first-class objects, you can set arbitrary attributes on them.

    def print_dangerous_methods(cls):
        """ yes, you could also return a list """
        for name in dir(cls):
            f = getattr(cls, name)
            if callable(f) and getattr(f, "dangerous", False):
                print(name)
    
    
    def dangerous(func):
        setattr(func, "dangerous", True)
        return func
    
    class MyClass:
    
        @dangerous
        def incinerate(self):
            print("incinerate")
    
        def watch_tv(self):
            pass
    
        @dangerous
        def stab(self):
            return "you've been stabbed"
    
        class_born_dangerous = print_dangerous_methods
    
    print("\non instance")
    obj = MyClass()
    print_dangerous_methods(obj)
    
    print("\non class")
    print_dangerous_methods(MyClass)
    
    print("\nand yes, they work")
    obj.incinerate()
    print (obj.stab())
    
    print("\nas a classmethod")
    obj.class_born_dangerous()
    

    output:

    
    on instance
    incinerate
    stab
    
    on class
    incinerate
    stab
    
    and yes, they work
    incinerate
    you've been stabbed
    
    as a classmethod
    incinerate
    stab
    
    

    If you want to generalize this approach and set arbitrary attributes, you need to set up a parametrized decorator:

    def annotate_func(**kwds):
        """set arbitrary attributes"""
        def actual_decorator(func):
            for k, v in kwds.items():
                setattr(func, k, v)
            return func
        return actual_decorator
    

    which you would use as follows:

        @annotate_func(dangerous=1,range=1000)
        def shoot(self, times):
            for i in range(0, times):
                print("bang!")