Python合同的一个简单实现。
项目描述
简介
此模块提供了一组装饰器,使得编写使用合同编写软件变得简单。
合同是一种调试和验证工具。它们是关于程序在运行时必须处于何种状态的声明性语句,以被认为是“正确”的。它们类似于断言,并在程序中定义良好的各个点上自动验证。合同可以指定在函数和类上。
合同作为文档的一种形式,以及正式指定程序行为的一种方式。良好的实践通常包括首先编写所有合同,这些合同指定了每个函数或方法调用之前和之后的精确预期状态,以及给定类对象应该始终为真的事物。
合同由两部分组成:描述和条件。描述是简单地描述合同正在测试的内容的可读字符串,而条件是测试该条件的单个函数。条件将自动执行并传递某些参数(这些参数取决于合同类型),必须返回布尔值:如果条件已满足,则返回True,否则返回False。
对旧版Python的支持
此模块支持Python >= 3.5的版本;即支持“async def”函数的版本。此模块有一个分支与Python 3.5之前的版本(包括Python 2.7)尽可能保持兼容。
Python 2和<= 3.5的分支可在以下位置找到:https://github.com/deadpixi/contracts/tree/python2
此与旧版本兼容的版本也随0.5.x分支发布在PyPI上;这个分支将尽可能与较新版本保持兼容。
该分支是该模块的直接替代品,包含除了对“async def”函数的支持之外的所有功能。
前置条件和后置条件
函数的契约由前置条件和后置条件组成。前置条件使用requires装饰器声明,描述函数进入时必须满足的条件。条件函数传递一个参数对象,其属性为装饰的函数的参数
>>> @require("`i` must be an integer", lambda args: isinstance(args.i, int)) ... @require("`j` must be an integer", lambda args: isinstance(args.j, int)) ... def add2(i, j): ... return i + j
注意,可以堆叠任意数量的前置条件。
这些装饰器已声明参数的类型必须是整数。用正确的参数类型调用add2函数可以正常工作
>>> add2(1, 2) 3
但是,使用错误的参数类型(违反了契约)将因PreconditionError(AssertionError的子类型)而失败
>>> add2("foo", 2) Traceback (most recent call last): PreconditionError: `i` must be an integer
函数还可以有后置条件,使用ensure装饰器指定。后置条件描述函数成功返回后必须满足的条件。与require装饰器一样,ensure装饰器也传递一个参数对象。它还传递一个额外的参数,即函数调用的结果。例如
>>> @require("`i` must be a positive integer", ... lambda args: isinstance(args.i, int) and args.i > 0) ... @require("`j` must be a positive integer", ... lambda args: isinstance(args.j, int) and args.j > 0) ... @ensure("the result must be greater than either `i` or `j`", ... lambda args, result: result > args.i and result > args.j) ... def add2(i, j): ... if i == 7: ... i = -7 # intentionally broken for purposes of example ... return i + j
我们现在可以调用该函数并确保一切正常工作
>>> add2(1, 3) 4
但该函数以意想不到的方式损坏了
>>> add2(7, 4) Traceback (most recent call last): PostconditionError: the result must be greater than either `i` or `j`
指定条件的函数不必是lambda;它可以是任何函数,前置条件和后置条件实际上不必引用函数的参数或结果。它们可以简单地检查函数的环境和效果
>>> names = set() >>> def exists_in_database(x): ... return x in names >>> @require("`name` must be a string", lambda args: isinstance(args.name, str)) ... @require("`name` must not already be in the database", ... lambda args: not exists_in_database(args.name.strip())) ... @ensure("the normalized version of the name must be added to the database", ... lambda args, result: exists_in_database(args.name.strip())) ... def add_to_database(name): ... if name not in names and name != "Rob": # intentionally broken ... names.add(name.strip())>>> add_to_database("James") >>> add_to_database("Marvin") >>> add_to_database("Marvin") Traceback (most recent call last): PreconditionError: `name` must not already be in the database >>> add_to_database("Rob") Traceback (most recent call last): PostconditionError: the normalized version of the name must be added to the database
支持Python的所有各种调用约定
>>> @require("`a` is an integer", lambda args: isinstance(args.a, int)) ... @require("`b` is a string", lambda args: isinstance(args.b, str)) ... @require("every member of `c` should be a boolean", ... lambda args: all(isinstance(x, bool) for x in args.c)) ... def func(a, b="Foo", *c): ... pass>>> func(1, "foo", True, True, False) >>> func(b="Foo", a=7) >>> args = {"a": 8, "b": "foo"} >>> func(**args) >>> args = (1, "foo", True, True, False) >>> func(*args) >>> args = {"a": 9} >>> func(**args) >>> func(10)
一个常见的契约是验证参数的类型。为此,还有一个额外的装饰器types,可以用来验证参数的类型
>>> class ExampleClass: ... pass>>> @types(a=int, b=str, c=(type(None), ExampleClass)) # or types.NoneType, if you prefer ... @require("a must be nonzero", lambda args: args.a != 0) ... def func(a, b, c=38): ... return " ".join(str(x) for x in [a, b])>>> func(1, "foo", ExampleClass()) '1 foo'>>> func(1.0, "foo", ExampleClass) # invalid type for `a` Traceback (most recent call last): PreconditionError: the types of arguments must be valid>>> func(1, "foo") # invalid type (the default) for `c` Traceback (most recent call last): PreconditionError: the types of arguments must be valid
类契约
require和ensure装饰器可以用于类方法,而不仅仅是裸函数
>>> class Foo: ... @require("`name` should be nonempty", lambda args: len(args.name) > 0) ... def __init__(self, name): ... self.name = name>>> foo = Foo() Traceback (most recent call last): TypeError: __init__ missing required positional argument: 'name'>>> foo = Foo("") Traceback (most recent call last): PreconditionError: `name` should be nonempty
类还可以有对其指定的一种额外的契约:不变量。不变量是通过使用invariant装饰器创建的,指定了该类实例必须始终满足的条件。在这种情况下,“始终”意味着“在任何方法调用之前和返回之后” - 方法可以违反不变量,只要在返回之前恢复即可。
不变量契约传递一个变量,即类的实例的引用。例如
>>> @invariant("inner list can never be empty", lambda self: len(self.lst) > 0) ... @invariant("inner list must consist only of integers", ... lambda self: all(isinstance(x, int) for x in self.lst)) ... class NonemptyList: ... @require("initial list must be a list", lambda args: isinstance(args.initial, list)) ... @require("initial list cannot be empty", lambda args: len(args.initial) > 0) ... @ensure("the list instance variable is equal to the given argument", ... lambda args, result: args.self.lst == args.initial) ... @ensure("the list instance variable is not an alias to the given argument", ... lambda args, result: args.self.lst is not args.initial) ... def __init__(self, initial): ... self.lst = initial[:] ... ... def get(self, i): ... return self.lst[i] ... ... def pop(self): ... self.lst.pop() ... ... def as_string(self): ... # Build up a string representation using the `get` method, ... # to illustrate methods calling methods with invariants. ... return ",".join(str(self.get(i)) for i in range(0, len(self.lst)))>>> nl = NonemptyList([1,2,3]) >>> nl.pop() >>> nl.pop() >>> nl.pop() Traceback (most recent call last): PostconditionError: inner list can never be empty>>> nl = NonemptyList(["a", "b", "c"]) Traceback (most recent call last): PostconditionError: inner list must consist only of integers
在以下情况下忽略违反不变量
在调用__init__和__new__之前(因为对象仍在初始化中)
在任何以“__”开头的方法的调用之前和之后(除了实现算术和比较操作以及容器类型模拟的方法,因为这些方法是私有的,并期望操作对象的内部状态,并且与某些__getattr(attribute)__的应用有关)
在任何从初始类定义外部添加的方法的调用之前和之后(因为不变量仅在类定义时处理)
在调用类方法之前和之后,因为它们适用于整个类而不是任何特定实例
例如
>>> @invariant("`always` should be True", lambda self: self.always) ... class Foo: ... always = True ... ... def get_always(self): ... return self.always ... ... @classmethod ... def break_everything(cls): ... cls.always = False>>> x = Foo() >>> x.get_always() True >>> x.break_everything() >>> x.get_always() Traceback (most recent call last): PreconditionError: `always` should be True
注意,如果一个方法在同一个对象上调用另一个方法,所有的不变量都将再次测试
>>> nl = NonemptyList([1,2,3]) >>> nl.as_string() == '1,2,3' True
在契约中转换数据
一般来说,你应该避免在契约内部转换数据;契约本身应该是无副作用的。
然而,在Python中这并不总是可能的。
以传递给函数的迭代器为例。我们可能希望验证给定集合中的每个元素都满足某些属性。显然的解决方案可能如下所示:
>>> @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l)) ... def my_func(l): ... return sum(l)
这在大多数情况下都有效
>>> my_func([1, 2, 3]) 6 >>> my_func([0, -1, 2]) Traceback (most recent call last): PreconditionError: every item in `l` must be > 0
但在生成器的情况下会失败
>>> def iota(n): ... for i in range(1, n): ... yield i>>> sum(iota(5)) 10 >>> my_func(iota(5)) 0
my_func 调用的结果为 0,因为在合同中的 all 调用中已经消耗了生成器。显然,这存在问题。
遗憾的是,没有通用的解决方案。在静态类型语言中,编译器可以验证无限列表的一些属性(尽管不是全部,具体取决于类型系统)。
在这里,我们通过使用一个名为 transform 的额外装饰器来克服这一限制,该装饰器将函数的参数进行转换,以及一个名为 rewrite 的函数来重写参数元组。
例如
>>> @transform(lambda args: rewrite(args, l=list(args.l))) ... @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l)) ... def my_func(l): ... return sum(l) >>> my_func(iota(5)) 10
请注意,这并不能完全解决无限序列的问题,但它确实允许验证无限序列的任何前缀。
当然,这也适用于类方法
>>> class TestClass: ... @transform(lambda args: rewrite(args, l=list(args.l))) ... @require("every item in `l` must be > 0", lambda args: all(x > 0 for x in args.l)) ... def my_func(self, l): ... return sum(l) >>> TestClass().my_func(iota(5)) 10
异步函数(即协程函数)上的合同
可以在协程(即异步函数)上放置合同
>>> import asyncio >>> @require("`a` is an integer", lambda args: isinstance(args.a, int)) ... @require("`b` is a string", lambda args: isinstance(args.b, str)) ... @require("every member of `c` should be a boolean", ... lambda args: all(isinstance(x, bool) for x in args.c)) ... async def func(a, b="Foo", *c): ... await asyncio.sleep(1)>>> asyncio.get_event_loop().run_until_complete( ... func( 1, "foo", True, True, False))
断言函数本身不能是协程,因为这可能会影响运行循环
>>> async def coropred_aisint(e): ... await asyncio.sleep(1) ... return isinstance(getattr(e, 'a'), int) >>> @require("`a` is an integer", coropred_aisint) ... @require("`b` is a string", lambda args: isinstance(args.b, str)) ... @require("every member of `c` should be a boolean", ... lambda args: all(isinstance(x, bool) for x in args.c)) ... async def func(a, b="Foo", *c): ... await asyncio.sleep(1) Traceback (most recent call last): AssertionError: contract predicates cannot be coroutines
合同与调试
合同是文档和测试工具;它们不是用于验证用户输入或实现程序逻辑的。实际上,将 Python 的 __debug__ 设置为 False(例如,通过使用带有“-O”选项的 Python 解释器)将禁用合同。
测试此模块
当从命令行调用此模块时,该模块将运行嵌入的 doctests。只需直接运行模块即可运行测试。
联系信息和许可
此模块在 GitHub 上有一个主页:GitHub。
此模块由 Rob King 编写(jking@deadpixi.com)。
此程序是免费软件:您可以在自由软件基金会的 GNU 较小通用公共许可证的条款下重新分配它和/或修改它,许可证版本 3 或(根据您的选择)任何后续版本。
此程序分发时希望它是有用的,但没有保证;甚至没有关于适销性或特定用途适用性的暗示保证。有关详细信息,请参阅 GNU 较小通用公共许可证。
您应该已经收到此程序的 GNU 较小通用公共许可证副本。如果没有,请参阅 <https://gnu.ac.cn/licenses/>。
项目详情
dpcontracts-0.6.0.tar.gz 的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 6cf9df1f16beaa48523b798b41170dabf7a536a6133328731665cdb29c42234a |
|
MD5 | c35b2bb2dd2ec389dbd10f6f1d1b8dee |
|
BLAKE2b-256 | aae2cad64673297a634a623808045d416ed85bad1c470ccc99e0cdc7b13b9774 |