首页 > 解决方案 > 创建一个可以跨模块以分布式方式加载的中央注册表

问题描述

我正在尝试创建一个可以使用 name-factory_method 对加载的注册表,以便客户端代码能够使用注册表通过它们的给定名称实例化这些对象。如果我在注册表模块中使用对加载注册表,我可以让它工作。

但是,如果我在其他模块之间分配加载(例如使用工厂方法),我似乎无法加载注册表。我更喜欢后一种选择,因为注册表模块不必知道所有潜在的工厂方法。但我似乎无法让它发挥作用。

我创建了一个简单的三个模块版本,它可以工作,然后一个在下面失败:

工作版本

注册表.py

registry = {}

def register_thing(description, thingmaker):
    registry[description] = thingmaker

def get_thing(description, *args, **kwargs):
    thingmaker = registry[description]
    return thingmaker(*args, **kwargs)

def show_things():
    return registry.keys()

from things import Thing1
from things import Thing2
register_thing("Thing1", Thing1)
register_thing("Thing2", Thing2)

东西.py

class Thing1(object):
      def __init__(self):
          pass
      def message(self):
          return "This is a thing"

class Thing2(object):
      def __init__(self, *args, **kwargs):
          self.args = args
          self.kwargs = kwargs
      def message(self):
          return "This is a different thing with args %r and kwargs %r" \
             % (self.args, self.kwargs)

使用_things.py

import registry

print("The things in the registry are: %r" % registry.show_things())

print("Getting a Thing1")
thing = registry.get_thing("Thing1")
print("It has message %s" % thing.message())

print("Getting a Thing2")
thing = registry.get_thing("Thing2", "kite", on_string="Mothers new gown")
print("It has message %s" % thing.message())

运行 use_things.py 给出:

The things in the registry are: dict_keys(['Thing1', 'Thing2'])
Getting a Thing1
It has message This is a thing
Getting a Thing2
It has message This is a different thing with args ('kite',) and kwargs {'on_string': 'Mothers new gown'}

分布式版本 registry.py失败

registry = {}

def register_thing(description, thingmaker):
    registry[description] = thingmaker

def get_thing(description, *args, **kwargs):
    thingmaker = registry[description]
    return thingmaker(*args, **kwargs)

def show_things():
    return registry.keys()

东西.py

import registry

class Thing1(object):
      def __init__(self):
          pass
      def message(self):
          return "This is a thing"

class Thing2(object):
      def __init__(self, *args, **kwargs):
          self.args = args
          self.kwargs = kwargs
      def message(self):
          return "This is a different thing with args %r and kwargs %r" \
             % (self.args, self.kwargs)


register.register_thing("Thing1", Thing1)
register.register_thing("Thing2", Thing2)

use_things.py (和以前一样)

现在,如果我运行 use_things.py 我会得到以下信息:

The things in the registry are: dict_keys([])
Getting a Thing1
Traceback (most recent call last):
  File "use_things.py", line 6, in <module>
    thing = registry.get_thing("Thing1")
  File "/home/luke/scratch/registry_example/registry.py", line 7, in get_thing
    thingmaker = registry[description]
KeyError: 'Thing1'

显然,things.py 模块永远不会被调用并填充注册表。如果我在 registry.py 的底部重新添加以下行,它会再次起作用:

import things

但这再次要求 registry.py 了解所需的模块。我希望注册表由某个目录下的模块自动填充,但我似乎无法让它工作。有人可以帮忙吗?

标签: pythonregistry

解决方案


您所描述的基本上是一种“插件”软件架构,并且有不同的实现方式。我个人认为使用 Python 包来做这件事是一种很好的方法,因为它是一种定义明确的“pythonic”方式来组织模块,并且语言直接支持它,这使得做一些涉及的事情变得更容易一些。

我认为这基本上可以满足您的所有需求。它基于我对如何导入包中所有模块的成员的问题的回答?这需要将所有工厂脚本放在包目录中,文件层次结构如下:

use_things.py              
things/                    
    __init__.py            
    thing1.py              
    thing2.py              

如果您愿意,可以轻松地将包和工厂脚本的名称更改为其他名称。

things在这个例子中,它没有一个显式的公共注册表,它只使用包的名称。_registry(但是,如果您觉得出于某种原因确实需要一本,那么该模块中有一本私人词典。)

尽管必须显式导入包,但它的初始化__init__.py脚本会自动导入子目录中的其余文件——因此添加或删除一个文件只需将其脚本放在子目录中或从那里删除即可。

这个实现中没有register_thing()函数,因为脚本中的私有_import_all_modules()函数__init__.py有效地自动执行它——但请注意,它“自动注册”每个工厂模块脚本中的所有公共内容。当然,如果您希望以不同的方式完成,您可以修改其工作方式。(如果你有兴趣,我有几个想法。)

以下是上述每个文件的内容:

use_things.py

import things  # Import package.

print("The things in the package are: %r" % things.show_things())

print("Getting a Thing1")
thing = things.get_thing("Thing1")
print(f"It has message {thing.message()!r}")

print("Getting a Thing2")
thing = things.get_thing("Thing2", "kite", on_string="Mothers new gown")
print(f"It has message {thing.message()!r}")

things/__init__.py

def _import_all_modules():
    """ Dynamically imports all modules in this package directory. """
    import traceback
    import os
    globals_, locals_ = globals(), locals()

    registry = {}
    # Dynamically import all the package modules in this file's directory.
    for filename in os.listdir(__name__):
        # Process all python files in directory that don't start with an underscore
        # (which also prevents this module from importing itself).
        if filename[0] != '_' and filename.split('.')[-1] in ('py', 'pyw'):
            modulename = filename.split('.')[0]  # Filename sans extension.
            package_module = '.'.join([__name__, modulename])
            try:
                module = __import__(package_module, globals_, locals_, [modulename])
            except:
                traceback.print_exc()
                raise
            for name in module.__dict__:
                if not name.startswith('_'):
                    registry[name] = module.__dict__[name]

    return registry

_registry = _import_all_modules()

def get_thing(description, *args, **kwargs):
    thingmaker = _registry[description]
    return thingmaker(*args, **kwargs)

def show_things():
    return list(_registry.keys())

things/thing1.py

class Thing1(object):
    def __init__(self):
        pass
    def message(self):
        return f'This is a {type(self).__name__}'

things/thing2.py

class Thing2(object):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs
    def message(self):
        return (f"This is a different thing with args {self.args}"
                f" and kwargs {self.kwargs}")

运行use_things.py给出:

The things in the package are: ['Thing1', 'Thing2']
Getting a Thing1
It has message 'This is a Thing1'
Getting a Thing2
It has message "This is a different thing with args ('kite',) and kwargs {'on_string': 'Mothers new gown'}"

推荐阅读