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

Viewed 165

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?

3 Answers

This is one way to implement that:

class MyClass:
    def __init__(self):
        self.dangerous_methods = []

    def dangerous_method(func):
        def inner(self):
            self.dangerous_methods.append(func)
            return func(self)
        return inner

    @dangerous_method
    def incinerate(self):
        print('Incinerate called!')
        pass

    def watch_tv(self):
        print('Watch_tv called!')
        pass

    @dangerous_method
    def stab(self):
        print('Stab called!')
        pass

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


obj = MyClass()
obj.incinerate()
# Incinerate called!
obj.watch_tv()
# Watch_tv called!
obj.stab()
# Stab called!
obj.incinerate()
# Incinerate called!
obj.print_dangerous_methods()
# [<function MyClass.incinerate at 0x0000029C11666EE8>, <function MyClass.stab at 0x0000029C11666B88>, <function MyClass.incinerate at 0x0000029C11666EE8>]

Just note that in this way, functions are being added to the list ONLY once they've called and there is a risk that a function being added to the list multiple times. However, if you know that there are some functions in mind that you want to add to the list and they're constants, you can simply add them while the object is being constructed:

class MyClass:
    def __init__(self):
        self.dangerous_methods = [self.incinerate, self.stab]

    def incinerate(self):
        print('Incinerate called!')
        pass

    def watch_tv(self):
        print('Watch_tv called!')
        pass

    def stab(self):
        print('Stab called!')
        pass

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


obj = MyClass()
obj.print_dangerous_methods()
# [<bound method MyClass.incinerate of <__main__.MyClass object at 0x0000029C11388F08>>, <bound method MyClass.stab of <__main__.MyClass object at 0x0000029C11388F08>>]

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!")

The following snippet does exactly what you described.

Note that print_dangerous_methods is declared as a class method, because that’s what it really is (it applies to the class, not to a certain instance). That means you can call it even without creating an instance.

class MyClass:

    def dangerous_method(meth):
        meth.is_dangerous = True
        return meth

    @dangerous_method
    def incinerate(self):
        pass

    def watch_tv(self):
        pass

    @dangerous_method
    def stab(self):
        pass

    @classmethod
    def print_dangerous_methods(cls):
        print ([
            meth for meth in [
                getattr(cls, methname) for methname in dir(cls)
            ]
            if getattr(meth, "is_dangerous", False)
        ])

MyClass.print_dangerous_methods()
Related