assert
将其用作标准代码的一部分而不是仅用于调试目的是否存在性能或代码维护问题?是
assert x >= 0, 'x is less than zero'
好于或差于
if x < 0: raise Exception, 'x is less than zero'
此外,是否有任何方法可以设置这样的业务规则
if x < 0 raise error
,总是在没有try/except/finally
so 的情况下进行检查,如果在整个代码中的任何时间x
小于 0,则会引发错误,例如,如果您assert x < 0
在函数的开头设置,则在函数内的任何位置哪里x
变得小于 0 引发异常?
15 回答
断言应该用于测试不应该发生的情况。目的是在程序状态损坏的情况下尽早崩溃。
异常应该用于可能发生的错误,并且您应该几乎总是创建自己的异常类。
例如,如果您正在编写一个从配置文件读取到 a 的函数,文件dict
中不正确的格式应该引发 a ConfigurationSyntaxError
,而您可以assert
不打算返回None
。
在您的示例中,如果x
是通过用户界面或外部来源设置的值,则最好例外。
如果x
仅由您自己在同一程序中的代码设置,请使用断言。
“assert”语句在编译优化时被删除。所以,是的,性能和功能都存在差异。
当前代码生成器在编译时请求优化时不会为断言语句发出代码。- Python 2 文档 Python 3 文档
如果您使用assert
实现应用程序功能,然后优化部署到生产,您将被“but-it-works-in-dev”缺陷所困扰。
当 x 在整个函数中变得小于零时,能够自动抛出错误。您可以使用类描述符。这是一个例子:
class LessThanZeroException(Exception):
pass
class variable(object):
def __init__(self, value=0):
self.__x = value
def __set__(self, obj, value):
if value < 0:
raise LessThanZeroException('x is less than zero')
self.__x = value
def __get__(self, obj, objType):
return self.__x
class MyClass(object):
x = variable()
>>> m = MyClass()
>>> m.x = 10
>>> m.x -= 20
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "my.py", line 7, in __set__
raise LessThanZeroException('x is less than zero')
LessThanZeroException: x is less than zero
四大目的assert
假设您与四个同事 Alice、Bernd、Carl 和 Daphne 一起处理 200,000 行代码。他们调用你的代码,你调用他们的代码。
然后assert
有四个角色:
通知 Alice、Bernd、Carl 和 Daphne 您的代码需要什么。
假设您有一个处理元组列表的方法,并且如果这些元组不是不可变的,则程序逻辑可能会中断:def mymethod(listOfTuples): assert(all(type(tp)==tuple for tp in listOfTuples))
这比文档中的等效信息更值得信赖,并且更易于维护。
通知计算机您的代码期望什么。
assert
强制执行代码调用者的正确行为。如果您的代码调用 Alices 的代码而 Bernd 的代码调用您的代码,那么如果没有assert
,如果程序在 Alices 代码中崩溃,Bernd 可能会认为这是 Alice 的错,Alice 会调查并可能会认为是你的错,你调查并告诉 Bernd 这实际上是他的。很多工作丢失了。
有了断言,无论谁接错电话,他们很快就能看出这是他们的错,而不是你的错。爱丽丝、伯恩德和你们都会受益。节省大量时间。告知您的代码的读者(包括您自己)您的代码在某个时候取得了什么成就。
假设您有一个条目列表,并且每个条目都可以是干净的(这很好),也可以是 smorsh、trale、gullup 或 twinkled(这些都是不可接受的)。如果是 smorsh,则必须是 unsmorshed;如果它是 trale,它必须是 baludoed;如果是 gullup,则必须小跑(然后也可能有节奏);如果它闪烁,则必须再次闪烁,周四除外。你明白了:这是复杂的东西。但最终结果是(或应该是)所有条目都是干净的。Right Thing(TM) 要做的是将清洁循环的效果总结为assert(all(entry.isClean() for entry in mylist))
这个陈述让每个试图理解美妙循环究竟是什么的人都头疼。这些人中最常见的可能是你自己。
通知计算机您的代码在某个时间点实现了什么。
如果您在小跑后忘记调整需要它的条目的速度,这assert
将节省您的时间并避免您的代码在很久以后破坏亲爱的 Daphne。
在我看来,assert
文档(1 和 3)和保障(2 和 4)的两个目的同样有价值。
通知人们甚至可能比通知计算机更有价值,因为它可以防止assert
目标捕获的错误(在案例 1 中)以及任何情况下的大量后续错误。
除了其他答案之外,断言本身抛出异常,但只有 AssertionErrors。从实用的角度来看,断言不适合当您需要对捕获的异常进行细粒度控制时。
这种方法唯一真正的错误是很难使用 assert 语句做出一个非常具有描述性的异常。如果您正在寻找更简单的语法,请记住您也可以执行以下操作:
class XLessThanZeroException(Exception):
pass
def CheckX(x):
if x < 0:
raise XLessThanZeroException()
def foo(x):
CheckX(x)
#do stuff here
另一个问题是使用 assert 进行正常的条件检查是很难使用 -O 标志禁用调试断言。
这里的英语单词assert有swear、firm、avow的意思。这并不意味着“检查”或“应该”。这意味着您作为编码员在这里宣誓:
# I solemnly swear that here I will tell the truth, the whole truth,
# and nothing but the truth, under pains and penalties of perjury, so help me FSM
assert answer == 42
如果代码是正确的,除了单事件扰乱、硬件故障等,任何断言都不会失败。这就是为什么程序对最终用户的行为不能受到影响的原因。特别是,即使在特殊的编程条件下,断言也不会失败。它只是永远不会发生。如果发生这种情况,程序员应该为此受到打击。
如前所述,当你的代码不应该达到某个点时,应该使用断言,这意味着那里有一个错误。我可以看到使用断言的最有用的原因可能是不变/前置/后置条件。这些都是在循环或函数的每次迭代开始或结束时必须为真的东西。
例如,一个递归函数(2 个单独的函数,其中 1 个处理错误的输入,另一个处理错误的代码,因为递归很难区分)。如果我忘记写 if 语句,这会很明显,出了什么问题。
def SumToN(n):
if n <= 0:
raise ValueError, "N must be greater than or equal to 0"
else:
return RecursiveSum(n)
def RecursiveSum(n):
#precondition: n >= 0
assert(n >= 0)
if n == 0:
return 0
return RecursiveSum(n - 1) + n
#postcondition: returned sum of 1 to n
这些循环不变量通常可以用断言来表示。
嗯,这是一个悬而未决的问题,我有两个方面想谈一谈:何时添加断言以及如何编写错误消息。
目的
向初学者解释它 - 断言是可能引发错误的语句,但您不会捕捉到它们。他们通常不应该被抚养,但在现实生活中,他们有时确实会被抚养。这是一个严重的情况,代码无法从中恢复,我们称之为“致命错误”。
接下来,它用于“调试目的”,虽然正确,但听起来很不屑一顾。我更喜欢“声明不变量,永远不应该被违反”的表述,尽管它对不同的初学者的工作方式不同......有些人“只是得到它”,而另一些人要么找不到任何用处,要么替换正常的异常,甚至用它来控制流量。
风格
在 Python 中,assert
是语句,而不是函数!(记住assert(False, 'is true')
不会加注。但是,不碍事:
何时以及如何编写可选的“错误消息”?
这实际上适用于单元测试框架,它通常有许多专门的方法来做断言(assertTrue(condition)
等assertFalse(condition), assertEqual(actual, expected)
)。它们通常还提供一种对断言进行评论的方法。
在一次性代码中,您可以在没有错误消息的情况下进行操作。
在某些情况下,没有什么可以添加到断言中:
def dump(something): assert isinstance(something, Dumpable) # ...
但除此之外,消息对于与其他程序员(有时是您的代码的交互式用户,例如在 Ipython/Jupyter 等中)的交流很有用。
给他们信息,而不仅仅是泄露内部实现细节。
代替:
assert meaningless_identifier <= MAGIC_NUMBER_XXX, 'meaningless_identifier is greater than MAGIC_NUMBER_XXX!!!'
写:
assert meaningless_identifier > MAGIC_NUMBER_XXX, 'reactor temperature above critical threshold'
甚至可能:
assert meaningless_identifier > MAGIC_NUMBER_XXX, f'reactor temperature({meaningless_identifier }) above critical threshold ({MAGIC_NUMBER_XXX})'
我知道,我知道 - 这不是静态断言的情况,但我想指出消息的信息价值。
负面信息还是正面信息?
这可能会引起争议,但读到以下内容让我很受伤:
assert a == b, 'a is not equal to b'
这是两个相互矛盾的东西。因此,每当我对代码库产生影响时,我都会通过使用“必须”和“应该”等额外动词来推动指定我们想要的东西,而不是说出我们不想要的东西。
断言 a == b, 'a 必须等于 b'
然后,gettingAssertionError: a must be equal to b
也是可读的,并且该语句在代码中看起来是合乎逻辑的。此外,您可以在不阅读回溯的情况下从中获得一些东西(有时甚至不可用)。
有性能问题吗?
请记住“先让它工作,然后再让它快速工作”。
任何程序中只有很少的百分比通常与其速度相关。如果它被证明是一个性能问题,你总是可以排除或简化assert
它——而且它们中的大多数永远不会。务实:
假设您有一个处理非空元组列表的方法,如果这些元组不是不可变的,则程序逻辑将中断。你应该写:def mymethod(listOfTuples): assert(all(type(tp)==tuple for tp in listOfTuples))
如果您的列表往往有 10 个条目,这可能很好,但如果它们有 100 万个条目,这可能会成为问题。但与其完全丢弃这张有价值的支票,你可以简单地将其降级为
def mymethod(listOfTuples): assert(type(listOfTuples[0])==tuple) # in fact _all_ must be tuples!
这很便宜,但无论如何都可能会捕获大多数实际的程序错误。
对于它的价值,如果您正在处理依赖于assert
正常运行的代码,那么添加以下代码将确保启用断言:
try:
assert False
raise Exception('Python assertions are not working. This tool relies on Python assertions to do its job. Possible causes are running with the "-O" flag or running a precompiled (".pyo" or ".pyc") module.')
except AssertionError:
pass
Assert 是检查 -
1. 有效条件,
2. 有效语句,
3. 真逻辑;
的源代码。它不会使整个项目失败,而是会发出警报,表明您的源文件中的某些内容不合适。
在示例 1 中,因为变量 'str' 不为空。所以不会引发任何断言或异常。
示例 1:
#!/usr/bin/python
str = 'hello Python!'
strNull = 'string is Null'
if __debug__:
if not str: raise AssertionError(strNull)
print str
if __debug__:
print 'FileName '.ljust(30,'.'),(__name__)
print 'FilePath '.ljust(30,'.'),(__file__)
------------------------------------------------------
Output:
hello Python!
FileName ..................... hello
FilePath ..................... C:/Python\hello.py
在示例 2 中,var 'str' 为空。因此,我们通过断言语句使用户免于执行错误程序。
示例 2:
#!/usr/bin/python
str = ''
strNull = 'NULL String'
if __debug__:
if not str: raise AssertionError(strNull)
print str
if __debug__:
print 'FileName '.ljust(30,'.'),(__name__)
print 'FilePath '.ljust(30,'.'),(__file__)
------------------------------------------------------
Output:
AssertionError: NULL String
在我们不想调试并意识到源代码中的断言问题的那一刻。禁用优化标志
python -O assertStatement.py
什么都不会打印
异常的使用assert
和引发都与通信有关。
断言是关于开发人员处理的代码正确性的声明:代码中的断言通知代码的读者关于代码正确必须满足的条件。运行时失败的断言会通知开发人员代码中存在需要修复的缺陷。
异常是关于可能在运行时发生但不能由手头代码解决的非典型情况的指示,在要处理的调用代码处解决。发生异常并不表示代码中存在错误。
最佳实践
因此,如果您将运行时出现的特定情况视为您想通知开发人员的错误(“您好开发人员,此情况表明某处存在错误,请修复代码。”)然后去断言。如果断言检查您的代码的输入参数,您通常应该在文档中添加当输入参数违反该条件时您的代码具有“未定义的行为”。
相反,如果这种情况的发生并不是您眼中的错误的指示,而是您认为应该由客户端代码处理的(可能很少但)可能的情况,请引发异常。引发异常的情况应该是相应代码文档的一部分。
使用是否存在性能 [...] 问题
assert
断言的评估需要一些时间。不过,它们可以在编译时消除。但是,这会产生一些后果,请参见下文。
使用 [...] 是否存在代码维护问题
assert
通常,断言提高了代码的可维护性,因为它们通过明确假设并在运行时定期验证这些假设来提高可读性。这也将有助于捕捉回归。然而,有一个问题需要牢记:断言中使用的表达式应该没有副作用。如上所述,可以在编译时消除断言——这意味着潜在的副作用也会消失。这可能会无意中改变代码的行为。
在 PTVS、PyCharm 等 IDE 中,Wingassert isinstance()
语句可用于为一些不清楚的对象启用代码完成。
我要补充一点,我经常assert
用来指定我的代码应该具有的属性,例如循环不变量或逻辑属性,就像我在经过正式验证的软件中指定它们一样。
它们的目的是通知读者,帮助我推理,并检查我的推理没有犯错误。例如 :
k = 0
for i in range(n):
assert k == i * (i + 1) // 2
k += i
#do some things
或更复杂的情况:
def sorted(l):
return all(l1 <= l2 for l1, l2 in zip(l, l[1:]))
def mergesort(l):
if len(l) < 2: #python 3.10 will have match - case for this instead of checking length
return l
k = len(l // 2)
l1 = mergesort(l[:k])
l2 = mergesort(l[k:])
assert sorted(l1) # here the asserts allow me to explicit what properties my code should have
assert sorted(l2) # I expect them to be disabled in a production build
return merge(l1, l2)
由于在优化模式下运行 python 时断言被禁用,所以不要犹豫在其中编写昂贵的条件,特别是如果它使您的代码更清晰且不易出错