42

我正在 github 上写一个软件。它基本上是一个带有一些额外功能的托盘图标。我想提供一段工作代码,而实际上不必让用户安装本质上是可选功能的依赖项,而且我实际上不想导入我不会使用的东西,所以我认为这样的代码将是“好的解决方案”:

---- IN LOADING FUNCTION ----
features = []

for path in sys.path:
       if os.path.exists(os.path.join(path, 'pynotify')):
              features.append('pynotify')
       if os.path.exists(os.path.join(path, 'gnomekeyring.so')):
              features.append('gnome-keyring')

#user dialog to ask for stuff
#notifications available, do you want them enabled?
dlg = ConfigDialog(features)

if not dlg.get_notifications():
    features.remove('pynotify')


service_start(features ...)

---- SOMEWHERE ELSE ------

def service_start(features, other_config):

        if 'pynotify' in features:
               import pynotify
               #use pynotify...

不过也有一些问题。如果用户格式化他的机器并安装他的操作系统的最新版本并重新部署这个应用程序,功能会突然消失而没有警告。解决方案是在配置窗口中显示:

if 'pynotify' in features:
    #gtk checkbox
else:
    #gtk label reading "Get pynotify and enjoy notification pop ups!"

但是,如果这是一个 mac,我怎么知道我没有让用户去寻找他们永远无法填补的依赖项?

第二个问题是:

if os.path.exists(os.path.join(path, 'gnomekeyring.so')):

问题。我可以确定该文件在所有 Linux 发行版中始终称为 gnomekeyring.so 吗?

其他人如何测试这些功能?基本问题

try:
    import pynotify
except:
    pynotify = disabled

是代码是全球性的,这些可能会乱七八糟,即使用户不想要 pynotify....无论如何它都已加载。

那么人们认为解决这个问题的最好方法是什么?

4

4 回答 4

55

try:方法不需要是全局的——它可以在任何范围内使用,因此模块可以在运行时“延迟加载”。例如:

def foo():
    try:
        import external_module
    except ImportError:
        external_module = None 

    if external_module:
        external_module.some_whizzy_feature()
    else:
        print("You could be using a whizzy feature right now, if you had external_module.")

当您的脚本运行时,不会尝试加载external_module. 第一次foo()被调用,external_module被(如果可用)加载并插入到函数的本地范围内。随后调用foo()重新external_module插入其范围,而无需重新加载模块。

一般来说,最好让 Python 处理导入逻辑——它已经这样做了一段时间。:-)

于 2009-02-18T22:05:59.867 回答
16

您可能想看看imp 模块,它基本上可以完成您在上面手动执行的操作。因此,您可以首先查找一个模块,find_module()然后通过load_module()或简单地导入它来加载它(在检查配置之后)。

顺便说一句,如果使用 except: 我总是会向它添加特定的异常(这里是 ImportError),以免意外捕获不相关的错误。

于 2009-02-18T22:09:38.830 回答
3

不确定这是否是一种好习惯,但我创建了一个执行可选导入(使用importlib)和错误处理的函数:

def _optional_import(module: str, name: str = None, package: str = None):
    import importlib
    try:
        module = importlib.import_module(module)
        return module if name is None else getattr(module, name)
    except ImportError as e:
        if package is None:
            package = module
        msg = f"install the '{package}' package to make use of this feature"
        raise ValueError(msg) from e

如果可选模块不可用,用户至少会知道该怎么做。例如

# code ...

if file.endswith('.json'):
    from json import load
elif file.endswith('.yaml'):
    # equivalent to 'from yaml import safe_load as load'
    load = _optional_import('yaml', 'safe_load', package='pyyaml')

# code using load ...

这种方法的主要缺点是您的导入必须在线完成,并且并非全部位于文件的顶部。因此,使用此函数的轻微改编可能被认为是更好的做法(假设您正在导入一个函数等):

def _optional_import_(module: str, name: str = None, package: str = None):
    import importlib
    try:
        module = importlib.import_module(module)
        return module if name is None else getattr(module, name)
    except ImportError as e:
        if package is None:
            package = module
        msg = f"install the '{package}' package to make use of this feature"
        import_error = e

        def _failed_import(*args):
            raise ValueError(msg) from import_error

        return _failed_import

现在,您可以使用其余的导入进行导入,并且只有在实际使用导入失败的函数时才会引发错误。例如

from utils import _optional_import_  # let's assume we import the function
from json import load as json_load
yaml_load = _optional_import_('yaml', 'safe_load', package='pyyaml')

# unimportant code ...

with open('test.txt', 'r') as fp:
    result = yaml_load(fp)    # will raise a value error if import was not successful

PS:抱歉回复晚了!

于 2020-08-06T17:32:05.440 回答
-2

处理不同特性的不同依赖关系问题的一种方法是将可选特性实现为插件。这样,用户可以控制在应用程序中激活了哪些功能,但不负责自己跟踪依赖项。然后在每个插件安装时处理该任务。

于 2009-02-19T12:49:09.357 回答