【问题标题】:Alternatives to decorators for saving metadata about classes用于保存有关类的元数据的装饰器的替代方案
【发布时间】:2025-12-10 01:00:02
【问题描述】:

我正在编写一个 GUI 库,我想让程序员提供关于他们的程序的元信息,我可以使用这些元信息来微调 GUI。我打算为此使用函数装饰器,例如:

class App:
    @Useraction(description='close the program', hotkey='ctrl+q')
    def quit(self):
        sys.exit()

问题是这些信息需要绑定到相应的类。例如,如果程序是一个图像编辑器,它可能有一个Image 类,它提供了更多的用户操作:

class Image:
    @Useraction(description='invert the colors')
    def invert_colors(self):
        ...

但是,由于在 python 3 中取消了未绑定方法的概念,似乎没有办法找到函数的定义类。 (我找到了this old answer,但这在装饰器中不起作用。)

那么,既然装饰器看起来不起作用,那么最好的方法是什么?我想避免使用类似的代码

class App:
    def quit(self):
        sys.exit()

Useraction(App.quit, description='close the program', hotkey='ctrl+q')

如果可能的话。


为了完整起见,@Useraction 装饰器看起来有点像这样:

class_metadata= defaultdict(dict)
def Useraction(**meta):
    def wrap(f):
        cls= get_defining_class(f)
        class_metadata[cls][f]= meta
        return f
    return wrap

【问题讨论】:

  • 您可以另外使用类装饰器或元类来检查方法并将其元数据保存在类中。
  • getattr(inspect.getmodule(f), f.__qualname__.rsplit('.', 1)[0]) 可能很老套,但可以省去编写元类的麻烦。字符串 f.__qualname__.rsplit('.', 1)[0] 可能已经足以作为您的 defaultdict 的键
  • f.__qualname__.split('.')[0] 可以工作,并为您提供类名,这可能足以作为 dict 键。
  • @schwobaseggl 这可行。以这种方式编写代码感觉不太好,但我想它不会在大多数实际场景中造成问题,而且它比元类更容易/更好使用。
  • 我刚刚意识到如果我只使用类的名称,会导致继承问题。类不会从其父类继承任何元数据。也许元类毕竟是要走的路。

标签: python python-3.x decorator


【解决方案1】:

我找到了一种让装饰器与 inspect 模块一起工作的方法,但这不是一个很好的解决方案,所以我仍然愿意接受更好的建议。

基本上我正在做的是遍历解释器堆栈,直到找到当前类。由于此时不存在类对象,因此我提取了类的 qualname 和 module。

import inspect

def get_current_class():
    """
    Returns the name of the current module and the name of the class that is currently being created.
    Has to be called in class-level code, for example:

    def deco(f):
        print(get_current_class())
        return f

    def deco2(arg):
        def wrap(f):
            print(get_current_class())
            return f
        return wrap

    class Foo:
        print(get_current_class())

        @deco
        def f(self):
            pass

        @deco2('foobar')
        def f2(self):
            pass
    """
    frame= inspect.currentframe()
    while True:
        frame= frame.f_back
        if '__module__' in frame.f_locals:
            break
    dict_= frame.f_locals
    cls= (dict_['__module__'], dict_['__qualname__'])
    return cls

然后在某种后处理步骤中,我使用模块和类名来查找实际的类对象。

def postprocess():
    global class_metadata

    def findclass(module, qualname):
        scope= sys.modules[module]
        for name in qualname.split('.'):
            scope= getattr(scope, name)
        return scope

    class_metadata= {findclass(cls[0], cls[1]):meta for cls,meta in class_metadata.items()}

这个解决方案的问题是延迟的类查找。如果类被覆盖或删除,后处理步骤将找到错误的类或完全失败。示例:

class C:
    @Useraction(hotkey='ctrl+f')
    def f(self):
        print('f')

class C:
    pass

postprocess()

【讨论】:

    【解决方案2】:

    您正在使用装饰器将元数据添加到方法中。没事儿。它可以做到,例如这样:

    def user_action(description):
        def decorate(func):
            func.user_action = {'description': description}
            return func
        return decorate
    

    现在,您想要收集该数据并将其存储在一个全局字典中,格式为 class_metadata[cls][f]= meta。为此,您需要找到所有装饰方法及其类。

    最简单的方法可能是使用元类。在元类中,您可以定义创建类时会发生什么。在这种情况下,遍历类的所有方法,找到修饰的方法并将它们存储在字典中:

    class UserActionMeta(type):
        user_action_meta_data = collections.defaultdict(dict)
    
        def __new__(cls, name, bases, attrs):
            rtn = type.__new__(cls, name, bases, attrs)
            for attr in attrs.values():
                if hasattr(attr, 'user_action'):
                    UserActionMeta.user_action_meta_data[rtn][attr] = attr.user_action
            return rtn
    

    我将全局字典 user_action_meta_data 放在元类中只是因为它感觉合乎逻辑。它可以在任何地方。

    现在,在任何类中使用它:

    class X(metaclass=UserActionMeta):
    
        @user_action('Exit the application')
        def exit(self):
            pass
    

    Static UserActionMeta.user_action_meta_data 现在包含您想要的数据:

    defaultdict(<class 'dict'>, {<class '__main__.X'>: {<function exit at 0x00000000029F36C8>: {'description': 'Exit the application'}}})
    

    【讨论】: