使用断言的最佳实践?
- 2025-01-13 08:53:00
- admin 原创
- 104
问题描述:
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
所以的情况下进行检查,如果在整个代码中的任何时候x
都小于 0,就会引发错误,比如如果你assert x < 0
在函数开始时设置,函数内任何地方都x
小于 0 就会引发异常?
解决方案 1:
断言应该用来测试那些不应该发生的情况。目的是在程序状态损坏的情况下尽早崩溃。
异常应该用于可能发生的错误,并且您几乎总是应该创建自己的异常类。
例如,如果您正在编写一个函数来从配置文件中读取dict
,文件中的不正确格式应该会引发ConfigurationSyntaxError
,而您可以assert
表示您不会返回None
。
在您的示例中,如果x
是通过用户界面或从外部源设置的值,则最好使用异常。
如果x
仅由同一个程序中的自己的代码设置,则使用断言。
解决方案 2:
编译优化时会删除“assert”语句。因此,性能和功能上确实存在差异。
当编译时请求优化时,当前代码生成器不会为断言语句发出任何代码。 - Python 2 文档 Python 3 文档
如果您使用它assert
来实现应用程序功能,然后优化部署到生产环境,那么您将会受到“但它在开发中有效”缺陷的困扰。
参见PYTHONOPTIMIZE和-O -OO
解决方案 3:
为了能够在整个函数中当 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
解决方案 4:
四个目的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
强制代码调用者采取正确的行为。如果您的代码调用 Alice 的代码,而 Bernd 的代码调用您的代码,那么如果没有assert
,如果程序在 Alice 的代码中崩溃,Bernd 可能会认为这是 Alice 的错误,Alice 会进行调查,并可能认为这是您的错误,您进行调查并告诉 Bernd 这实际上是他的错误。大量工作付诸东流。
有了断言,无论谁调用错误,他们都会很快发现这是他们的错误,而不是您的错误。Alice、Bernd 和你们都会受益。节省大量时间。
告知代码的读者(包括您自己)您的代码在某个时刻取得了什么成就。
假设您有一个条目列表,每个条目都可以是干净的(这很好),也可以是 smorsh、trale、gullup 或 twinkled(这些都不可接受)。如果是 smorsh,则必须 unsmorshed;如果是 trale,则必须 baludoed;如果是 gullup,则必须 trotted(然后可能还要 paced);如果是 twinkled,则必须再次 twinled,星期四除外。您明白了:这是很复杂的事情。但最终结果是(或应该是)所有条目都是干净的。正确的做法是将清理循环的效果总结为
assert(all(entry.isClean() for entry in mylist))
这句话让那些想知道这个奇妙的循环到底在做什么的人省去了头疼。这些人中最常见的人可能就是你。
在某个时刻告知计算机您的代码已实现什么。
如果您在跑步后忘记了设置需要它的条目,这assert
将节省您的时间并避免您的代码在很久以后被破坏。
在我看来,assert
记录(1 和 3)和保护(2 和 4)这两个目的同等重要。
告知人们甚至可能比告知计算机更有价值,因为它可以防止想要捕捉的错误assert
(在情况 1 中)以及无论如何大量后续错误。
解决方案 5:
除了其他答案之外,断言本身也会抛出异常,但只会抛出 AssertionErrors。从实用主义的角度来看,当您需要对捕获的异常进行细粒度控制时,断言并不合适。
解决方案 6:
这种方法唯一真正错误的地方是,很难使用 assert 语句来生成非常具有描述性的异常。如果您正在寻找更简单的语法,请记住您也可以这样做:
class XLessThanZeroException(Exception):
pass
def CheckX(x):
if x < 0:
raise XLessThanZeroException()
def foo(x):
CheckX(x)
#do stuff here
另一个问题是,使用断言进行正常条件检查会使得使用 -O 标志禁用调试断言变得困难。
解决方案 7:
此处的英语单词assert的用法是发誓、肯定、承认。它不表示“检查”或“应该”。它的意思是,你作为一名程序员在此做出宣誓声明:
# 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
如果代码正确,除非发生单事件干扰、硬件故障等,否则断言永远不会失败。这就是为什么程序对最终用户的行为不能受到影响的原因。特别是,即使在异常的编程条件下,断言也不会失败。它永远不会发生。如果发生了,程序员应该为此受到惩罚。
解决方案 8:
正如之前所说,当您的代码永远不应该达到某个点(即那里有错误)时,应该使用断言。我认为使用断言最有用的原因可能是不变量/前置/后置条件。这些是循环或函数每次迭代开始或结束时必须为真的内容。
例如,递归函数(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
这些循环不变量通常可以用断言来表示。
解决方案 9:
不管怎样,如果您正在处理依赖于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
解决方案 10:
嗯,这是一个开放式的问题,我想谈两个方面:何时添加断言以及如何编写错误消息。
目的
向初学者解释一下 - 断言是可能引发错误但你无法捕捉到的语句。通常情况下,它们不应该被引发,但在现实生活中,它们有时会被引发。这是一种严重的情况,代码无法恢复,我们称之为“致命错误”。
接下来,它是为了“调试目的”,虽然正确,但听起来很不屑一顾。我更喜欢“声明不变量,永远不应违反”的表述,尽管它对不同的初学者有不同的作用……有些人“刚好明白”,而其他人要么觉得它没什么用,要么用它来代替正常的异常,甚至控制流程。
风格
在 Python 中,assert
是一个语句,而不是一个函数!(记住assert(False, 'is true')
不会引发。但是,有了它:
何时以及如何编写可选的“错误消息”?
这实际上适用于单元测试框架,这些框架通常有许多专用方法来执行断言(assertTrue(condition)
等等assertFalse(condition), assertEqual(actual, expected)
)。它们通常还提供一种方法来对断言进行评论。
在一次性代码中你可以不用错误消息。
在某些情况下,没有什么可以补充到这个断言中:
def dump(something):断言 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'
然后,获取AssertionError: a must be equal to b
也是可读的,并且该语句在代码中看起来合乎逻辑。此外,您可以从中获取一些内容而无需阅读回溯(有时甚至无法获得)。
解决方案 11:
是否存在性能问题?
请记住“先让它运行起来,然后再让它运行得更快”。
任何程序中只有极少数部分与速度有关。如果某个部分被证实存在性能问题,您可以随时将其删除或简化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!
虽然这种方法成本低廉,但无论如何都能够捕获大多数实际程序错误。
解决方案 12:
异常的使用assert
和引发都与沟通有关。
断言是针对开发人员的代码正确性的陈述:代码中的断言告知代码读者代码正确必须满足的条件。运行时失败的断言告知开发人员代码中存在需要修复的缺陷。
异常是指运行时可能发生但无法由当前代码解决的非典型情况,由调用代码处理。发生异常并不表示代码中存在错误。
最佳实践
因此,如果您认为运行时发生的特定情况是错误,并希望通知开发人员(“嗨,开发人员,这种情况表明某处存在错误,请修复代码。”),那么请进行断言。如果断言检查代码的输入参数,则通常应在文档中添加当输入参数违反该条件时代码具有“未定义行为”的内容。
相反,如果这种情况的发生并不表明存在错误,而是一种(可能很少见但)可能的情况,您认为应该由客户端代码处理,则引发异常。引发异常的情况应成为相应代码文档的一部分。
使用 [...] 是否存在性能问题
assert
断言的评估需要一些时间。不过,它们可以在编译时被消除。但这会产生一些后果,见下文。
使用 [...] 是否存在代码维护问题
assert
通常,断言可以提高代码的可维护性,因为它们通过明确假设并在运行时定期验证这些假设来提高可读性。这也有助于捕捉回归。然而,有一个问题需要牢记:断言中使用的表达式不应该有副作用。如上所述,断言可以在编译时消除 - 这意味着潜在的副作用也会消失。这可能会无意中改变代码的行为。
解决方案 13:
断言用于检查源代码的 -
**1. 有效条件,
有效语句,
真实逻辑**
。它不会让整个项目失败,而是发出警报,指出源文件中存在不合适的内容。
在示例 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:/Pythonhello.py
在示例 2 中,var 'str' 为空。因此,我们通过assert语句阻止用户继续执行错误的程序。
示例 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
不会打印任何内容
解决方案 14:
在 PTVS、PyCharm 等 IDE 中,assert isinstance()
可以使用 Wing 语句来为一些不清楚的对象启用代码补全。
解决方案 15:
我想补充的是,我经常使用它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 以优化模式运行时断言被禁用,因此不要犹豫在其中写入代价高昂的条件,特别是如果它使你的代码更清晰、更不容易出错
解决方案 16:
设置业务规则,例如if x < 0 raise error
据我所知,对于此问题没有通用的(独立于实现的)解决方案。
然而,如果x
是某个类的对象的属性C
,则可以通过将普通属性转换x
为不仅具有 getter 而且具有 setter 的属性来实现: x
https://docs.python.org/3/library/functions.html#property
然后,制定者可以实施并执行业务规则。