提供函数元数据的最Pythonic方式是什么?
我正在创建一个非常基础的平台,使用的是Python 2.7模块。这个模块有一个读取-评估-打印的循环,用户输入的命令会被映射到相应的函数调用上。因为我想让插件模块的构建变得简单,所以这些函数调用会从我的主模块调用任意的插件模块。我希望插件开发者能够指定一个命令来触发他的函数,所以我在寻找一种Pythonic的方式,让插件模块能够远程地在主模块的命令-函数字典中添加映射。
我考虑了几种方法:
方法名解析:主模块会导入插件模块,并扫描其中符合特定格式的方法名。例如,它可能会把download_file_command(file)这个方法添加到字典中,作为“下载文件” -> download_file_command的映射。然而,要得到一个简洁、易于输入的命令名(比如“dl”),就需要函数的名字也要短,这样不利于代码的可读性。而且,这要求插件开发者必须遵循一个精确的命名格式。
跨模块装饰器:装饰器可以让插件开发者随意命名他的函数,只需添加类似@Main.register("dl")的东西,但这必然要求我修改另一个模块的命名空间,并在主模块中保持全局状态。我知道这样做是非常不好的。
同模块装饰器:使用与上述相同的逻辑,我可以添加一个装饰器,将函数的名字添加到插件模块本地的某个命令名->函数映射中,并通过API调用将映射提取到主模块。不过,这要求某些方法必须始终存在或被继承,而且——如果我对装饰器的理解是正确的——这个函数只会在第一次运行时注册自己,之后每次运行都会不必要地重新注册。
因此,我真正需要的是一种Pythonic的方式来标注一个函数,指定应该触发它的命令名,而这个方式不能是函数的名字。我需要在导入模块时提取命令名->函数的映射,插件开发者的工作越少越好。
感谢你的帮助,如果我对Python的理解有任何不足之处,请多多包涵;我对这个语言还比较陌生。
3 个回答
插件系统主要分为两个部分:
- 发现插件
- 在插件中触发一些代码执行
你提到的解决方案只涉及第二个部分。
实现这两个部分的方法有很多,具体取决于你的需求。例如,为了启用插件,可以在你的应用程序的配置文件中指定它们:
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}
它定义了两个命令add
和multiply
。
在@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}
用户自定义的函数可以有任意的属性。这意味着你可以给插件函数指定一个特定名称的属性。例如:
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
想了解用户自定义函数的任意属性,可以查看文档