【问题标题】:Best practice for using assert?使用断言的最佳实践?
【发布时间】:2010-10-30 23:37:07
【问题描述】:
  1. assert 用作标准代码的一部分而不是仅用于调试目的是否存在性能或代码维护问题?

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

    比更好或更差

    if x < 0:
        raise Exception, 'x is less than zero'
    
  2. 1234563比如如果你在函数的开头设置assert x &lt; 0,在函数内x 小于0 的任何地方都会引发异常?

【问题讨论】:

标签: python assert assertion raise


【解决方案1】:

当 x 在整个函数中小于零时,能够自动抛出错误。您可以使用class descriptors。这是一个例子:

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

【讨论】:

  • 虽然属性是作为描述符实现的,但我不会将此称为使用它们的示例。这更多是属性本身的示例:docs.python.org/library/functions.html#property
  • 设置 x 时应在 MyClass 中使用属性。这个解决方案太笼统了。
  • 非常好的答案,喜欢它,但与问题无关...我们不能将 Deestan 或 John Mee 的答案标记为有效回答吗?
  • 这似乎无法回答问题的标题。此外,这对于 Python 的类属性功能来说是一个糟糕的替代方案。
  • @VajkHermecz:实际上,如果你重读这个问题,这是一个两个问题。只看标题的人只熟悉第一个问题,这个答案没有回答。这个答案实际上包含了第二个问题的答案。
【解决方案2】:

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

class XLessThanZeroException(Exception):
    pass

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

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

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

【讨论】:

  • 您可以将错误消息附加到断言。这是第二个参数。这将使它具有描述性。
【解决方案3】:

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

异常应该用于可能发生的错误,并且您应该几乎总是创建自己的异常类


例如,如果您正在编写一个从配置文件中读取到dict 的函数,则文件中不正确的格式应该会引发ConfigurationSyntaxError,而您可以assert,而您不会这样做返回None


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

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

【讨论】:

  • 这是使用断言的正确方式。它们不应该用于控制程序流程。
  • +1 最后一段 - 虽然你应该 explicitly 提到 assert 包含一个隐含的 if __debug__ 并且可能是 optimized 离开 - 如 John Mee's answer 状态
  • 重读你的答案我想你可能并不是说不应该发生的情况作为一个规则,而是目的是在早期崩溃程序状态损坏的情况,通常与您不希望发生的情况相吻合.
  • assert 只能用于捕获没有已知恢复的问题;几乎总是代码错误(不错的输入)。当一个断言被触发时,它应该意味着程序处于继续下去的危险状态,因为它可能开始与网络通信或写入磁盘。面对错误(或恶意)输入,健壮的代码会“原子地”从有效状态移动到有效状态。每个线程的顶层都应该有一个故障屏障。消耗来自外部世界的输入的故障屏障通常会在屏障的一次迭代(while/try)、回滚/登录错误中失败。
  • "断言应该用于测试不应该发生的情况。"是的。而第二个“应该”的意思是:如果出现这种情况,说明程序代码不正确。
【解决方案4】:

优化编译时删除“assert”语句。所以,是的,性能和功能都存在差异。

当在编译时请求优化时,当前代码生成器不会为断言语句发出代码。 - Python 2 DocsPython 3 Docs

如果您使用assert 实现应用程序功能,然后优化部署到生产,您将受到“but-it-works-in-dev”缺陷的困扰。

PYTHONOPTIMIZE-O -OO

【讨论】:

  • 哇!超级重要的注意事项!我一直计划使用断言来检查一些永远不会失败的事情,这些事情的失败表明有人非常小心地操纵了他们发送的数据,以试图访问他们不应该访问的数据。这是行不通的,但我想通过断言迅速关闭他们的尝试,因此在生产中进行优化会破坏目的。我想我会用raiseException 代替。哦 - 我刚刚发现了一个恰当地命名为 SuspiciousOperation Exception 的子类在 Django 中!完美!
  • 顺便说一下@ArtOfWarfare,如果你在你的代码上运行bandit,它会警告你。
  • @John Mee,感谢您提供重要信息。我使用 assert 和 oython 版本检查以在所需版本上正确运行。但断言不适用于通过#!/bin/python 在可执行python 脚本中进行版本检查。现在我从你关于断言的信息中找出原因。谢谢。
