什么';s Python导入和提供可选功能的良好实践?

什么';s Python导入和提供可选功能的良好实践?,python,python-import,Python,Python Import,我正在github上写一个软件。它基本上是一个托盘图标,有一些额外的功能。我想提供一段工作代码,而不需要让用户安装可选功能的基本依赖项,我不想导入我不打算使用的东西,所以我认为这样的代码是“好的解决方案”: 然而,也存在一些问题。如果用户格式化他的机器并安装最新版本的操作系统并重新部署此应用程序,则功能会突然消失而不发出警告。解决方案是在配置窗口中显示: if 'pynotify' in features: #gtk checkbox else: #gtk label readi

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

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

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…它还是被加载了


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

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.")
运行脚本时,不会尝试加载
外部\u模块
。第一次调用
foo()。对
foo()
的后续调用将
external_module
重新插入其作用域,而无需重新加载模块


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

您可能想看看,它基本上完成了上面手动执行的操作。因此,您可以首先使用
find_module()
查找模块,然后通过
load_module()


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

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

不确定这是否是一个好的做法,但我创建了一个函数,用于执行可选导入(使用
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 ...
这种方法的主要缺点是,您的导入必须在线完成,并且不能全部放在文件的顶部。因此,使用此函数的轻微修改(假设您正在导入函数或类似函数)可能被认为是更好的做法:

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

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:很抱歉回答晚了

我认为
except importorror
块需要设置
external\u module=None
,否则当您尝试在if块中访问它时,您会得到
namererror
。很像try:except:(无异常类型),是一种反模式,捕获importorror可能不是您想要的。如果模块存在,但引发异常,您将捕获该异常并继续,从而隐藏错误。最好先检查模块是否存在,决定是否要导入,然后不处理导入。或者添加
external\u module=None
内容您可以在pandas库中找到类似的解决方案,该库具有许多可选功能。
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