我正在研究实现科学模型的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 ...
语句这些选项应该有效,但它们似乎都很笨拙且难以维护。如果我们想完全放弃依赖怎么办?或强制要求?
答案 0 :(得分:4)
最简单的解决方案当然是简单地在需要它们的函数体中导入可选的依赖项。但总是正确的PEP 8
说:
导入总是放在文件的顶部,就在任何模块之后 注释和文档字符串,以及模块全局和常量之前。
不想违背蟒蛇大师的美好愿望,我采取以下方法,这有几个好处......
假设我的一个函数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
存在的代码,但保留它不会导致程序中断。
只需删除先前需要它的任何函数中的依赖项检查。如果你使用装饰器实现了依赖性检查,你可以改变它,这样它就可以直接传递原始函数。
这种方法的好处是可以将所有导入放在模块的顶部,这样我就可以一目了然地看到需要什么和什么是可选的。
答案 1 :(得分:0)
我会使用mixin风格组成一个类。将可选行为保存在单独的类中,并在主类中继承这些类。如果您检测到无法执行可选行为,请改为创建一个虚拟mixin类。例如:
model.py
import numpy
import plotting
class Model(PrimaryBaseclass, plotting.Plotter):
def do_something(self):
...
plotting.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):
...