Python中`assert`语句的可接受用例是什么?

31

我经常使用 Python 的 assert 语句来检查用户输入,如果处于损坏状态就快速失败。我知道当 Python 使用 -o(优化)标志时会删除 assert。尽管我个人不会在优化模式下运行任何应用程序,但感觉还是应该避免使用 assert,以防万一。

使用 assert 后代码看起来更加简洁

assert filename.endswith('.jpg')
if not filename.endswith('.jpg'):
    raise RuntimeError

这是否是assert的有效用例?如果不是,那么Python的assert语句的有效用例是什么?


3
你好,根据我的理解,你提供的两个片段存在矛盾。其中一个应该加上“不”字。 - Will McCutchen
8个回答

27

断言应该用于表达不变量前置条件
在你的示例中,你使用它们来检查意外输入——这是完全不同的异常类

根据要求,如果出现错误的输入,引发异常并停止应用程序可能是完全可以的;然而,代码应该始终为表达性而定制,引发AssertionError并不够明确。
更好的做法是引发自己的异常或ValueError


2
没错。随着我在编程方面的技能越来越好,我发现我更深刻地理解了我的代码为什么有效,它的假设是什么,而且我看到越来越多的机会可以用 assert 来实现这一点。 - Jason Orendorff
@Gattster:if expr: raise ValueError 是一行完全有效的代码。如果您有问题,请开一个单独的问题。 - S.Lott
完全不令人信服。有效输入是前提条件。我完全看不出为什么不能使用一个简洁而优雅的语句_专门设计_来检查有效性,好吧,检查有效性。 "表现力"条款...别逗我笑了,比代码目的更具表现力的是什么呢?assert <condition>,“无效参数值:<details>” - ivan_pozdeev
1
在我15年以上的编程经验中,我从未见过一个只有在调试时才有意义而不在生产环境中的单个健全性检查 - 或者在其他人的代码中看到过这样的情况。 - ivan_pozdeev
重点在于检查的意图。如果你想表达这是一个_可能的_,但_错误的_值,那么你应该引发自己的异常。相反,如果你想表达算法的一些前提条件——永远不应该被打破的东西——那么你应该使用assert - rob

18

如果优雅不可行,那就要戏剧化

这是您的代码的正确版本:

if filename.endswith('.jpg'):
    # convert it to the PNG we want
    try:
        filename = convert_jpg_to_png_tmpfile(filename)
    except PNGCreateError:
        # Tell the user their jpg was crap
        print "Your jpg was crap!"

在我看来,以下情况是有效的:

  1. 错误是完全、100%致命的,处理它将会太可怕而难以理解。
  2. 断言仅在某些事情改变逻辑规则时才会失败。

否则,要处理可能性,因为你可以预料到它会发生。

ASSERT == "这应该永远不会在现实中发生,如果确实发生了,我们放弃"

当然,这不同于:

#control should never get here

但我总是这么做

#control should never get here
#but i'm not 100% putting my money where my mouth
#is
assert(False)

这样我就会得到一个清晰的错误信息。在你的例子中,我会使用if语句版本并将文件转换为jpg!


3
我喜欢你的开场白和用 assert False 的巧妙技巧,感觉你的文章可以以“只有傻瓜才会断言绝对”的结尾。 - Adam
1
注意:在代码的最后一块中,应该是assert False而不是assert(False),因为assert是一个语句而不是函数(在Python 2和3中都是如此)。 - kratenko

9

完全有效。这个断言是关于程序状态的正式声明。

您应该将它们用于无法证明的事情。然而,对于您认为可以证明的事情,它们非常方便,可以作为逻辑检查。

另一个例子。

def fastExp( a, b ):
    assert isinstance(b,(int,long)), "This algorithm raises to an integer power"
    etc.

又一个。最后的断言有点儿可笑,因为它应该是可以被证明的。

# Not provable, essential.
assert len(someList) > 0, "Can't work with an empty list."
x = someList[]
while len(x) != 0:
    startingSize= len(x)
    ... some processing ...
    # Provable.  May be Redundant.
    assert len(x) < startingSize, "Design Flaw of the worst kind."

