在Python中使用契约式设计

在Python中使用契约式设计,python,design-by-contract,Python,Design By Contract,我希望在大量基于Python的项目中开始使用DBC,我想知道其他人在这方面有什么经验。到目前为止,我的研究结果如下: -PEP 316,它应该为Python标准化合同设计,但已被推迟。本PEP建议使用docstring -Python合同。这似乎是一个使用docstring的完整但未维护的框架 -PyDBC,它使用元类实现契约。也有几年没有保养 我的问题是:您是否将DBC与Python一起用于成熟的生产代码?它的效果如何/值得付出努力吗?你会推荐哪些工具 我没有在python中使用契约式设计

我希望在大量基于Python的项目中开始使用DBC,我想知道其他人在这方面有什么经验。到目前为止,我的研究结果如下:

  • -PEP 316,它应该为Python标准化合同设计,但已被推迟。本PEP建议使用docstring
  • -Python合同。这似乎是一个使用docstring的完整但未维护的框架
  • -PyDBC,它使用元类实现契约。也有几年没有保养

我的问题是:您是否将DBC与Python一起用于成熟的生产代码?它的效果如何/值得付出努力吗?你会推荐哪些工具

我没有在python中使用契约式设计,所以我无法回答您所有的问题。然而,我花了一些时间研究了这个库,它的最新版本最近已经发布,看起来非常不错


中有一些关于此库的讨论。

您找到的PEP尚未被接受,因此没有一种标准或公认的方法来实现这一点(但是--您始终可以自己实现PEP!)。然而,正如您所发现的,有几种不同的方法

最轻量级的可能只是简单地使用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
>>def在20(无效)中:
...    断言无效>=20,“输入值<20”
...
>>>def输出lt30(返回、无效):
...    断言retval<30,“返回值>=30”
...
>>>@前提条件(in_ge20)
... @后置条件(输出lt30)
... def公司(价值):
...   返回值+1
...
>>>公司(5)
回溯(最近一次呼叫最后一次):
...
断言错误:输入值<20
现在,你提到了类不变量。这有点困难,但我要做的是定义一个可调用的方法来检查不变量,然后在每个方法调用结束时让类似于post-condition decorator的东西来检查不变量。作为第一个切入点,您可能只需按原样使用postcondition decorator。

虽然不完全按合同设计,但一些测试框架支持属性测试方法,并且在概念上非常接近

随机测试某些属性是否在运行时保持,可以轻松检查:

  • 不变量
  • 输入和输出值的域
  • 其他前置和后置条件
对于Python,有一些QuickCheck风格的测试框架:


以我的经验,即使没有语言支持,按合同设计也是值得的。对于未被重写断言的方法,以及docstring对于前置条件和后置条件都是足够的。对于被重写的方法,我们将该方法分为两部分:一部分是检查前置和后置条件的公共方法,另一部分是提供实现的受保护方法,可以被子类重写。以下是后者的一个例子:

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()
课堂数学:
def平方根(自身,数字)
"""
计算C{number}的平方根
@前提条件:C{number>=0}
@后置条件:C{abs(结果*结果-数字)<0.01}
"""
断言编号>=0
结果=自平方根(数字)
断言abs(结果*结果-数字)<0.01
返回结果
定义平方根(自身,数字):
"""
实现L{square_root()}的抽象方法
"""
引发未实现的错误()
我从软件工程电台()上的一集“按合同设计”中得到了平方根作为按合同设计的一般示例。他们还提到了语言支持的必要性,因为断言对确保Liskov替换原则没有帮助,尽管我上面的示例旨在说明其他方面。我还应该提到C++ PIPML(私有实现)习语作为灵感来源,尽管这有一个完全不同的用途。 在我的工作中,我最近将这种契约检查重构到一个更大的类层次结构中(契约已经有文档记录,但没有经过系统测试)。现有的单元测试显示合同多次被违反。我只能得出这样的结论:这应该在很久以前就完成了,而且一旦契约式设计被应用,单元测试的覆盖率将得到更大的回报。我希望任何尝试这种技术组合的人都能做出同样的观察


更好的工具支持可能在将来为我们提供更多的功能,我对此表示欢迎。

我们希望在生产代码中使用前置/后置条件/不变量,但发现所有当前的契约式设计库都缺乏信息性消息和适当的继承

因此我们发展了。通过重新遍历函数的反编译代码并计算所有涉及的值,自动生成错误消息:

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

我们发现该库在生产过程中(由于信息性消息)和开发过程中(因为它允许您在早期发现bug)都非常有用。

请注意,您可以从TestCase继承,并在任何类中包含单元测试。对,但DBC有点不同,它将在生产和所有数据输入中运行检查。据我所知,单元测试是具有预定义数据集的运行时断言,而DBC是具有所有输入的断言之上的级别。明确地我认为在我的例子中使用DBC是有意义的,因为很多代码实际上都是重状态的,并且经常必须从外部数据库获取状态,该数据库具有频繁变化的模式和相当复杂的关系,这些关系对于模型来说非常混乱。契约式设计就是明确指定每段代码所遵循的规范。您不必在运行时对其进行完整测试。单元测试可以是该规范的一部分,也可以是其他规范的一部分。TDD是一个不同的概念