我希望开始在工作中的大量基于 Python 的项目中使用 DBC,并且想知道其他人有哪些使用它的经验。到目前为止,我的研究结果如下:
我的问题是:您是否将DBC与Python一起用于成熟的生产代码?效果如何/值得付出努力吗?您会推荐哪些工具?
您发现的 PEP 尚未被接受,因此没有标准或可接受的方法来执行此操作(但是 - 您始终可以自己实施 PEP!)。然而,正如您所发现的,有几种不同的方法。
最轻量级的可能就是简单地使用 Python 装饰器。 Python 装饰器库中有一组用于前置/后置条件的装饰器,使用起来非常简单。这是该页面的一个示例:
>>> def in_ge20(inval):
... assert inval >= 20, 'Input value < 20'
...
>>> def out_lt30(retval, inval):
... assert retval < 30, 'Return value >= 30'
...
>>> @precondition(in_ge20)
... @postcondition(out_lt30)
... def inc(value):
... return value + 1
...
>>> inc(5)
Traceback (most recent call last):
...
AssertionError: Input value < 20
现在,你提到了类不变量。这些有点困难,但我的方法是定义一个可调用来检查不变量,然后在每个方法调用结束时使用后置条件装饰器之类的东西检查不变量。作为第一次切割,您可能可以按原样使用后置条件装饰器。
根据我的经验,即使没有语言支持,按合同设计也是值得做的。对于未设计为被重写的方法,断言和文档字符串足以满足前置条件和后置条件。对于设计为可重写的方法,我们将方法分为两部分:检查前置条件和后置条件的公共方法,以及提供实现并可能被子类重写的受保护方法。这是后者的一个例子:
class Math:
def square_root(self, number)
"""
Calculate the square-root of C{number}
@precondition: C{number >= 0}
@postcondition: C{abs(result * result - number) < 0.01}
"""
assert number >= 0
result = self._square_root(number)
assert abs(result * result - number) < 0.01
return result
def _square_root(self, number):
"""
Abstract method for implementing L{square_root()}
"""
raise NotImplementedError()
我从软件工程电台的关于按合同设计的一集中得到了平方根作为按合同设计的一般示例。他们还提到了语言支持的必要性,声称断言无助于执行里氏替换原则,尽管我上面的例子旨在证明相反的情况。我还应该提到 C++ pimpl(私有实现)习惯用法作为灵感来源,尽管它有完全不同的目的。
在我的工作中,我最近将这种契约检查重构为更大的类层次结构(契约已经记录在案,但没有进行系统测试)。现有的单元测试显示合同多次被违反。我只能得出结论,这应该是很久以前就应该完成的,并且一旦应用按合同设计,单元测试覆盖率会带来更多回报。我希望尝试这种技术组合的任何人都会做出相同的观察。
更好的工具支持可能会在未来为我们提供更强大的力量;我对此表示欢迎。
我们想在我们的生产代码中使用前置/后置条件/不变量,但发现所有当前的契约设计库都缺乏信息性消息和正确的继承。
因此我们开发了icontract。通过重新遍历函数的反编译代码并评估所有涉及的值,会自动生成错误消息:
import icontract
>>> class B:
... def __init__(self) -> None:
... self.x = 7
...
... def y(self) -> int:
... return 2
...
... def __repr__(self) -> str:
... return "instance of B"
...
>>> class A:
... def __init__(self)->None:
... self.b = B()
...
... def __repr__(self) -> str:
... return "instance of A"
...
>>> SOME_GLOBAL_VAR = 13
>>> @icontract.pre(lambda a: a.b.x + a.b.y() > SOME_GLOBAL_VAR)
... def some_func(a: A) -> None:
... pass
...
>>> an_a = A()
>>> some_func(an_a)
Traceback (most recent call last):
...
icontract.ViolationError:
Precondition violated: (a.b.x + a.b.y()) > SOME_GLOBAL_VAR:
SOME_GLOBAL_VAR was 13
a was instance of A
a.b was instance of B
a.b.x was 7
a.b.y() was 2
我们发现该库在生产(由于信息丰富的消息)和开发过程中(因为它允许您尽早发现错误)都非常有用。