另一个例子。
def sqrt( x ):
    # This may not be provable and is essential.
    assert x >= 0, "Can't cope with non-positive numbers"
    ...
    # This is provable and may be redundant.
    assert abs( n*n - x ) < 0.00001 
    return n

制作正式断言有许多理由。


感谢提供有效用例的示例。我希望看到更多的赞同您的答案,以帮助验证其他人是否与我们持有相同的观点。 - Gattster
3
@Gattster: 这是一个少数派观点。许多人声称正式证明方法要么(a)无效,要么(b)不适用于现实世界的问题。这种立场很难得到验证。 - S.Lott
在第二个例子中,你难道不能只是“assert someList”吗? - Steven Sproat
@sproaty:你可以这样做。断言的目的是澄清代码的含义,而不仅仅是重复它。因此,另一种(更明确的)表述似乎具有一定的价值。 - S.Lott
2
为什么这些不是 raise ValueError,我原以为这是用于无效参数的? - Demis
@S.Lott +100: "在我15年以上的编程经验中,我从未见过一个只有在调试时才有意义而在生产环境中没有意义的合理性检查,也从未在别人的代码中看到过这样的情况。" 对于Python的assert而言,这一点更加明显。与C++/C#不同的是,Python的assert语句被有机地纳入到代码的自然控制流中,而不会使程序不优雅地中断/终止。 - ivan_pozdeev

6

assert最适用于在测试期间需要使用的代码,当您确定不会运行-o时。

您个人可能永远不会运行-o,但如果您的代码最终进入更大的系统,并且管理员想要使用-o来运行它,那将会发生什么?

系统可能看起来运行良好,但是由于使用-o而被激活的微妙错误可能会出现。


在我15年以上的编程经验中,我从未见过一个只有在调试时才有意义而在生产环境中没有意义的健全性检查...如果我们谈论编译器优化,它们只会使检查更加相关,因为启用优化比禁用优化更容易引入错误。 - ivan_pozdeev
因此,这更加证明了不要使用“-o”禁用检查的情况 :-) - ivan_pozdeev

4

就个人而言,我会在遇到意外错误或者在实际使用中不太可能发生的情况下使用assert。而在处理来自用户或文件的输入时,应该使用异常,因为它们可以被捕获并告诉用户“嘿,我期望的是一个.jpg文件!”


但是 AssertionError 确实是一个异常... (而且,它是一个 Exception ;-)) - ivan_pozdeev

2

Python Wiki有一个关于有效使用断言的很好的指南

上面的答案并没有必要解释在运行Python时-O选项的反对意见。引用上面的页面:

如果使用-O选项启动Python,则会剥离并不评估断言。


1
S.Lott的回答是最好的。但这太长了,只是在他的评论中添加,所以我把它放在这里。无论如何,这就是我对assert的看法,基本上它只是一种缩写方式,用于执行#ifdef DEBUG。
无论如何,有两种关于输入检查的思路。你可以在目标处进行,也可以在源代码处进行。
在目标处进行是在代码内部进行的:
def sqrt(x):
    if x<0:
        raise ValueError, 'sqrt requires positive numbers'
    root = <do stuff>
    return root

def some_func(x):
    y = float(raw_input('Type a number:'))
    try:
        print 'Square root is %f'%sqrt(y)
    except ValueError:
        # User did not type valid input
        print '%f must be a positive number!'%y

现在,这有很多优点。可能编写sqrt函数的人最了解算法中哪些是有效值。在上述示例中,我不知道从用户那里得到的值是否有效。必须有人进行检查,并且在最了解什么是有效的代码 - sqrt算法本身中进行检查是有意义的。

然而,这会带来性能损失。想象一下像这样的代码:

def sqrt(x):
    if x<=0:
        raise ValueError, 'sqrt requires positive numbers'
    root = <do stuff>
    return root

def some_func(maxx=100000):
    all_sqrts = [sqrt(x) for x in range(maxx)]
    i = sqrt(-1.0)
    return(all_sqrts)

