关于python:根据安装的库动态选择适配器

Choose adapter dynamically depending on librarie(s) installed

我正在设计一个库,该库具有支持各种库的适配器。我希望库在导入特定类时动态选择在计算机上安装了它使用的库的适配器。

目标是能够更改程序所依赖的库,而不必修改代码。此特定功能用于处理rabbitmq连接,因为我们在PIKA中遇到了很多问题,所以我们希望能够在不必更改基础代码的情况下更改到其他库,例如pyampq或rabbitpy。

我正在考虑在servicelibrary.simple__init__.py文件中实现类似的功能。

1
2
3
4
5
6
7
8
try:
    #import pika # Is pika installed?
    from servicelibrary.simple.synchronous import Publisher
    from servicelibrary.simple.synchronous import Consumer
except ImportError:
    #import ampq # Is ampq installed?
    from servicelibrary.simple.alternative import Publisher
    from servicelibrary.simple.alternative import Consumer

然后当用户导入库时

1
from servicelibrary.simple import Publisher

底层看起来像这样

可替代的

1
2
3
4
5
6
7
import amqp

class Publisher(object):
    ......

class Consumer(object):
     ......

同步Py

1
2
3
4
5
6
7
import pika

class Publisher(object):
    ......

class Consumer(object):
     ......

这将在第一个未安装时自动选择第二个。

有没有更好的方法来实现这样的事情?如果有人可以将库/适配器与类似的实现链接起来,这也会很有帮助。

[编辑]

什么是实现这种功能最干净的方法?将来,我还希望能够更改默认首选项。最终,我可能会选择使用安装的库,因为我可以控制它,但这将是一个很好的特性。

亚历山德斯的建议很有趣,但我想知道是否有一个更清洁的方法。

[编辑2]

原来的例子被简化了。每个模块可以包含多种类型的导入,例如使用者和发布者。


灵活的解决方案,使用importlib。这是一个完整的、有效的解决方案,我已经测试过了。

首先,标题:

1
2
3
4
5
import importlib
parent = 'servicelib.simple'
modules = {'.synchronous':['.alternative', '.alternative_2']}
success = False #an indicator, default is False,
#changed to True when the import succeeds.

我们导入所需的模块,设置指示器,并指定模块。modules是一个字典,其键设置为默认模块,值作为可选选项列表。

接下来,导入Ant部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#Obtain the module
for default, alternatives in modules.items():
    try: #we will try to import the default module first
        mod = importlib.import_module(parent+default)
        success = True
    except ImportError: #the default module fails, try the alternatives
        for alt in alternatives:
            try: #try the first alternative, if it still fails, try the next one.
                mod = importlib.import_module(parent+alt)
                success = True
                #Stop searching for alternatives!
                break
            except ImportError:
                    continue

print 'Success: ', success

要想上这些课,只需:

1
2
Publisher = mod.Publisher
Consumer = mod.Consumer

有了这个解决方案,您可以同时拥有多个备选方案。例如,您可以同时使用rabbitpy和pyampq作为备选方案。

注意:适用于python 2和python 3。

如果您有更多问题,请随时发表评论和提问!


importlib.import_模块可以执行您需要的操作:

1
2
3
4
5
6
7
8
9
10
11
12
INSTALLED = ['syncronous', 'alternative']  

for mod_name in INSTALLED:
    try:
        module = importlib.import_module('servicelibrary.simple.' + mod_name)
        Publisher = getattr(module, 'Publisher')

        if Publisher:
            break  # found, what we needed

    except ImportError:
        continue

我想,这不是最先进的技术,但想法应该很清楚。你也可以看看IMP模块。


基于这些答案,我最终得到了下面的Python2.7实现。

StackOverflow的示例被简化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from importlib import import_module

PARENT = 'myservicelib.rabbitmq'
MODULES = ['test_adapter',
           'test_two_adapter']
SUCCESS = False

for _module in MODULES:
    try:
        __module = import_module('{0}.{1}'.format(PARENT, _module))
        Consumer = getattr(__module, 'Consumer')
        Publisher = getattr(__module, 'Publisher')
        SUCCESS = True
        break
    except ImportError:
        pass

if not SUCCESS:
    raise NotImplementedError('no supported rabbitmq library installed.')

尽管如此,因为我的一些项目运行的是python 2.6,所以我不得不修改代码,或者包含importlib。生产平台的问题是,包含新的依赖项并不总是容易的。

这是我提出的折衷方案,基于__import__,而不是importlib

可能需要检查sys.modules是否确实包含命名空间,这样就不会引发KeyError,但这不太可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys

PARENT = 'myservicelib.rabbitmq'
MODULES = ['test_adapter',
           'test_two_adapter']
SUCCESS = False

for _module in MODULES:
    try:
        __module_namespace = '{0}.{1}'.format(PARENT, _module)
        __import__(__module_namespace)
        __module = sys.modules[__module_namespace]
        Consumer = getattr(__module, 'Consumer')
        Publisher = getattr(__module, 'Publisher')
        SUCCESS = True
        break
    except ImportError:
        pass

if not SUCCESS:
    raise NotImplementedError('no supported rabbitmq library installed.')


我知道两种方法,一种被广泛使用,另一种是我的猜测。你可以根据自己的情况选择一个。

第一种,广泛使用,如from tornado.concurrent import Future

1
2
3
4
5
6
7
8
9
10
11
try:
    from concurrent import futures
except ImportError:
    futures = None

#define _DummyFuture balabala...

if futures is None:
    Future = _DummyFuture
else:
    Future = futures.Future

然后您可以在其他文件中使用from tornado.concurrent import Future

第二个,这是我的猜测,我写了简单的演示,但我没有在生产环境中使用它,因为我不需要它。

1
2
3
4
5
6
import sys
try:
    import servicelibrary.simple.synchronous
except ImportError:
    import servicelibrary.simple.alternative
    sys.modules['servicelibrary.simple.synchronous'] = servicelibrary.simple.alternative

您可以在其他脚本import servicelibrary.simple.synchronous之前运行该脚本。然后您可以像以前一样使用脚本:

1
2
from servicelibrary.simple.synchronous import Publisher
from servicelibrary.simple.synchronous import Consumer

我唯一想知道的是,我的猜测是什么。


你的想法是对的。因为每个子对象都有相同类型的类,例如,两个API都有一个名为Publisher的类,您只需确保导入了正确的版本即可。

如果这不是真的(如果可能的话,实现A和B不相似),您可以编写自己的Facade,它只是您自己的简单API,然后使用该库的正确方法/参数调用真正的API。

显然,在选择之间切换可能需要一些开销(我不知道您的情况,但例如,假设您有两个库来遍历打开的文件,库处理打开的文件。您不能只切换到文件中间的第二个库,并期望它从第一个库停止的地方开始)。但这只是拯救它的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
accessmethods = {}
try:
    from modA.modB import classX as apiA_classX
    from modA.modB import classY as apiA_classY
    accessmethods['apiA'] = [apiA_classX, apiA_classY]
    classX = apiA_classX
    classY = apiA_classY
except:
    pass

try:
    from modC.modD import classX as apiB_classX
    from modC.modD import classY as apiB_classY
    accessmethods['apiB'] = [apiB_classX, apiB_classY]
    classX = apiB_classX
    classY = apiB_classY
except:
    pass

def switchMethod(method):
    global classX
    global classY
    try:
        classX, classY = accessmethods[method]
    except KeyError as e:
        raise ValueError, 'Method %s not currently available'%method

等。