这是用户在 2024-7-28 1:00 为 https://import-that.dreamwidth.org/676.html 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?
import_that: XKCD guy flying with Python (Default)
[personal profile] import_that
Python's assert statement is a very useful feature that unfortunately often gets misused. assert takes an expression and an optional error message, evaluates the expression, and if it gives a true value, does nothing. If the expression evaluates to a false value, it raises an AssertionError exception with optional error message. For example:
Python 的 assert 语句是一个非常有用的功能,但不幸的是,它经常被误用。assert 接受一个表达式和一个可选的错误信息,评估表达式,如果结果为真,则不做任何操作。如果表达式评估为假,则会引发一个带有可选错误信息的 AssertionError 异常。例如:


py> x = 23
py> assert x > 0, "x is zero or negative"
py> assert x%2 == 0, "x is an odd number"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: x is an odd number


Many people use assertions as a quick and easy way to raise an exception if an argument is given the wrong value. But this is wrong, badly wrong, for two reasons. The first is that AssertionError is usually the wrong error to give when testing function arguments. Exceptions have meaning to the reader: when we see a KeyError, we expect that the error has something to do with a missing or invalid key, even before reading the error message itself. When we see an IndexError, we expect that an index was out of bounds. It's usually a sign of poor coding to violate those expectations, code like this is wrong:
许多人使用断言作为一种快速简便的方法来引发异常,如果参数值错误的话。但这是错误的,非常错误,有两个原因。第一个原因是AssertionError通常不是测试函数参数时应该给出的错误。异常对读者有意义:当我们看到KeyError时,我们期望错误与缺失或无效的键有关,即使在阅读错误信息之前也是如此。当我们看到IndexError时,我们期望索引超出了范围。违反这些期望通常是编码不良的标志,像这样的代码是错误的:


if not isinstance(x, int):
    raise AssertionError("not an int")


Here the appropriate exception to raise is TypeError, not AssertionError, and using assert raises the wrong sort of exception.
这里应该引发的适当异常是TypeError,而不是AssertionError,使用assert会引发错误类型的异常。


But, and more dangerously, there's a twist with assert: when you run Python with the -O or -OO optimization flags, assertions will be compiled away and never executed. Consequently there is no guarantee that assertions will actually be run. When using assert properly, this is a feature, but when it is used inappropriately, it can lead to code that is completely broken when running with the -O flag.
但是,更危险的是,assert有一个转折点:当你使用-O 或-OO 优化标志运行 Python 时,断言将被编译掉并且永远不会执行。因此,不能保证断言实际上会运行。当正确使用assert时,这是一个特性,但当不适当地使用时,它可能导致代码在使用-O 标志运行时完全崩溃。


When should we use assert? In no particular order, assertions should be used for:
我们什么时候应该使用断言?没有特别的顺序,断言应该用于:


  • defensive programming 防御性编程

  • runtime checks on program logic
    程序逻辑的运行时检查

  • checking contracts (e.g. pre-conditions and post-conditions)
    检查契约(例如前置条件和后置条件)

  • program invariants 程序不变量

  • checked documentation 检查文档


(It's also acceptable to use assertions when testing code, as a sort of quick-and-dirty poor man's unit testing, so long as you accept that the tests simply won't do anything if you run with the -O flag. And I sometimes use assert False to mark code branches that haven't been written yet, and I want them to fail. Although raise NotImplementedError is probably better for that use, if a little more verbose.)
(在测试代码时使用断言也是可以接受的,作为一种快速简便的简陋单元测试,只要你接受在使用-O 标志运行时测试将不起作用。而且我有时会使用assert False来标记尚未编写的代码分支,我希望它们失败。尽管raise NotImplementedError可能更适合这种用途,尽管有点冗长。)


Opinions on assertions vary, because they can be a statement of confidence about the correctness of the code. If you're certain that the code is correct, then assertions are pointless, since they will never fail and you can safely remove them. If you're certain the checks may fail (e.g. when testing input data provided by the user), then you dare not use assert since it may be compiled away and then your checks will be skipped.
对断言的看法各不相同,因为它们可以是对代码正确性的信心声明。如果你确定代码是正确的,那么断言是无意义的,因为它们永远不会失败,你可以安全地删除它们。如果你确定检查可能会失败(例如在测试用户提供的输入数据时),那么你不敢使用assert,因为它可能会被编译掉,然后你的检查将被跳过。


It's the situations in between those two that are interesting, times when you're certain the code is correct but not quite absolutely certain. Perhaps you've missed some odd corner case (we're all only human). In this case an extra runtime check helps reassure you that any errors will be caught as early as possible rather than in distant parts of the code.
介于这两者之间的情况才是有趣的,当你确定代码是正确的但不完全确定时。也许你错过了一些奇怪的边缘情况(我们都是人)。在这种情况下,额外的运行时检查有助于确保任何错误尽早被捕获,而不是在代码的远处部分。


