使用断言的最佳实践?

2025-01-13 08:53:00
admin
原创
104
摘要:问题描述:assert将其用作标准代码的一部分而不是仅用于调试目的是否存在性能或代码维护问题?是assert x >= 0, 'x is less than zero' 优于或劣于if x < 0: raise Exception('x is less than zero') 此外,有没有...

问题描述:

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

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

优于或劣于

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


解决方案 1:

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

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


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


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

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

解决方案 2:

编译优化时会删除“assert”语句。因此,性能和功能上确实存在差异。

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

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

参见PYTHONOPTIMIZE和-O -OO

解决方案 3:

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

解决方案 4:

四个目的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))

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

  1. 告知计算机您的代码需要什么。

assert强制代码调用者采取正确的行为。如果您的代码调用 Alice 的代码,而 Bernd 的代码调用您的代码,那么如果没有assert,如果程序在 Alice 的代码中崩溃,Bernd 可能会认为这是 Alice 的错误,Alice 会进行调查,并可能认为这是您的错误,您进行调查并告诉 Bernd 这实际上是他的错误。大量工作付诸东流。

有了断言,无论谁调用错误,他们都会很快发现这是他们的错误,而不是您的错误。Alice、Bernd 和你们都会受益。节省大量时间。

  1. 告知代码的读者(包括您自己)您的代码在某个时刻取得了什么成就。

假设您有一个条目列表,每个条目都可以是干净的(这很好),也可以是 smorsh、trale、gullup 或 twinkled(这些都不可接受)。如果是 smorsh,则必须 unsmorshed;如果是 trale,则必须 baludoed;如果是 gullup,则必须 trotted(然后可能还要 paced);如果是 twinkled,则必须再次 twinled,星期四除外。您明白了:这是很复杂的事情。但最终结果是(或应该是)所有条目都是干净的。正确的做法是将清理循环的效果总结为

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

这句话让那些想知道这个奇妙的循环到底在做什么的人省去了头疼。这些人中最常见的人可能就是你。

  1. 在某个时刻告知计算机您的代码已实现什么。

如果您在跑步后忘记了设置需要它的条目,这assert将节省您的时间并避免您的代码在很久以后被破坏。

在我看来,assert记录(1 和 3)和保护(2 和 4)这两个目的同等重要。

告知人们甚至可能比告知计算机有价值,因为它可以防止想要捕捉的错误assert(在情况 1 中)以及无论如何大量后续错误。

解决方案 5:

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

解决方案 6:

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

class XLessThanZeroException(Exception):
    pass

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

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

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

解决方案 7:

此处的英语单词assert的用法是发誓肯定承认。它不表示“检查”“应该”。它的意思是,作为一名程序员在此做出宣誓声明

# 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

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

解决方案 9:

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

解决方案 10:

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

目的

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

接下来,它是为了“调试目的”,虽然正确,但听起来很不屑一顾。我更喜欢“声明不变量,永远不应违反”的表述,尽管它对不同的初学者有不同的作用……有些人“刚好明白”,而其他人要么觉得它没什么用,要么用它来代替正常的异常,甚至控制流程。

风格

在 Python 中,assert是一个语句,而不是一个函数!(记住assert(False, 'is true')不会引发。但是,有了它:

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

这实际上适用于单元测试框架,这些框架通常有许多专用方法来执行断言(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也是可读的,并且该语句在代码中看起来合乎逻辑。此外,您可以从中获取一些内容而无需阅读回溯(有时甚至无法获得)。

解决方案 11:

是否存在性能问题?

  • 请记住“先让它运行起来,然后再让它运行得更快”

任何程序中只有极少数部分与速度有关。如果某个部分被证实存在性能问题,您可以随时将其删除或简化assert——而且大多数程序永远不会出现性能问题。

  • 务实

假设您有一个处理非空元组列表的方法,如果这些元组不是不可变的,程序逻辑就会中断。您应该这样写:

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

如果您的列表通常只有 10 个条目,那么这可能没问题,但如果列表有 100 万个条目,那么这可能会成为问题。但与其完全丢弃这个有价值的检查,不如简单地将其降级​​为

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

虽然这种方法成本低廉,但无论如何都能够捕获大多数实际程序错误。

解决方案 12:

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

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

  • 异常是指运行时可能发生但无法由当前代码解决的非典型情况,由调用代码处理。发生异常并不表示代码中存在错误。

最佳实践

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

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

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

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

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

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

解决方案 13:

断言用于检查源代码的 -

**1. 有效条件,

  1. 有效语句,

  2. 真实逻辑**
    。它不会让整个项目失败,而是发出警报,指出源文件中存在不合适的内容。

在示例 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:/Pythonhello.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

不会打印任何内容

解决方案 14:

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

解决方案 15:

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

解决方案 16:

设置业务规则,例如if x < 0 raise error

据我所知,对于此问题没有通用的(独立于实现的)解决方案。

然而,如果x是某个类的对象的属性C,则可以通过将普通属性转换x为不仅具有 getter 而且具有 setter 的属性来实现: x

https://docs.python.org/3/library/functions.html#property

然后,制定者可以实施并执行业务规则。

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用