如何正确处理Python中的可选功能

问题描述 投票:0回答:3

我正在开发实现科学模型的Python包,我想知道处理可选功能的最佳方法是什么。 这是我想要的行为: 如果无法导入某些可选依赖项(例如,在无头机器上绘制模块),我想在我的类中禁用使用这些模块的功能,警告用户如果他尝试使用它们以及所有这些,而不会破坏执行。 所以下面的脚本在任何情况下都可以工作:

mymodel.dostuff()
mymodel.plot() <= only plots if possible, else display log an error 
mymodel.domorestuff() <= get executed regardless of the result of the previous statement

到目前为止,我看到的选项如下:

  • __init __.py
    中检查可用模块并保留以下模块的列表 它们(但如何在包的其余部分正确使用它?)
  • 对于每个依赖可选依赖项的函数都有一个
    try import ... 
    except ...
    声明
  • 将取决于特定模块的函数放入单独的文件中

这些选项应该有效,但它们似乎都相当老套且难以维护。如果我们想完全放弃依赖怎么办?或者强制执行?

python python-3.x
3个回答
33
投票

当然,最简单的解决方案是简单地将可选依赖项导入到需要它们的函数体中。但永远正确的

PEP 8
说:

导入始终放在文件顶部,紧接在任何模块之后 注释和文档字符串,以及模块全局变量和常量之前。

不想违背Python大师的良好愿望,我采取以下方法,它有几个好处......

首先,使用 try- except 导入

假设我的一个函数

foo
需要
numpy
,我想将其设为可选依赖项。在模块的顶部,我放置了:

try:
    import numpy as _numpy
except ImportError:
    _has_numpy = False
else:
    _has_numpy = True

这里(在 except 块中)是打印警告的地方,最好使用

warnings
模块。

然后在函数中抛出异常

如果用户调用

foo
并且没有numpy怎么办?我在那里抛出异常并记录此行为。

def foo(x):
    """Requires numpy."""
    if not _has_numpy:
        raise ImportError("numpy is required to do this.")
    ...

或者,您可以使用装饰器并将其应用于任何需要该依赖项的函数:

@requires_numpy
def foo(x):
    ...

这有防止代码重复的好处。

并将其作为可选依赖项添加到您的安装脚本中

如果您要分发代码,请了解如何将额外的依赖项添加到安装配置中。例如,使用

setuptools
,我可以写:

install_requires = ["networkx"],

extras_require = {
    "numpy": ["numpy"],
    "sklearn": ["scikit-learn"]}

这指定安装时绝对需要

networkx
,但我的模块的额外功能需要
numpy
sklearn
,它们是可选的。


使用这种方法,以下是您的具体问题的答案:

  • 如果我们想要强制依赖怎么办?

我们可以简单地将可选依赖项添加到设置工具的所需依赖项列表中。在上面的示例中,我们将

numpy
移动到
install_requires
。然后可以删除所有检查
numpy
是否存在的代码,但保留它不会导致程序中断。

  • 如果我们想完全放弃依赖怎么办?

只需删除对以前需要它的任何函数中的依赖项的检查即可。如果您使用装饰器实现了依赖项检查,您可以更改它,以便它简单地传递原始函数而不改变。

这种方法的好处是将所有导入都放在模块的顶部,这样我就可以一目了然地看到什么是必需的,什么是可选的。


0
投票

我会使用 mixin 风格来编写类。将可选行为保留在单独的类中,并在主类中对这些类进行子类化。如果您发现可选行为不可行,则创建一个虚拟 mixin 类。例如:

模型.py

import numpy
import plotting

class Model(PrimaryBaseclass, plotting.Plotter):
    def do_something(self):
        ...

绘图.py

from your_util_module import headless as _headless
__all__ = ["Plotter"]
if _headless:
    import warnings
    class Plotter:
        def plot(self):
            warnings.warn("Attempted to plot in a headless environment")
else:
    class Plotter:
        """Expects an attribute called `data' when plotting."""
        def plot(self):
            ...

或者,作为替代方案,使用装饰器来描述函数何时可能不可用。

例如。

class unavailable:

    def __init__(self, *, when):
        self.when = when

    def __call__(self, func):
       if self.when:
           def dummy(self, *args, **kwargs):
               warnings.warn("{} unavailable with current setup"
                   .format(func.__qualname__))
           return dummy
       else:
           return func

class Model:
    @unavailable(when=headless)
    def plot(self):
        ...

0
投票

根据jme的回答,我最终编写了一个参数化装饰器:

from functools import wraps
from importlib.util import find_spec


def depends_on_optional(module_name: str):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            spec = find_spec(module_name)
            if spec is None:
                raise ImportError(
                    f"Optional dependency {module_name} not found ({func.__name__})."
                )
            else:
                return func(*args, **kwargs)

        return wrapper

    return decorator

尽管出现找不到 matplotlib 的警告,但在运行时调用

ImportError
时,我仍使用它来获取
plot_with_matplotlib

import warnings

try:
    import matplotlib.pyplot as plt
except ImportError:
    warnings.warn("matplotlib not found, plotting functions will not work")

from my_decorator import depends_on_optional


@depends_on_optional("matplotlib.pyplot")
def plot_with_matplotlib():
    pass

为了获得 OP 想要的确切行为,您可以将警告移动到装饰器中代替

ImportError

我还根据

这个答案
改编了requirements.txt,并带有可选的依赖项:

matplotlib[matplotlib]

通过此设置,我不必自己跟踪任何导入状态(如

_has_matplotlib
)!

© www.soinside.com 2019 - 2024. All rights reserved.