(This is why assert can be divisive. Since we can vary in our confidence about the correctness of code, one person's useful assertion may be another person's useless runtime test.)
(这就是为什么assert可能具有分歧性。由于我们对代码正确性的信心可能不同,一个人有用的断言可能是另一个人无用的运行时测试。)


Another good use for assertions is checking program invariants. An invariant is some condition which you can rely on to be true unless a bug causes it to become false. If there's a bug, better to find out as early as possible rather than later in some far off distant piece of code, so we make a test for it, but we don't want to slow the code down with such tests. Hence assert, which can be left on in development and off in production by running with the -O switch.
另一个使用断言的好方法是检查程序不变量。不变量是某种条件,你可以依赖它为真,除非一个错误导致它变为假。如果有错误,最好尽早发现,而不是在代码的某个遥远部分,所以我们为它做一个测试,但我们不希望这些测试减慢代码的速度。因此assert,可以在开发中保持开启,在生产中通过使用-O 开关关闭。


An example of an invariant might be, if your function expects a database connection to be open when it starts, and promises that it will still be open when it returns, that's an invariant of the function:
一个不变量的例子可能是,如果你的函数在开始时期望数据库连接是打开的,并且承诺在返回时它仍然是打开的,那就是函数的不变量:


def some_function(arg):
    assert not DB.closed()
    ... # code goes here
    assert not DB.closed()
    return result


Assertions also make good checked comments. Instead of writing a comment:
断言也可以作为良好的检查注释。与其写一个注释:


# when we reach here, we know that n > 2


you can ensure it is checked at runtime by turning it into an assertion:
你可以通过将其转化为断言来确保它在运行时被检查:


assert n > 2


Assertions are also a form of defensive programming. You're not protecting against errors in the code as it is now, but protecting against changes which introduce errors later. Ideally, unit tests will pick those up, but let's face it, even when tests exist at all, they're often incomplete. Build-bots can be down and nobody notices for weeks, or people forget to run tests before committing code. Having an internal check is one more line of defence against errors sneaking in, especially those which don't noisily fail but cause the code to malfunction and return incorrect results.
断言也是一种防御性编程。你不是在保护当前代码中的错误,而是在保护引入错误的更改。理想情况下,单元测试会捕捉到这些错误,但让我们面对现实,即使测试存在,它们也常常不完整。构建机器人可能会停机数周而无人注意,或者人们忘记在提交代码前运行测试。拥有一个内部检查是防止错误潜入的另一道防线,特别是那些不会明显失败但会导致代码故障并返回错误结果的错误。


Suppose you have a series of if...elif blocks, where you know ahead of time what values some variable is expected to have:
假设你有一系列if...elif块,你提前知道某个变量预期会有的值:


# target is expected to be one of x, y, or z, and nothing else.
if target == x:
    run_x_code()
elif target == y:
    run_y_code()
else:
    run_z_code()


Assume that this code is completely correct now. But will it stay correct? Requirements change. Code changes. What happens if the requirements change to allow target = w, with associated action run_w_code? If we change the code that sets target, but neglect to change this block of code, it will wrongly call run_z_code() and Bad Things will occur. It would be good to write this block of code defensively, so that it will either be correct, or fail immediately, even in the face of future changes.
假设这段代码现在是完全正确的。但它会保持正确吗?需求会改变。代码会改变。如果需求改变以允许target = w,并且有相关的操作run_w_code会发生什么?如果我们更改设置 target 的代码,但忽略更改这段代码块,它将错误地调用run_z_code(),并且会发生不好的事情。最好以防御性方式编写这段代码,使其要么正确,要么立即失败,即使在未来的更改面前也是如此。


The comment at the start of the block is a good first step, but people are notorious for failing to read and update comments. Chances are it will soon be obsolete. But with an assertion, we can both document the assumptions of this block, and cause a clean, immediate failure if they are violated:
块开头的注释是一个好的第一步,但人们以不阅读和更新注释而闻名。很可能它很快就会过时。但通过断言,我们可以同时记录这个块的假设,并在它们被违反时导致干净、立即的失败:


assert target in (x, y, z)
if target == x:
    run_x_code()
elif target == y:
    run_y_code()
else:
    assert target == z
    run_z_code()


Here, the assertions are both defensive programming and checked documentation. I consider this to be a far superior solution than this:
在这里,断言既是防御性编程也是检查文档。我认为这是一个远优于以下方法的解决方案:


if target == x:
    run_x_code()
elif target == y:
    run_y_code()
elif target == z:
    run_z_code()
else:
    # This can never happen. But just in case it does...
    raise RuntimeError("an unexpected error occurred")


