使用assert的最佳实践?

  1. 使用assert作为标准代码的一部分,而不是仅用于调试目的,是否存在性能或代码维护问题?

    assert x >= 0, 'x is less than zero'
    

    或好或坏

    if x < 0:
    raise Exception('x is less than zero')
    
  2. 还有,有没有办法设置一个像if x < 0 raise error这样的业务规则,总是没有try/except/finally进行检查,如果在整个代码中x小于0就会引发错误,就像如果你在函数的开始设置assert x < 0,在函数的任何地方x小于0就会引发异常?

281733 次浏览

当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

这种方法唯一真正的错误是,很难使用断言语句产生非常描述性的异常。如果你正在寻找更简单的语法,记住你可以也做这样的事情:

class XLessThanZeroException(Exception):
pass


def CheckX(x):
if x < 0:
raise XLessThanZeroException()


def foo(x):
CheckX(x)
#do stuff here

另一个问题是,使用assert进行正常的条件检查会使使用-O标志禁用调试断言变得困难。

断言应该用于测试不应该发生. #的条件。目的是在程序状态损坏的情况下尽早崩溃。

异常应该用于可能发生的错误,你应该总是创建自己的Exception类


例如,如果您正在编写一个函数来从配置文件读取到dict,那么文件中不正确的格式将引发ConfigurationSyntaxError,而您可以assert,而您不打算返回None


在您的示例中,如果x是通过用户界面或外部源设置的值,则最好出现异常。

如果在同一个程序中,x仅由您自己的代码设置,则使用断言。

# EYZ0。所以,是的,它们在性能和功能上都有差异。

在编译时请求优化时,当前代码生成器不会为assert语句生成代码。- # eyz0 # eyz1

如果您使用assert来实现应用程序功能,然后优化部署到生产环境,那么您将受到“但它在开发中工作”缺陷的困扰。

参见PYTHONOPTIMIZE- o oo

除了其他答案之外,断言本身也会抛出异常,但仅抛出AssertionErrors。从实用主义的角度来看,当您需要对捕获的异常进行精细控制时,断言并不适合。

如前所述,当您的代码should NOT到达某个点(意味着那里存在错误)时,应该使用断言。在我看来,使用断言最有用的原因可能是不变量/前置/后置条件。在循环或函数的每次迭代的开始或结束时,这些必须为真。

例如,一个递归函数(两个独立的函数,一个处理错误的输入,另一个处理错误的代码,因为它很难与递归区分开来)。如果我忘记写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

这些循环不变量通常可以用断言表示。

assert . #的四个目的

假设你和四位同事Alice、Bernd、Carl和Daphne一起处理20万行代码。 他们调用你的代码,你调用他们的代码。

然后assert四个角色:

    <李> < p > # EYZ0 < br > 假设您有一个处理元组列表的方法,如果这些元组不是不可变的,程序逻辑就会中断:

    def mymethod(listOfTuples):
    assert(all(type(tp)==tuple for tp in listOfTuples))
    
    这比文档中的同等信息更值得信赖

    <李> < p > # EYZ0 < br > assert强制代码的调用者采取正确的行为。 如果你的代码调用爱丽丝的,贝恩德的代码调用你的, 如果没有assert,如果程序在alice代码中崩溃, 贝恩德可能会认为是爱丽丝的错, 爱丽丝调查后可能会认为是你的错, 你去调查,告诉贝恩德这实际上是他的。 大量工作丢失。
    使用断言,无论谁调用错了,他们都能很快看出是错误的 他们的错,不是你的。爱丽丝,贝恩德和你们都受益。

    <李> < p > # EYZ0 < br > 假设您有一个条目列表,并且每个条目都是干净的(这很好) 或者它可以是smorsh, trale, gullup,或闪闪发光(这些都是不可接受的)。 如果它被弄脏了,就必须清理干净;如果是真实的,就必须加以掩饰; 如果它是gullup,它必须小跑(然后可能也要踱步); 如果它已经亮了,就必须再亮一次,星期四除外。 你知道的:这是很复杂的东西。 但是最终结果是(或者应该是)所有的条目都是干净的。 正确的事情(TM)要做的是总结你的效果 清洗循环为

    assert(all(entry.isClean() for entry in mylist))
    
    这句话为每个试图理解的人省去了麻烦 这是什么完全,这是奇妙的循环正在实现。 而这些人中最常见的可能就是你自己 <李> < p > # EYZ0 < br > 如果你在小跑后忘记在需要的地方踱步, assert将挽救你的一天,并避免你的代码

在我看来,assert文档的两个目的(1和3)和 护具(2和4)同样有价值 告知人们甚至可能比告知计算机更有价值 因为它可以防止assert旨在捕捉的错误(在情况1中) 在任何情况下都有大量的后续错误

