Python断言的最佳实践


482
  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引发异常?



29
-O和-OO python参数将删除您的断言。那应该促使您思考它的好处。
彼得·拉达

4
Thomasz Zielinski的链接已断开,它现在是:mail.python.org/pipermail/python-list/2013-November/660568.html。我很确定pipermail具有不稳定的ID功能,我从同一pipermail内部发现了其他指向同一目的的URL。
quodlibetor

3
万一mail.python.org/pipermail/python-list/2013-November/660568.html再次移动,它将存档在archive.is/5GfiG。该文章的标题是“何时使用assert”,是有关Python最佳实践的出色文章(实际上是一篇文章)assert
clacke

Answers:


144

为了能够在整个函数中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

10
尽管属性被实现为描述符,但我不会将其称为使用它们的示例。这是属性本身的一个示例: docs.python.org/library/functions.html#property
Jason Baker,2009年

3
设置x时,应在MyClass中使用这些属性。这个解决方案太笼统了。

112
像这样的答案很不错,但是与问题无关...我们不能将Deestan或John Mee的答案标记为有效答案吗?
Vajk Hermecz

4
这似乎没有回答问题的标题。另外,这是Python的class属性功能的替代品。
Dooms101

10
@VajkHermecz:实际上,如果您重读该问题,那么这是两个问题合而为一。只看标题的人只会熟悉第一个问题,而这个问题不会回答。该答案实际上包含对第二个问题的答案。
ArtOfWarfare 2015年

742

应该使用断言来测试永远不会发生的条件。目的是在程序状态损坏的情况下尽早崩溃。

应该将异常用于可能发生的错误,并且几乎应该始终创建自己的Exception类


例如,如果您要编写一个从配置文件读取到的函数,则文件中dict不正确的格式将引发a ConfigurationSyntaxError,同时assert您可以避免返回None


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

如果x仅由您自己的代码在同一程序中设置,请声明。


126
这是使用断言的正确方法。它们不应用于控制程序流。
塔恩·布里姆霍尔

41
上一段+1(尽管您应该明确提及其中assert包含一个隐式if __debug__且可能已被优化),如John Mee的回答所述
Tobias Kienzler 2013年

3
重读答案我认为您可能并不意味着永远都不会碰到这种情况,而目的是在程序状态异常的情况下尽早崩溃,该状态通常与您不希望的情况相吻合永远发生
Bentley4年

10
assert只能用于发现没有已知恢复的问题;几乎总是编码错误(不是不好的输入)。当触发一个断言时,这应该意味着该程序处于可能继续进入的危险状态,因为它可能开始与网络对话或写入磁盘。面对错误(或恶意)的输入,健壮的代码从有效状态“原子地”转移到有效状态。每个线程的顶层都应具有故障屏障。消耗外界输入的故障屏障通常仅在屏障的一次迭代(while / try),回滚/登录错误时失败。
罗布

10
“应该使用断言来测试永远不会发生的条件。” 是。第二个“应该”的含义是:如果发生这种情况,则程序代码不正确。
Lutz Prechelt 2014年

362

优化编译后,将删除“ assert”语句。因此,是的,在性能和功能上都存在差异。

当在编译时请求优化时,当前代码生成器不会为assert语句生成任何代码。- Python 2的文档 的Python 3文档

如果您用于assert实现应用程序功能,然后优化对生产的部署,那么“ but-it-works-in-dev”缺陷将给您带来困扰。

参见PYTHONOPTIMIZE-O -​​OO


26
哇!超级重要的说明是!我一直在计划使用断言检查一些永不失败的事情,这些失败的失败将表明有人非常小心地操纵了我正在发送的数据,以试图访问他们不应该访问的数据。这是行不通的,但是我想用断言迅速结束他们的尝试,因此在生产中进行优化将无法达到目的。我想我就raise一个Exception来代替。哦-我刚刚在中发现了一个SuspiciousOperation Exception带有子类的恰当名称Django!完善!
ArtOfWarfare 2014年

顺便说一下,如果您bandit在代码上运行@ArtOfWarfare ,它将警告您。
Nagev

132

的四个目的 assert

假设您与四个同事Alice,Bernd,Carl和Daphne一起处理了200,000行代码。他们叫您的代码,您叫他们的代码。