【解决方案5】:

除了其他答案,断言自己抛出异常,但只有 AssertionErrors。从实用的角度来看,当您需要对捕获的异常进行细粒度控制时,断言不适合。

【讨论】:

  • 对。在调用者中捕获断言错误异常似乎很愚蠢。
  • 非常好。仅从宏观层面看原始问题时很容易忽略的细微差别。即使不是因为优化时断言被丢弃的问题,丢失发生哪种错误的具体细节也会使调试更具挑战性。干杯,outis!
  • 您的答案可以被解读为您可能想抓住AssertionErrors,但您可以接受粗粒度的回答。实际上,您不应该抓住它们。
【解决方案6】:

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

例如,递归函数(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

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

【讨论】:

  • 最好使用装饰器(@precondition 和 @postcondition)
  • @Caridorc 这样做的具体好处是什么?
  • @ChieltenBrinke 自我记录代码,而不是#precondition: n &gt;= 0 和断言,他可以只写@precondition(lambda n: n &gt;= 0)
  • @Caridorc 那是那些内置的装饰器吗?以及如何从中生成文档?
  • @ChieltenBrinke 不是内置但易于实现 stackoverflow.com/questions/12151182/… 。对于文档,只需通过提供附加字符串来修补 __doc__ 属性
【解决方案7】:

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 代码中崩溃, 伯恩德可能会认为这是爱丽丝的错, 爱丽丝调查并可能认为这是你的错, 你调查并告诉伯恩德这实际上是他的。 很多工作丢失了。
    使用断言,无论谁接错电话,他们都可以很快看到 他们的错,不是你的。爱丽丝、伯恩德和你们都会受益。 节省大量时间。

  3. 告知您的代码的读者(包括您自己)您的代码在某个时候取得了哪些成就。
    假设您有一个条目列表,并且每个条目都可以是干净的(这很好) 或者它可以是 smorsh、trale、gullup 或 twinkled(这些都是不可接受的)。 如果是 smorsh,则必须是 unsmorshed;如果它是 trale,它必须是 baludoed; 如果是 gullup,则必须小跑(然后也可能有节奏); 如果它闪烁,则必须再次闪烁,周四除外。 你明白了:这是复杂的东西。 但最终结果是(或应该是)所有条目都是干净的。 The Right Thing(TM) 要做的是总结你的效果 清洗循环为

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

    这句话让每个试图理解的人都头疼 正是,美妙的循环正在实现。 而这些人中最常出现的可能是你自己。

  4. 通知计算机您的代码在某个时间点实现了什么。
    如果您在小跑后忘记调整需要它的条目的速度, assert 将节省您的时间并避免您的代码 亲爱的达芙妮很久以后才打破的。

在我看来,assert 的两个文档目的(1 和 3)和 保障(2 和 4)同样有价值。
通知人们甚至可能比通知计算机更有价值 因为它可以防止assert 旨在捕获的错误(在第 1 种情况下) 在任何情况下都有很多后续错误。

【讨论】:

  • 5. assert isinstance() 帮助 PyCharm (python IDE) 知道变量的类型,它用于自动完成。
  • 断言自文档代码假设在当前执行时为真。这是一个假设注释,会被检查。
  • 关于 2 和 4:您应该非常小心,您的断言不要太严格。否则,断言本身可能是唯一让您的程序在更一般的环境中使用的东西。尤其是断言类型违背了 python 的鸭子类型。
  • @Cjkjvfnby 请注意 isinstance() 的过度使用,如此博客条目中所述:“isinstance() considered harmful”。你现在可以在 Pycharm 中use docstrings to specify types
  • 以一种确保契约的方式使用断言。有关按合同设计的更多信息en.wikipedia.org/wiki/Design_by_contract
【解决方案8】:

是否存在性能问题?

  • 请记住“在快速运行之前先让它运行”
    任何程序中只有很少的百分比通常与其速度相关。 你总是可以踢出或简化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!
    

    这很便宜,但很可能会捕获大多数实际程序错误。

【讨论】:

  • 应该是assert(len(listOfTuples)==0 or type(listOfTyples[0])==tuple)
  • 不,不应该。那将是一个弱得多的测试,因为它不再检查第二个断言检查的“非空”属性。 (第一个没有,虽然它应该。)
  • 第二个断言没有明确检查非空属性;这更像是一个副作用。如果由于列表为空而引发异常,则使用代码的人(其他人或作者,在编写代码一年后)会盯着它,试图弄清楚断言是否真的是为了捕捉空列表的情况,或者如果这是断言本身的错误。此外,我看不出不检查空案例“弱得多”,而只检查第一个元素是“97% 正确”。
  • @SergeyOrshanskiy 它要弱得多,因为列表不为空也是一个前提条件,您的检查不会检测到是否违反了该前提条件。 (我同意 assert(type(listOfTuples[0])==tuple) 的失败在这种情况下可能会令人困惑。)
  • 让你的断言更快并不是真正有用,因为在生产代码中(使用python -O),它们根本不会运行
【解决方案9】:

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

【讨论】:

  • 这似乎早于使用类型注释或typing.cast
  • cf cmets on Lutz Prechelt's answer(通常不建议这样做,因为您有更好的方法来指定类型,例如类型提示)
【解决方案10】:

断言是检查 -
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' 为空。因此,我们通过 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
什么都不会被打印出来

【讨论】:

    【解决方案11】:

    这里的英文单词 assert 用于 swear确认承认。这并不意味着 “检查”“应该”。这意味着作为一名编码员正在这里作出宣誓声明

    # 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
    

    如果代码正确,除非Single-event upsets、硬件故障等,任何断言都不会失败。这就是为什么程序对最终用户的行为不能受到影响的原因。尤其是,即使在异常编程条件下,断言也不会失败。它只是永远不会发生。如果发生这种情况,程序员应该为此受到打击。

    【讨论】:

      【解决方案12】:

      不管怎样,如果您正在处理依赖 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
      

      【讨论】:

      • 这没有回答 OP 关于最佳实践的问题。
      【解决方案13】:

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

      目的

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

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

      风格

      在 Python 中,assert 是一个语句,而不是一个函数! (记住assert(False, 'is true') 不会加注。但是,别碍事:

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

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

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

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

      def 转储(某些东西): 断言 isinstance(某事,可转储) # ...

      但除此之外,消息对于与其他程序员(有时是您的代码的交互式用户,例如在 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'
      
      • 这是两个互相矛盾的东西。因此,每当我对代码库产生影响时,我都会通过使用“必须”和“应该”等额外动词来明确我们想要什么,而不是说我们不想要什么。

        assert a == b, 'a 必须等于 b'

      然后,获取AssertionError: a must be equal to b 也是可读的,并且该语句在代码中看起来是合乎逻辑的。此外,您可以在不阅读回溯的情况下从中获得一些东西(有时甚至不可用)。

      【讨论】:

        【解决方案14】:

        assert 的使用和异常的引发都是关于通信的。

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

        • 异常是关于可能在运行时发生但无法通过手头代码解决的非典型情况的指示,在要处理的调用代码处解决。出现异常并不代表代码有bug。

        最佳实践

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

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

        使用 assert 是否存在性能 [...] 问题

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

        使用 assert 是否存在 [...] 代码维护问题

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

        【讨论】:

          【解决方案15】:

          我要补充一点,我经常使用 assert 来指定属性,例如 loop invariants 或我的代码应该具有的逻辑属性,就像我在经过正式验证的软件中指定它们一样。

          它们的目的是通知读者,帮助我推理,并检查我的推理没有犯错误。例如:

          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 时断言被禁用,所以不要犹豫在其中编写代价高昂的条件,尤其是如果它使您的代码更清晰不易出错

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多