现在,这个函数会调用sqrt 100k次。每次,sqrt都会检查值是否大于等于0。但是,由于我们如何生成这些数字,我们已经知道它们是有效的,那些额外的有效性检查只是浪费执行时间。如果能摆脱它们不好吗?然后还有一个将引发ValueError的函数,我们将捕获它并意识到我们犯了一个错误。我编写程序依赖于子函数来检查我,所以当它不起作用时,我只需要担心恢复。
第二种思路是,不是目标函数检查输入,而是在定义中添加约束,并要求调用者确保使用有效数据进行调用。该函数承诺使用良好的数据将返回其合同规定的内容。这样可以避免所有这些检查,因为调用者比目标函数更了解发送的数据,它来自哪里以及固有的约束条件。这些的最终结果是代码契约和类似结构,但最初只是通过惯例,在设计中,就像下面的注释:
# This function valid if x > 0
def sqrt(x):
    root = <do stuff>
    return root

def long_function(maxx=100000):
    # This is a valid function call - every x i pass to sqrt is valid
    sqrtlist1 = [sqrt(x) for x in range(maxx)]
    # This one is a program error - calling function with incorrect arguments
    # But one that can't be statically determined
    # It will throw an exception somewhere in the sqrt code above
    i = sqrt(-1.0)

当然,错误总是难免的,合同可能会被违反。但到目前为止,结果基本相同 - 在两种情况下,如果我调用sqrt(-1.0),我将在sqrt代码内部收到异常,可以遍历异常堆栈,并找出我的错误所在。
然而,还有更加隐蔽的情况......例如,假设我的代码生成列表索引,存储它,稍后查找列表索引,提取值并进行某些处理。假设我们意外得到了一个-1的列表索引。所有这些步骤可能实际上都完成了而没有任何错误,但最终测试结果是错误的,我们不知道为什么。
那么为什么要使用assert?在测试和证明合同时,我们希望有一些东西能够让我们更接近失败的调试信息。这基本上与第一种形式完全相同 - 毕竟,它执行的是完全相同的比较,但它的语法更加整洁,稍微更专门用于验证合同。一个副作用是,一旦您相当确信您的程序工作正常,并且正在优化和寻找更高的性能而非可调试性,所有这些现在多余的检查都可以删除。

这并不是在为 Python 中的 assert 辩护。更像是在说明它在 Python 之前的一些关注代码性能胜过正确性的语言中有其存在的意义,因此在 Python 中,它的原始用例已经成为了退化的。 - ivan_pozdeev

0

底线:

  • assert及其语义是早期语言的遗产。
  • Python的重心转移已经证明了传统用例的无关性。
  • 截至本文撰写时,没有其他用例被正式提出,尽管有一些想法将其改造成通用检查。
  • 如果您可以接受限制,它现在可以被用作(实际上也被用作)通用检查。

ASSERT语句最初被创建的预期用途为:

  • 进行有关精神健康的检查,这些检查在生产中被认为过于昂贵或不必要。
    • 例如:这可能是内部函数的输入检查,检查free()是否传递了先前从malloc()获取的指针,对它们进行非平凡操作后的内部结构完整性检查等。

这在旧有的环境中曾经是一件大事,现在在为性能而设计的环境中仍然如此。这是C++/C#中其语义的全部原因:

  1. 在发布版本中取消剪切
  2. 立即、不优雅且尽可能响亮地终止程序。

然而,Python有意识地为了程序员的表现而牺牲了代码性能(信不信由你,我最近通过将一些代码从Python移植到Cython获得了100倍的加速 - 甚至没有禁用边界检查!)。Python代码在“安全”环境中运行,因此您无法完全“破坏”进程(或整个系统)以致于无法跟踪的段错误/BSoD/砖化 - 最糟糕的情况是会出现一个未处理的异常,并附带大量调试信息,以可读的形式优雅地呈现给您。

  • 这意味着运行时和库代码应在适当的时刻包括各种检查 - 在所有时候,无论是“调试模式”还是其他模式。
此外,Python 强大的影响力在于始终提供源码(透明编译、回溯中的源代码行、自带调试器期望源代码与 .pyc 一起使用才能发挥作用),这很大程度上模糊了“开发”和“使用”的界限(这就是为什么 setuptools 的自包含 .egg 文件引起了强烈反对,以及为什么 pip 总是安装未打包文件:如果一个文件已经被打包,源代码将不再容易获得,因而难以诊断问题)。
这些特性的结合几乎摧毁了任何“仅限调试”的代码用例。
你猜对了,把 assert 重新用作通用检查的想法最终浮出水面

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接