This tempts some helpful developer to "clean it up" by removing the "unnecessary" test for target == z and removing the "dead code" of the RuntimeError. Besides, "unexpected error" messages are embarrassing when they occur, and they will.
这会诱使一些热心的开发人员通过删除“多余的”target == z测试和删除 RuntimeError 的“死代码”来“清理”它。此外,“意外错误”消息在发生时是令人尴尬的,而且它们会发生。


Design by contract is another good use of assertions. In design by contract, we consider that functions make "contracts" with their callers. E.g. something like this:
契约设计是另一个使用断言的好方法。在契约设计中,我们认为函数与其调用者达成“契约”。例如,像这样:


"If you pass me an non-empty string, I guarantee to return the first character of that string converted to uppercase."
“如果你传给我一个非空字符串,我保证返回该字符串的第一个字符并将其转换为大写。”


If the contract is broken by either the function or the code calling it, the code is buggy. We say that functions have pre-conditions (the constraints that arguments are expected to have) and post-conditions (the constraints on the return result). So this function might be coded as:
如果契约被函数或调用它的代码破坏了,那么代码就是有问题的。我们说函数有前置条件(参数预期具有的约束)和后置条件(返回结果的约束)。所以这个函数可能会被编码为:


def first_upper(astring):
    # Check the pre-conditions.
    assert isinstance(astring, str) and len(astring) > 0
    result = astring[0].upper()
    # Check the post-conditions.
    assert isinstance(result, str) and len(result) == 1
    # We cannot use isupper() here, because the char might not be a letter.
    assert result == result.upper()
    return result


The aim of Design By Contract is that in a correct program, the pre-conditions and post-conditions will always hold. Assertions are typically used, since (so the idea goes) by the time we release the bug-free program and put it into production, the program will be correct and we can safely remove the checks.
契约设计的目标是在一个正确的程序中,前置条件和后置条件将始终成立。通常使用断言,因为(理论上)当我们发布无错误的程序并将其投入生产时,程序将是正确的,我们可以安全地删除这些检查。


It's important here to realise that contracts apply between parts of a single program. They are inappropriate between (say) a library and the caller of the library. Since the library cannot trust that callers will honour contracts, it is not appropriate to disable the contract checking (particularly the pre-condition checks), and so libraries should not rely on assert for error checking (at least not for parts of the public API). Instead, they should perform the test and explicitly use raise if the text fails.
这里重要的是要意识到契约适用于单个程序的各个部分之间。它们不适用于(例如)库和库的调用者之间。由于库不能信任调用者会遵守契约,因此不适合禁用契约检查(特别是前置条件检查),因此库不应依赖assert进行错误检查(至少不适用于公共 API 的部分)。相反,如果测试失败,它们应该执行测试并明确使用raise


Here's my advice when not to use assertions:
这是我关于使用断言的建议:


  • Never use them for testing user-supplied data, or for anything where the check must take place under all circumstances.
    永远不要用它们来测试用户提供的数据,或任何在所有情况下都必须进行检查的事情。

  • Don't use assert for checking anything that you expect might fail in the ordinary use of your program. Assertions are for extraordinary failure conditions. Your users should never see an AssertionError; if they do, it's a bug in your code to be fixed.
    不要使用断言来检查你期望在程序的正常使用中可能会失败的任何事情。断言是用于非凡的失败情况。你的用户永远不应该看到AssertionError;如果他们看到了,那是你代码中的一个错误,需要修复。

  • In particular, don't use assert just because it's shorter than an explicit test followed by a raise. assert is not a shortcut for lazy coders.
    特别是,不要使用assert只是因为它比显式测试后跟一个 raise 更短。assert不是懒惰编码者的捷径。

  • Don't use them for checking input arguments to public library functions (private ones are okay) since you don't control the caller and can't guarantee that it will never break the function's contract.
    不要使用它们来检查公共库函数的输入参数(私有函数可以),因为你无法控制调用者,也无法保证它不会破坏函数的契约。

  • Don't use assert for any error which you expect to recover from. In other words, you've got no reason to catch an AssertionError exception in production code.
    不要对任何你期望恢复的错误使用 assert。换句话说,你没有理由在生产代码中捕获 AssertionError 异常。

  • Don't use so many assertions that they obscure the code.
    不要使用太多的断言,以至于掩盖了代码。


An earlier version of this post appeared here.
这篇文章的早期版本出现在这里

Profile 个人资料

import_that: XKCD guy flying with Python (Default)
Steven D'Aprano

May 2015
五月 2015 年

S M T W T F S
     12
345678 9
10111213141516
17181920212223
24252627282930
31      

Most Popular Tags 最受欢迎的标签

Style Credit 样式来源

Expand Cut Tags 展开隐藏标签

No cut tags 没有隐藏标签