【问题标题】:Using Design by Contract in Python在 Python 中使用契约式设计
【发布时间】:2011-12-19 15:23:32
【问题描述】:

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

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

【问题讨论】:

  • 请注意,您可以只从 TestCase 继承,并在任何类中包含单元测试。
  • 对,但是 DBC 有点不同,它会在生产环境和所有数据输入上运行检查。据我了解,单元测试是具有预定义数据集的运行时断言,而 DBC 是具有所有输入的高于级别的断言。具体来说,我认为在我的案例中使用 DBC 是有意义的,因为很多代码确实是状态繁重的,并且经常必须从具有频繁更改模式和相当复杂的关系的外部 DB 获取状态,这些关系非常混乱,难以模拟.
  • 契约式设计是您明确指定每段代码符合的规范的地方。您不必在运行时对其进行全面测试。单元测试可以是那个规范,就像其他任何东西一样。 TDD 是一种使用单元测试的不同方式,在这种情况下可以对一组预期的行为进行建模。
  • 我明白这一点。在我的情况下,TDD 有点混乱:从一个,有时是两个外部数据库中提取数据,其中大量数据以意想不到的方式相关并且不能轻易模拟。似乎 DBC 可能更合适,不必担心模拟数据不再是问题。
  • 哦,绝对!一个不能替代另一个。我只是想弄清楚下一步我们的努力方向,似乎 DBC 将在这一点上提供更大的投资回报率。当然,单元测试和 DBC 不会相互排斥,并且会一起有效。

标签: python design-by-contract


【解决方案1】:

您找到的 PEP 尚未被接受,因此没有标准或可接受的方式来执行此操作(但是 - 您始终可以自己实施 PEP!)。但是,正如您所发现的,有几种不同的方法。

可能最轻量级的就是简单地使用 Python 装饰器。 Python Decorator Library 中有一组用于前置/后置条件的装饰器,使用起来非常简单。这是该页面的示例:

  >>> 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

现在,您提到了类不变量。这些有点困难,但我要做的方式是定义一个可调用来检查不变量,然后在每个方法调用结束时使用后置条件装饰器检查该不变量。作为第一次切割,您可能可以按原样使用后置条件装饰器。

【讨论】:

  • 感谢您的回答。我了解这可用于在 Python 中实现 DBC 的方式。我想知道是否有人已经在我提到的任何库或任何其他库上取得了成功。问题不是“我如何实现 DBC?”更多的是“我应该打扰实施 DBC 吗?”。
  • @ipartola,因为 PEP 316 已被“推迟”,它没有被积极处理,但没有被拒绝。所以,如果你想把它向前推进,对 PyContract 做一些改进可能是一个很好的前进方向。我认为你可以假设 PyContract 的工作现在已经停滞不前。因此,在我看来,“我是否应该费心实施 DBC”的答案是“是”,这将是一个非常有用的补充,但这可能是主观的,因为 DBC 尚未在 Python 生态系统中广泛使用。
  • @jcollado 提到的链接中的 Covenant 库 (bitbucket.org/kisielk/covenant) 也具有不变量。
  • 如果示例代码抱怨“NameError: name 'precondition' is not defined”,我错过了什么?谢谢!
  • 这是不久前的@Jie,但我认为它依赖于从答案中提到的Python Decorator Library 导入一些代码。不过这是 Python 2,我已经有一段时间没有看过它了,所以你可能需要稍微调整一下代码!
【解决方案2】:

根据我的经验,即使没有语言支持,按合同设计也是值得的。对于未被覆盖的断言的方法,连同文档字符串对于前置条件和后置条件都足够了。对于被覆盖的方法,我们将方法分为两部分:一个检查前置条件和后置条件的公共方法,一个提供实现的受保护方法,并且可以被子类覆盖。这里是后者的一个例子:

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()

我从软件工程电台 (http://www.se-radio.net/2007/03/episode-51-design-by-contract/) 的按合同设计的一集中获得了平方根作为按合同设计的一般示例。他们还提到了语言支持的必要性,因为断言对确保 Liskov-substitution-principle 没有帮助,尽管我上面的示例旨在证明并非如此。我还应该提到 C++ pimpl(私有实现)习语作为灵感来源,尽管它有完全不同的目的。

在我的工作中,我最近将这种合同检查重构为更大的类层次结构(合同已经记录在案,但没有经过系统测试)。现有的单元测试显示合同被多次违反。我只能得出结论,这应该在很久以前就已经完成了,并且一旦应用了按合同设计,单元测试覆盖率就会得到更多回报。我希望任何尝试这种技术组合的人都能做出相同的观察。

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

【讨论】:

  • 很好的轶事,实际上回答了这个问题。
  • 如果我错了,请纠正我,但这似乎不适用于类不变量。您可以将它们包含在每个前置条件和后置条件中,但是当方法 (1) 破坏不变量 (2) 调用另一个检查不变量的方法 (3) 恢复不变量(这是允许的)时,这将返回误报。您可以采用从不自己调用公共方法,只调用私有助手的原则,但不会检查这些调用的前置条件和后置条件。
  • 如果重写私有实现更新前置或后置条件,它还需要您重写公共方法,但我认为在实践中这不是什么大不了的事,公平地说,我不知道其他提到的实现可以更好地处理这些问题。
  • 不,我的帖子只关注前置条件和后置条件。我没有提供在 Python 中强制执行类不变量的智能技巧的经验。不过,想出如何去做的想法并不难。
【解决方案3】:

我没有在 python 中使用契约式设计,所以我无法回答你所有的问题。不过,我花了一些时间看contracts library,它的最新版本最近发布了,看起来还不错。

reddit 有一些关于这个库的讨论。

【讨论】:

  • 这个看起来不错,但缺乏对 DBC 主要部分的支持:类不变量。不过我会记住的。
【解决方案4】:

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

因此我们开发了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

我们发现该库在生产(由于信息丰富)和开发过程中(因为它可以让您及早发现错误)都非常有用。

【讨论】:

  • 两年后,这似乎是最有趣的包,既积极维护,又提供大多数功能。不错!
【解决方案5】:

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

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

  • 不变量
  • 输入和输出值域
  • 其他前置条件和后置条件

对于 Python,有一些 QuickCheck 风格的测试框架:

【讨论】:

    猜你喜欢
    • 2011-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-25
    相关资源
    最近更新 更多