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

14

我正在构建一个非常基本的平台,以Python 2.7模块的形式存在。该模块拥有一个读取-求值-打印循环,在此循环中将用户输入的命令映射到函数调用。由于我试图使其易于构建我的平台的插件模块,因此来自主模块的函数调用将是对任意插件模块的调用。我希望插件开发人员能够指定触发其函数的命令,因此我一直在寻找一种Pythonic的方法,从插件模块中远程输入命令->函数字典中的映射到主模块。

我看过几个东西:

  1. 方法名称解析:主模块将导入插件模块并扫描匹配特定格式的方法名。例如,它可能将download_file_command(file)方法添加到其字典中,如“download file” - > download_file_command。但是,获取简洁、易于键入的命令名称(如“dl”)要求函数的名称也很短,这对代码可读性不利。它还要求插件开发人员遵守精确的命名格式。

  2. 跨模块装饰器:装饰器可以让插件开发人员命名其函数任何名称,并简单地添加类似于@Main.register(“dl”)的内容,但它们必然需要我修改另一个模块的命名空间并保留Main模块中的全局状态。我知道这是非常糟糕的。

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

因此,我真正需要的是一种用Python的方式来注释一个函数,以指定应该触发它的命令名称,而且这种方式不能是函数的名称。我需要在导入模块时能够提取命令名称->函数映射关系,插件开发者的工作越少越好。感谢您的帮助,如果我的Python理解有任何缺陷,敬请谅解;我对这门语言还比较新。

1
只是想确认您是否意识到,装饰器(以及函数定义)在运行时执行,Python 在编译时只编译您的代码。 - Duncan
1
方法2没有任何问题。让他们执行类似于import plugin@plugin.register('my_method_name')\ndef somefunc_meeting_an_api_spec():...的操作。 - Jon Clements
你可能想看看plac是如何做到这一点的。 - georg
1
使用 cmd.Cmdreadline 的制表符扩展支持,不会使短名称成为问题吗? - sotapme
@sotapme:谢谢,这几乎就是我一直在尝试构建的东西!不幸的是,Cmd通过继承来注册方法,所以我认为这意味着我需要在运行时动态地对主模块进行多重继承以使用不同的插件。 - mieubrisse
3个回答

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

要了解用户自定义函数的任意属性,请参阅文档


1
这正是我正在寻找的东西! - mieubrisse
我意识到这个问题有点老了,但请看看我的相关问题,链接在这里http://stackoverflow.com/questions/37192712/how-can-attributes-on-functions-survive-wrapping。您是否遇到过使用存储在函数上的属性进行包装的问题? - matthewatabet

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

谢谢你;我喜欢这个设计背后的解释。这个解释建议实现 get_commands() 列表的超类可能是一个好主意,这样开发人员就可以尽可能少地工作。 - mieubrisse
@mieubrisse:不需要类。我已经添加了完整的插件示例。 - jfs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接