有性能问题?

  • 请记住“先让它工作,再让它快速工作”.
    任何程序中只有极少数的部分通常与速度有关。 如果事实证明可以,您总是可以删除或简化assert 是一个性能问题——而且大多数永远不会。

  • <李> < p > # EYZ0: < br > 假设您有一个处理非空元组列表的方法,如果这些元组不是不可变的,那么程序逻辑就会中断。你应该写:

    def mymethod(listOfTuples):
    assert(all(type(tp)==tuple for tp in listOfTuples))
    

    如果你的列表往往有10个条目那么长,这可能没问题,但是 如果有一百万个条目,这就会成为一个问题。 但与其完全丢弃这张有价值的支票,你可以 只需将其降级为

    def mymethod(listOfTuples):
    assert(type(listOfTuples[0])==tuple)  # in fact _all_ must be tuples!
    

    这是廉价的,但可能会捕捉大多数实际程序错误

在诸如PTVS、PyCharm等IDE中,Wing assert isinstance()语句可用于为一些不清楚的对象启用代码补全。

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

当我们不想调试并意识到源代码中的断言问题时。禁用优化标志

< p > # EYZ0
什么都不会打印

英语单词< em >断言< / em >在这里被用来表示< em > < / em >发誓< em >肯定< / em >< em >承认< / em >。这并不意味着“检查”“应该”。这意味着作为一个编码器在这里做一个< em > < / em >宣誓声明:

# 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

如果代码正确,则排除单事件令、硬件故障等没有断言会失败。这就是为什么程序对终端用户的行为不能受到影响。特别是,断言即使在特殊的规划条件下也不能失败。这种事从来没有发生过。如果发生这种情况,程序员应该为此受到惩罚。

无论如何,如果你处理的代码依赖于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

这是一个悬而未决的问题,我有两个方面想涉及:何时添加断言以及如何编写错误消息。

目的

向初学者解释一下,断言是一种可能引发错误的语句,但您不会捕获它们。他们通常不应该被抚养,但在现实生活中,他们有时还是会被抚养。这是一个严重的情况,代码无法恢复,我们称之为“致命错误”。

其次,它是为了“调试目的”,虽然正确,但听起来非常轻蔑。我更喜欢“声明不变量,它永远不应该被违反”的提法,尽管它对不同的初学者有不同的作用……有些人“只是得到它”,而另一些人要么没有找到它的任何用途,要么取代正常的异常,甚至用它来控制流。

风格

在Python中,assert是一个语句,而不是一个函数!(记住assert(False, 'is true')不会提升。但是,先说一下:

何时以及如何编写可选的“错误消息”?

这实际上适用于单元测试框架,它通常有许多专门的方法来做断言(assertTrue(condition)assertFalse(condition), assertEqual(actual, expected)等)。它们通常还提供了一种对断言进行评论的方法。

在一次性代码中,您可以不使用错误消息。

在某些情况下,没有什么可以添加到断言:

< p > def转储(东西): assert isinstance(某事,可删除的) #…< / p >

但除此之外,消息对于与其他程序员(有时是你代码的交互式用户,例如在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也是可读的,并且语句在代码中看起来是合乎逻辑的。此外,您可以在不读取回溯(有时甚至不可用)的情况下从中获得一些信息。

使用assert和抛出异常都与通信有关。

  • 断言是针对开发人员的关于代码正确性的语句 :代码中的断言告诉代码的读者代码正确必须满足的条件。在运行时失败的断言会通知开发人员代码中存在需要修复的缺陷。

  • 异常是关于非典型情况的指示,这些情况可能在运行时发生,但无法由手头的代码解决,在要处理的调用代码中进行处理。异常的出现并不表明代码中存在错误。

最佳实践

因此,如果您将运行时发生的特定情况视为您想要通知开发人员的错误(“嗨,开发人员,这种情况表明某处存在错误,请修复代码。”),那么请使用断言。如果断言检查代码的输入参数,当输入参数违反条件时,您通常应该在文档中添加代码具有“未定义行为”。

相反,如果这种情况的出现并不是您眼中的错误,而是您认为应该由客户端代码处理的(可能很少,但可能)情况,则引发异常。引发哪种异常的情况应该是相应代码文档的一部分。

有没有演出[…]使用assert的问题

断言的计算需要一些时间。但是,它们可以在编译时被删除。然而,这有一些后果,见下文。

有[…]使用assert时的代码维护问题

通常,断言可以提高代码的可维护性,因为它们通过明确假设并在运行时定期验证这些假设来提高可读性。这也将有助于捕捉倒退。但是,有一个问题需要记住:断言中使用的表达式应该没有副作用。如上所述,断言可以在编译时消除——这意味着潜在的副作用也将消失。这可能(无意中)改变代码的行为。

我补充说,我经常使用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在优化模式下运行时,断言是禁用的,所以不要犹豫在其中编写代价高昂的条件,特别是如果它使您的代码更清晰的更不易出错