仅当我的模块需要某个对象时,如何触发另一个模块的延迟导入?

How to trigger a late import of another module only when a certain object is needed from my module?

情况

我想要一个大致如下工作的模块:

# my_module.py

my_number = 17

from other_module import foo
my_object = foo(23)

但是,存在一个问题:安装 other_module 会给某些用户带来问题,并且只有那些想要使用 my_object 的用户才需要 - 而这又只是一小部分用户。我想让那些不需要 my_object 的用户免于安装 other_module.

因此,我希望仅当从 my_module 导入 my_object 时才会导入 other_module。换句话说,用户应该能够 运行 以下内容而无需安装 other_module:

from my_module import my_number

目前我的最佳解决方案

我可以通过包含导入的函数提供 my_object

# in my_module.py

def get_my_object():
    from other_module import foo
    my_object = foo(23)
    return my_object

然后用户必须执行如下操作:

from my_module import get_my_object
my_object = get_my_object()

问题

是否有更好的方法来有条件地触发 other_module 的导入?我最感兴趣的是让用户尽可能简单。

方法 A – 清洁解决方案

创建一个新的独立模块并让用户从另一个模块导入对象。例如

from my_module import my_number # regular use
from my_module.extras import my_object # for a small part of the user base

这意味着在您的代码中,您创建了一个模块文件夹 my_module,其中包含一个 __init__.py,您可以在其中导入常用内容而不导入 extras 子模块。

如果您不想将 extras 放入 my_module(更简单),只需在单独的 extras.py 模块中创建 my_object

方法 B – 99% 的时间都表明架构不好

您可以使用 importlib.import_moduleget_my_object 中动态导入模块而不会污染全局 space 并且 比在创建函数的函数中导入更干净副作用,例如使用该导入名称覆盖您的全局变量(参见),但是这通常其他部分的错误编码模式的标志代码。

方法 C – 简单有效

当有些用户可能没有库时,我通常倾向于使用这种简单的模式,因为 Python 3 不鼓励不在顶级的导入:

try:
    import other_module
    _HAS_OTHER_MODULE_ = True
except:
    _HAS_OTHER_MODULE_ = False


def get_my_object():
    assert _HAS_OTHER_MODULE_, "Please install other module"
    return other_module.foo(23)

我更喜欢 get_my_object() 方法,但是从 Python 3.7 开始,您可以通过定义 module-level __getattr__ function:

# my_module.py

my_number = 17

def __getattr__(name):
    if name != 'my_object':
        raise AttributeError
    global my_object
    from other_module import foo
    my_object = foo(23)
    return my_object

这将尝试导入 other_module 并仅在访问 my_object 时调用 foo。一些注意事项:

  • 它将不会触发尝试通过my_module内的全局变量查找访问my_object。它只会在 my_module.my_object 属性访问或 from my_module import my_object 导入(在后台执行属性访问)时触发。
  • 如果您忘记在 __getattr__ 中分配全局 my_object 名称,则每次访问时都会重新计算 my_object
  • Module-level __getattr__ 在 Python 3.7 之前什么都不做,因此您可能想要执行版本检查并为 Python 3.6 做一些其他事情及以下:

    import sys
    if sys.version_info >= (3, 7):
        def __getattr__(name):
            ...
    else:
        # Do something appropriate. Maybe raise an error. Maybe unconditionally
        # import other_module and compute my_object up front.
    

这是一个常见的技巧:

import sys

class MockModule:
    def __init__(self, module):
        self.module = module

    def __getattr__(self, attr):
        if attr == 'dependency_required_var':
            try:
                import foo
                return self.module.dependency_required_var
            except ImportError:
                raise Exception('foo library is required to use dependency_required_var')
        else:
            return getattr(self.module, attr)


MockModule.__name__ = __name__
sys.modules[__name__] = MockModule(sys.modules[__name__])

dependency_required_var = 0

使用 this PEP,我们可以在 Python 3.7 及更高版本中简单地执行(我们应该能够但我无法让它工作)以下操作:

def __getattr__(attr):
    if attr == 'dependency_required_var':
        try:
            import foo
            return dependency_required_var
        except ImportError:
            raise Exception('foo library is required to use dependency_required_var')
    else:
        return globals()[attr]

PEP貌似被采纳了,但是PEP的相关pull request貌似关闭了,具体有没有实现我也不确定。