Python断言的最佳实践

Best practice for Python assert

  • 使用assert作为标准代码的一部分,而不是仅用于调试目的,是否存在性能或代码维护问题?

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

    好或坏于

    1
    2
    if x < 0:
        raise Exception, 'x is less than zero'
  • 此外,是否有任何方法可以设置一个像if x < 0 raise error这样的业务规则,该规则总是在没有try/except/finally的情况下进行检查,因此,如果在整个代码中,x小于0,就会引发错误,就像在函数开始时设置assert x < 0一样,在x变小的函数中的任何地方,都会引发异常D?


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

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

    例如,如果要将函数从配置文件读取到dict,则文件中不正确的格式应引发ConfigurationSyntaxError,而可以使assert不返回None

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

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


    "assert"语句在编译优化时被删除。所以,是的,性能和功能都有区别。

    The current code generator emits no code for an assert statement when optimization is requested at compile time. - Python 2.6.4 Docs

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

    参见pythonoptimeize和-o-oo


    在整个函数中,当x小于零时,能够自动抛出错误。可以使用类描述符。下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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


    assert的四个目的

    假设您与四位同事Alice、Bernd、Carl和Daphne一起处理200000行代码。他们叫你的代码,你叫他们的代码。

    那么,assert有四个角色:

  • 通知Alice、Bernd、Carl和Daphne您的代码需要什么。假设您有一个处理元组列表的方法,并且如果这些元组不可变,则程序逻辑可能中断:

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

    这比文档中的等效信息更可靠更容易维护。

  • 通知计算机您的代码需要什么。assert强制代码调用方采取适当的行为。如果您的代码调用了Alices,Bernd的代码调用了您的,如果没有assert,如果程序在alices代码中崩溃,伯德可能认为是爱丽丝的错,艾丽斯调查,可能会认为是你的错,你调查并告诉伯恩德这实际上是他的。损失了很多工作。有了断言,无论谁打错电话,他们都能很快看到他们的错,不是你的错。爱丽丝,伯纳德,你们都受益。节省大量时间。

  • 告诉读者您的代码(包括您自己)您的代码在某个时刻已经实现了什么。假设您有一个条目列表,并且每个条目都可以是干净的(这很好)也可以是smorsh、trale、gullup或flinkle(这些都是不可接受的)。如果它是烟雾,它一定是不郁闷的;如果它是垃圾,它一定是栏杆做的;如果它是海鸥,它必须小跑(然后可能步调,也);如果它闪烁,必须再次闪烁,除了星期四。你明白了:这是复杂的事情。但最终的结果是(或应该是)所有条目都是干净的。正确的做法是总结清洁回路组件

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

    这句话让每个试图理解的人都头疼。正是这个奇妙的循环正在实现。这些人中最频繁的可能就是你自己。

  • 告诉计算机你的代码在某个时刻已经实现了什么。如果你在小跑后忘了用步调调整一个需要的项目,assert将节省您的一天,并避免您的代码亲爱的达芙妮很晚才回来。

  • 在我看来,assert的两个文档目的(1和3)和保障措施(2和4)同样有价值。通知人们可能比通知计算机更有价值。因为它可以防止assert想要抓住的错误(在案例1中)在任何情况下都会有很多后续的错误。


    除了其他答案外,断言本身抛出异常,但只抛出断言错误。从功利主义的角度来看,断言不适用于需要精细控制捕获哪些异常的情况。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class XLessThanZeroException(Exception):
        pass

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

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

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


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

    例如,递归函数(2个独立的函数,1个处理错误的输入,另一个处理错误的代码,因为很难区分递归)。如果我忘了写if语句,什么地方出错了,这就很明显了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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

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


    这里使用的英语单词assert是指宣誓、肯定、声明。这并不意味着"检查"或"应该检查"。这意味着你作为一个编码员在这里做了一个宣誓声明:

    1
    2
    3
    # 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

    如果代码是正确的,除了单个事件的混乱、硬件故障等,没有任何断言会失败。这就是为什么程序对最终用户的行为不能受到影响的原因。尤其是,即使在特殊的程序条件下,断言也不会失败。这是永远不会发生的。如果发生这种情况,程序员应该为此而受到抨击。


    是否存在性能问题?

    • 请记住"先让它工作,然后让它快速工作"。任何程序中很少有与速度相关的部分。如果一个assert被证明是成为一个性能问题——而且他们中的大多数永远不会。

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

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

      如果您的列表往往有十个条目长,这可能很好,但是如果他们有一百万个条目,这可能会成为一个问题。但你可以不把这张有价值的支票全部扔掉只需将其降级为

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

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


    断言是要检查的-
    1。有效条件,
    2。有效声明,
    三。真逻辑;
    源代码的。它并没有使整个项目失败,而是发出一个警报,提示您的源文件中存在不适当的内容。

    在示例1中,因为变量"str"不是nul。因此不会引发任何断言或异常。

    例1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #!/usr/bin/python

    str = 'hello Pyhton!'
    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 Pyhton!
    FileName ..................... hello
    FilePath ..................... C:/Python\hello.py

    在示例2中,var'str'是nul。所以我们通过断言语句来避免用户提前执行错误的程序。

    例2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #!/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断言语句.py
    什么也得不到印刷品


    Java有一个JBaseDROLLS框架,它运行运行时监控来维护业务规则,它回答了问题的第二部分。但是,我不确定是否有这样一个针对Python的框架。


    在诸如ptvs、pycharm、wing-assert isinstance()等IDE中,可以使用语句为一些不清楚的对象实现代码完成。


    如果您处理的是依赖assert正常工作的遗留代码,即使它不应该正常工作,那么添加以下代码是一种快速修复方法,直到有时间进行重构:

    1
    2
    3
    4
    5
    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