Auto-register class methods using decorator

22,859

Solution 1

Not with just a decorator, no. But a metaclass can automatically work with a class after its been created. If your register decorator just makes notes about what the metaclass should do, you can do the following:

registry = {}

class RegisteringType(type):
    def __init__(cls, name, bases, attrs):
        for key, val in attrs.iteritems():
            properties = getattr(val, 'register', None)
            if properties is not None:
                registry['%s.%s' % (name, key)] = properties

def register(*args):
    def decorator(f):
        f.register = tuple(args)
        return f
    return decorator

class MyClass(object):
    __metaclass__ = RegisteringType
    @register('prop1','prop2')
    def my_method( arg1,arg2 ):
        pass

    @register('prop3','prop4')
    def my_other_method( arg1,arg2 ):
        pass

print registry

printing

{'MyClass.my_other_method': ('prop3', 'prop4'), 'MyClass.my_method': ('prop1', 'prop2')}

Solution 2

Here's a little love for class decorators. I think the syntax is slightly simpler than that required for metaclasses.

def class_register(cls):
    cls._propdict = {}
    for methodname in dir(cls):
        method = getattr(cls, methodname)
        if hasattr(method, '_prop'):
            cls._propdict.update(
                {cls.__name__ + '.' + methodname: method._prop})
    return cls


def register(*args):
    def wrapper(func):
        func._prop = args
        return func
    return wrapper


@class_register
class MyClass(object):

    @register('prop1', 'prop2')
    def my_method(self, arg1, arg2):
        pass

    @register('prop3', 'prop4')
    def my_other_method(self, arg1, arg2):
        pass

myclass = MyClass()
print(myclass._propdict)
# {'MyClass.my_other_method': ('prop3', 'prop4'), 'MyClass.my_method': ('prop1', 'prop2')}

Solution 3

If you need the classes name, use Matt's solution. However, if you're ok with just having the methods name -- or a reference to the method -- in the registry, this might be a simpler way of doing it:

class Registry:
    r = {}

    @classmethod
    def register(cls, *args):
        def decorator(fn):
            cls.r[fn.__name__] = args
            return fn
        return decorator

class MyClass(object):

    @Registry.register("prop1","prop2")
    def my_method( arg1,arg2 ):
        pass

    @Registry.register("prop3","prop4")
    def my_other_method( arg1,arg2 ):
        pass

print Registry.r

print

{'my_other_method': ('prop3', 'prop4'), 'my_method': ('prop1', 'prop2')}

Solution 4

Not as beautiful or elegant, but probably the simplest way if you only need this in one class only:

_registry = {}
class MyClass(object):
    def register(*prop):
        def decorator(meth):
            _registry[MyClass.__name__ + '.' + meth.__name__] = prop
        return decorator

    @register('prop1', 'prop2')
    def my_method(self, arg1, arg2):
        pass
    @register('prop3', 'prop4')
    def my_other_method(self, arg1, arg2):
        pass

    del register

Solution 5

To summarize, update and explain the existing answers, you have two options:

  1. Using Class Decorators (suggested by @unutbu)
  2. Using a Metaclass (suggested by @Matt Anderson)

However, both of them rely on giving the function an attribute so it can be identified:

def register(*args):
    """
    Creates an attribute on the method, so it can
    be discovered by the metaclass
    """

    def decorator(f):
        f._register = args
        return f

    return decorator

1. The Class Decorator Approach

import inspect

def class_register(cls):
    for method_name, _ in inspect.getmembers(cls):
        method = getattr(cls, method_name)
        if hasattr(method, "_prop"):
            cls._propdict.update({f"{cls.__name__}.{method_name}": method._prop})
    return cls


@class_register
class MyClass:

    _propdict = {}

    @register("prop1", "prop2")
    def my_method(self, arg1, arg2):
        pass

    @register("prop3", "prop4")
    def my_other_method(self, arg1, arg2):
        pass


print(MyClass._propdict)

2. The Metaclass Approach

registry = {}


class RegisteringType(type):
    def __init__(cls, name, bases, attrs):
        for key, val in attrs.items():
            properties = getattr(val, "_register", None)
            if properties is not None:
                registry[f"{name}.{key}"] = properties


class MyClass(metaclass=RegisteringType):
    @register("prop1", "prop2")
    def my_method(self, arg1, arg2):
        pass

    @register("prop3", "prop4")
    def my_other_method(self, arg1, arg2):
        pass


print(registry)
Share:
22,859
adamk
Author by

adamk

Just me.

Updated on May 30, 2020

Comments

  • adamk
    adamk about 4 years

    I want to be able to create a python decorator that automatically "registers" class methods in a global repository (with some properties).

    Example code:

    class my_class(object):
    
        @register(prop1,prop2)
        def my_method( arg1,arg2 ):
           # method code here...
    
        @register(prop3,prop4)
        def my_other_method( arg1,arg2 ):
           # method code here...
    

    I want that when loading is done, somewhere there will be a dict containing:

    { "my_class.my_method"       : ( prop1, prop2 )
      "my_class.my_other_method" : ( prop3, prop4 ) }
    

    Is this possible?

  • Fabio Suarez
    Fabio Suarez about 14 years
    But you can write your own metaclass (or class decorator) to look through the methods of the class and search for methods with an additional _register_properties field, which was added by the register decorator before. Please tell me, if you want a example on that.
  • jMyles
    jMyles about 9 years
    What if you have a subclass, OurClass(MyClass) - and you want to do the same thing? How can you get the metaclass to traverse the attrs of both base and child?
  • Matt Anderson
    Matt Anderson about 9 years
    @jMyles, The parent class's methods are already in the registry; you want them to show up again as methods on the child class? You have the bases tuple for the child class; you can iterate over any or all of the class dictionaries of the parent / base classes, as well as the attributes of the new child class, when doing the child class's registration (if that's what you mean).
  • DBCerigo
    DBCerigo over 6 years
    Does this work if MyClass is defined in another module? (assuming that model imports class_register)
  • Uduse
    Uduse almost 5 years
    For those who're using python3, you need class MyClass(object, metaclass= RegisteringType): instead
  • Erikw
    Erikw over 2 years
    Thanks for the summary! Method 2 works for me but Method 1 does not register any methods (using exact copy of code above. I'm using Python 3.9.