然后assert具有四个角色

  1. 告知Alice,Bernd,Carl和Daphne您的代码期望什么。
    假设您有一个处理元组列表的方法,并且如果这些元组不是不可变的,则程序逻辑可能会中断:

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

    比文档中的等效信息更值得信赖,并且更易于维护。

  2. 通知计算机您的代码期望什么。
    assert强制代码调用者采取适当的行为。如果您的代码调用了Alices的代码,而Bernd的代码调用了您的代码,则没有assert,如果程序在Alices代码中崩溃,Bernd可能认为这是Alice的错误,Alice进行了调查,并可能认为这是您的错误,您调查了并告诉Bernd实际上他的。很多工作丢失了。
    有了断言,无论谁打错电话,他们都将能够迅速看到这是他们的错,而不是您的错。爱丽丝,伯恩德,你们都将从中受益。节省大量时间。

  3. 通知您的代码(包括您自己)的读者在某些时候取得了什么成就。
    假设您有一个条目列表,并且每个条目都可以是干净的(很好),也可以是乱码,流浪汉,gullup或闪烁的(都不可接受)。如果它很轻便,必须将其清零。如果是流浪汉,则必须加以保护;如果它是古怪的,则必须将其放小(然后也可能要加快速度);如果已闪烁,则必须再次闪烁(星期四除外)。您会明白:这是复杂的东西。但是最终结果是(或应该是)所有条目都是干净的。Right Thing(TM)要做的是将清洁循环的效果总结为

    assert(all(entry.isClean() for entry in mylist))

    该报表节省了大家试图了解头痛究竟它是一个美妙的循环实现。这些人中最常出现的人可能就是你自己。

  4. 通知计算机您的代码在某些时候已经实现了什么。
    如果您在小跑后忘记了需要它的条目,assert它将节省您的时间,并避免您的代码在以后很长时间内破坏亲爱的达芙妮。

在我看来,assert文档的两个目的(1和3)和保障(2和4)同等重要。
通知人们甚至比通知计算机有价值,因为通知人们可以防止assert目标要抓住的错误(在情况1中)以及在任何情况下都可以避免许多后续错误。


34
5. assert isinstance()帮助PyCharm(python IDE)知道变量的类型,它用于自动完成。
Cjkjvfnby 2014年

1
为当前执行时的真实情况声明自文档代码假设。这是一个假设注释,将被检查。
pyj 2014年

9
关于2和4:您应该非常小心,以确保您的主张不太严格。否则,只有断言本身可以使您的程序在更通用的环境中使用。特别是断言类型有悖于python的鸭子式输入。
zwirbeltier

9
@Cjkjvfnby请小心此博客条目中所述的isinstance()的过度使用:“ isinstance()被视为有害 ”。现在,您可以使用文档字符串在Pycharm中指定类型
binarysubstrate

2
以确保合同的一种方式使用断言。有关按合同设计的详细信息,请访问en.wikipedia.org/wiki/Design_by_contract
Leszek Zarna 2015年

22

除了其他答案外,断言本身会引发异常,但仅断言AssertionErrors。从功利主义的角度来看,断言不适用于需要精细控制所捕获的异常的情况。


3
对。在调用方中捕获断言错误异常似乎很愚蠢。
拉菲·哈查杜安

很好的一点。仅从宏级别看原始问题时,就可以轻松忽略这一细微差别。即使不是在优化时删除断言的问题,丢失发生错误的具体细节也会使调试更具挑战性。干杯,outis!
cfwschmidt

可以将您的答案读为好像您想抓住一样AssertionErrors,只要您可以将其设为粗粒度即可。实际上,您不应该抓住它们。
Tomasz Gandor

19

这种方法唯一真正出错的地方是,使用assert语句很难创建非常描述性的异常。如果您正在寻找更简单的语法,请记住您可以执行以下操作:

class XLessThanZeroException(Exception):
    pass

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

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

另一个问题是,使用assert进行正常的条件检查是,使用-O标志很难禁用调试声明。


24
您可以将错误消息附加到断言。这是第二个参数。这将使其具有描述性。
拉菲·哈查杜安

10

英语语言文字断言在这里的意义上使用发誓申明招认。这并不意味着“检查”“应该”。这意味着作为编码人员正在此处宣誓就职

# 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

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


2
最好用装饰器(@precondition和@postcondition)完成
Caridorc

@Caridorc的具体好处是什么?
Chiel 10 Brinke

