提供函数元数据的最Pythonic方式是什么?

14 投票
3 回答
9837 浏览
提问于 2025-04-17 15:36

我正在创建一个非常基础的平台,使用的是Python 2.7模块。这个模块有一个读取-评估-打印的循环,用户输入的命令会被映射到相应的函数调用上。因为我想让插件模块的构建变得简单,所以这些函数调用会从我的主模块调用任意的插件模块。我希望插件开发者能够指定一个命令来触发他的函数,所以我在寻找一种Pythonic的方式,让插件模块能够远程地在主模块的命令-函数字典中添加映射。

我考虑了几种方法:

  1. 方法名解析:主模块会导入插件模块,并扫描其中符合特定格式的方法名。例如,它可能会把download_file_command(file)这个方法添加到字典中,作为“下载文件” -> download_file_command的映射。然而,要得到一个简洁、易于输入的命令名(比如“dl”),就需要函数的名字也要短,这样不利于代码的可读性。而且,这要求插件开发者必须遵循一个精确的命名格式。

  2. 跨模块装饰器:装饰器可以让插件开发者随意命名他的函数,只需添加类似@Main.register("dl")的东西,但这必然要求我修改另一个模块的命名空间,并在主模块中保持全局状态。我知道这样做是非常不好的。

  3. 同模块装饰器:使用与上述相同的逻辑,我可以添加一个装饰器,将函数的名字添加到插件模块本地的某个命令名->函数映射中,并通过API调用将映射提取到主模块。不过,这要求某些方法必须始终存在或被继承,而且——如果我对装饰器的理解是正确的——这个函数只会在第一次运行时注册自己,之后每次运行都会不必要地重新注册。

因此,我真正需要的是一种Pythonic的方式来标注一个函数,指定应该触发它的命令名,而这个方式不能是函数的名字。我需要在导入模块时提取命令名->函数的映射,插件开发者的工作越少越好。

感谢你的帮助,如果我对Python的理解有任何不足之处,请多多包涵;我对这个语言还比较陌生。

3 个回答

2

插件系统主要分为两个部分:

  1. 发现插件
  2. 在插件中触发一些代码执行

你提到的解决方案只涉及第二个部分。

实现这两个部分的方法有很多,具体取决于你的需求。例如,为了启用插件,可以在你的应用程序的配置文件中指定它们:

plugins = some_package.plugin_for_your_app
    another_plugin_module
    # ...

要实现插件模块的加载:

plugins = [importlib.import_module(name) for name in config.get("plugins")]

获取一个字典:命令名称 -> 函数

commands = {name: func 
            for plugin in plugins
            for name, func in plugin.get_commands().items()}

插件的作者可以使用任何方法来实现get_commands(),比如使用前缀或装饰器——只要get_commands()能返回每个插件的命令字典,你的主应用程序就不需要关心具体实现。

举个例子,some_plugin.py(完整源代码):

def f(a, b):
    return a + b

def get_commands():
    return {"add": f, "multiply": lambda x,y: x*y}

它定义了两个命令addmultiply

14

在@ericstalbot的回答的基础上,你可能会觉得使用下面这样的装饰器很方便。

################################################################################
import functools
def register(command_name):
    def wrapped(fn):
        @functools.wraps(fn)
        def wrapped_f(*args, **kwargs):
            return fn(*args, **kwargs)
        wrapped_f.__doc__ += "(command=%s)" % command_name
        wrapped_f.command_name = command_name
        return wrapped_f
    return wrapped
################################################################################
@register('cp')
def copy_all_the_files(*args, **kwargs):
    """Copy many files."""
    print "copy_all_the_files:", args, kwargs
################################################################################

print  "Command Name: ", copy_all_the_files.command_name
print  "Docstring   : ", copy_all_the_files.__doc__

copy_all_the_files("a", "b", keep=True)

运行时的输出结果:

Command Name:  cp
Docstring   :  Copy many files.(command=cp)
copy_all_the_files: ('a', 'b') {'keep': True}
8

用户自定义的函数可以有任意的属性。这意味着你可以给插件函数指定一个特定名称的属性。例如:

def a():
  return 1

a.command_name = 'get_one'

然后,在你的模块中,你可以建立一个这样的映射:

import inspect #from standard library

import plugin

mapping = {}

for v in plugin.__dict__.itervalues():
    if inspect.isfunction(v) and v.hasattr('command_name'):
        mapping[v.command_name] = v

想了解用户自定义函数的任意属性,可以查看文档

撰写回答