@ChieltenBrinke自我记录代码,而不是#precondition: n >= 0 断言,他可以编写@precondition(lambda n: n >= 0)
Caridorc

@Caridorc那是那些内置装饰器吗?以及如何从中产生文件?
Chiel 10 Brinke

@ChieltenBrinke不是内置的,但易于实现stackoverflow.com/questions/12151182/…。对于文档,只需__doc__通过提供附加字符串来修补属性
-Caridorc

4

是否存在性能问题?

  • 请记住“先使其工作,然后再使其快速工作”
    通常,几乎没有任何程序的百分比与其速度有关。assert如果事实证明存在性能问题,您总是可以开除或简化它,而其中大多数绝不会。

  • 务实
    假设您有一种处理元组的非空列表的方法,并且如果这些元组不是不可变的,则程序逻辑将中断。您应该写:

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

    如果您的列表往往有十个条目,这可能很好,但是如果它们有一百万个条目,则可能会成为问题。但是,与其完全丢弃这张贵重的支票,不如将其降级为

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

    这很便宜,但无论如何都会捕获大多数实际程序错误。


2
应该是assert(len(listOfTuples)==0 or type(listOfTyples[0])==tuple)
osa 2014年

不,不应该。这将是一个较弱的测试,因为它不再检查第二个断言检查的“非空”属性。(第一个没有,尽管应该。)
Lutz Prechelt 2015年

1
第二个断言不会显式检查非空属性;这更多的是副作用。如果由于列表为空而引发异常,则使用该代码的人员(编写该代码一年后的其他人或作者)会盯着它,试图找出断言是否真的要抓住空列表的情况,或者如果这是断言本身的错误。此外,我看不出如何不检查空情况是“弱得多”,而仅检查第一个元素是“ 97%正确”。
osa

3

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

目的

向初学者解释它-断言是可能引发错误的语句,但是您不会抓住它们。而且通常不应该将它们提高,但是在现实生活中,无论如何它们有时都会得到提高。这是一种严重的情况,代码无法从中恢复,我们称之为“致命错误”。

接下来,它是出于“调试目的”,虽然正确,但听起来很不屑一顾。我更喜欢“声明不变式,永远不应该被违反”的表述,尽管它在不同的初学者中的工作方式有所不同……有些“只懂它”,而另一些要么找不到用处,要么替换正常的异常,甚至用它控制流程。

样式

在Python中,assert它是语句,而不是函数!(请记住assert(False, 'is true')不会提高。但是,请注意:

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

此acually适用于单元测试框架,其通常具有许多专用的方法来做断言(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也是可读的,并且该语句在代码中看起来合乎逻辑。另外,您可以从中获得某些东西而无需阅读回溯(有时甚至不可用)。


1

assert异常的使用和引发都与沟通有关。

  • 断言是关于开发人员要解决的代码正确性的声明:代码中的断言将代码的正确性告知读者,有关正确代码必须满足的条件。在运行时失败的断言通知开发人员代码中存在需要修复的缺陷。

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

最佳实践

因此,如果您将运行时发生的特定情况视为要通知开发人员的错误(“开发人员,此情况表明某个地方存在错误,请修复代码。”)然后断言。如果断言检查代码的输入参数,则通常应在输入参数违反条件时向文档添加代码具有“未定义行为”的文档。

如果不是这样的情况的发生并不是您眼中的错误的迹象,而是您认为应该由客户端代码处理的(可能很少见但)可能的情况,请引发异常。引发异常的情况应该是相应代码文档的一部分。

使用时是否存在性能问题? assert

断言的评估需要一些时间。不过,可以在编译时将其消除。但是,这会带来一些后果,请参见下文。

使用时是否存在代码维护问题 assert

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


1

声明将检查-1
.有效条件,
2.有效语句,
3.真实逻辑;
源代码。不会使整个项目失败,而是发出警报,指出源文件中不适当的内容。

在示例1中,由于变量'str'不为null。因此,不会引发任何断言或异常。

范例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'为null。因此,我们可以通过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
什么也不会得到打印


0

在PTVS,PyCharm等IDE中,assert isinstance()可以使用Wing 语句为一些不清楚的对象启用代码完成功能。


这似乎早于使用类型注释或的类型typing.cast
Acumenus

-1

对于它的价值,如果您要处理依靠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

2
这不能回答OP的最佳实践问题。
codeforester
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.