python学习笔记---错误、调试和测试【廖雪峰】

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

错误、调试和测试

①三类错误

●有的错误是程序编写有问题造成的比如本来应该输出整数结果输出了字符串这种错误我们通常称之为bugbug是必须修复的。

●有的错误是用户输入造成的比如让用户输入email地址结果得到一个空字符串这种错误可以通过检查用户输入来做相应的处理。

●还有一类错误是完全无法在程序运行过程中预测的比如写入文件的时候磁盘满了写不进去了或者从网络抓取数据网络突然断掉了。这类错误也称为异常在程序中通常是必须处理的否则程序会因为各种问题终止并退出。

错误处理

try…except…finally…

让我们用一个例子来看看try的机制

try:
    print('try...')
    r = 10 / 0
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
finally:
    print('finally...')
print('END')

​ 当我们认为某些代码可能会出错时就可以用try来运行这段代码如果执行出错则后续代码不会继续执行而是直接跳转至错误处理代码即except语句块执行完except如果有finally语句块则执行finally语句块至此执行完毕【finally如果有则一定会被执行可以没有finally语句】

Python的错误其实也是class所有的错误类型都继承自BaseException所以在使用except时需要注意的是它不但捕获该类型的错误还把其子类也“一网打尽”


使用try...except捕获错误还有一个巨大的好处就是可以跨越多层调用比如函数main()调用bar()bar()调用foo()结果foo()出错了这时只要main()捕获到了就可以处理

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')

也就是说不需要在每个可能出错的地方去捕获错误只要在合适的层次去捕获错误就可以了。这样一来就大大减少了写try...except...finally的麻烦。

调用栈

如果错误没有被捕获它就会一直往上抛最后被Python解释器捕获打印一个错误信息然后程序退出。

$ python3 err.py
Traceback (most recent call last):
  File "err.py", line 11, in <module>
    main()
  File "err.py", line 9, in main
    bar('0')
  File "err.py", line 6, in bar
    return foo(s) * 2
  File "err.py", line 3, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero

记录错误

​ 如果不捕获错误自然可以让Python解释器来打印出错误堆栈但程序也被结束了既然我们能捕获错误就可以把错误堆栈打印出来然后分析错误原因同时让程序继续执行下去

logging:可以写入日记中

# err_logging.py

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
         logging.exception(e)

main()
print('END')
$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
  File "err_logging.py", line 13, in main
    bar('0')
  File "err_logging.py", line 9, in bar
    return foo(s) * 2
  File "err_logging.py", line 6, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END

image-20230113194603453

抛出错误raise

​ 因为错误是class捕获一个错误就是捕获到该class的一个实例。因此错误并不是凭空产生的而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误我们自己编写的函数也可以抛出错误。

​ 如果要抛出错误首先根据需要可以定义一个错误的class选择好继承关系然后用==raise语句==抛出一个错误的实例

# err_raise.py
class FooError(ValueError):
    pass

def foo(s):
    n = int(s)
    if n==0:
        raise FooError('invalid value: %s' % s)
    return 10 / n

foo('0')

执行可以最后跟踪到我们自己定义的错误

$ python3 err_raise.py 
Traceback (most recent call last):
  File "err_throw.py", line 11, in <module>
    foo('0')
  File "err_throw.py", line 8, in foo
    raise FooError('invalid value: %s' % s)
__main__.FooError: invalid value: 0

只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型比如ValueErrorTypeError尽量使用Python内置的错误类型。


程序也可以**主动抛出错误**让调用者来处理相应的错误。但是应该在文档中写清楚可能会抛出哪些错误以及错误产生的原因。

# err_reraise.py

def foo(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n

def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise

bar()

bar()函数中我们明明已经捕获了错误但是打印一个ValueError!后又把错误通过raise语句抛出去了这不有病么

其实这种错误处理方式不但没病而且相当常见。捕获错误目的只是记录一下便于后续追踪。但是由于当前函数不知道应该怎么处理该错误所以最恰当的方式是继续往上抛让顶层调用者去处理。好比一个员工处理不了一个问题时就把问题抛给他的老板如果他的老板也处理不了就一直往上抛最终会抛给CEO去处理。


raise语句如果不带参数就会把当前错误原样抛出。此外exceptraise一个Error还可以把一种类型的错误转化成另一种类型

try:
    10 / 0
except ZeroDivisionError:
    raise ValueError('input error!')

只要是合理的转换逻辑就可以但是决不应该把一个IOError转换成毫不相干的ValueError

调试

第一种方法简单直接粗暴有效就是用print()把可能有问题的变量打印出来看看

def foo(s):
    n = int(s)
    print('>>> n = %d' % n)
    return 10 / n

def main():
    foo('0')

main()

断言assert

凡是用print()来辅助查看的地方都可以用断言assert来替代

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

assert的意思是表达式n != 0应该是True否则根据程序运行的逻辑后面的代码肯定会出错。

如果断言失败assert语句本身就会抛出AssertionError

$ python err.py
Traceback (most recent call last):
  ...
AssertionError: n is zero!

程序中如果到处充斥着assertprint()相比也好不到哪去。不过启动Python解释器时可以用-O参数来关闭assert[英文大写字母O]

$ python -O err.py
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

关闭后你可以把所有的assert语句当成pass来看

logging

print()替换为logging是第3种方式和assertlogging不会抛出错误而且可以输出到文件

import logging
logging.basicConfig(level=logging.INFO)
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

输出

$ python err.py
INFO:root:n = 0
Traceback (most recent call last):
  File "err.py", line 8, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

这就是logging的好处它允许你指定记录信息的级别debuginfowarningerror等几个级别当我们指定level=INFOlogging.debug就不起作用了。同理指定level=WARNINGdebuginfo就不起作用了。这样一来你可以放心地输出不同级别的信息也不用删除最后统一控制输出哪个级别的信息。

logging的另一个好处是通过简单的配置一条语句可以同时输出到不同的地方比如console和文件。


【Python logging 模块定义了为应用程序和库实现灵活的事件日志记录的函数和类。】

​ 程序开发过程中很多程序都有记录日志的需求并且日志包含的信息有正常的程序访问日志还可能有错误、警告等信息输出Python 的 logging 模块提供了标准的日志接口可以通过它存储各种格式的日志,日志记录提供了一组便利功能用于简单的日志记录用法。

日志记录函数以它们用来跟踪的事件的级别或严重性命名。下面描述了标准级别及其适用性从高到低的顺序

日志级别等级排序critical > error > warning > info > debug

级别越高打印的日志越少反之亦然即

  • debug : 打印全部的日志( notset 等同于 debug )
  • info : 打印 info, warning, error, critical 级别的日志
  • warning : 打印 warning, error, critical 级别的日志
  • error : 打印 error, critical 级别的日志
  • critical : 打印 critical 级别

Logging 模块日志记录方式

一种方式是使用 Logging 提供的模块级别的函数

import logging

# 打印日志级别
def test_logging():
    logging.debug('Python debug')
    logging.info('Python info')
    logging.warning('Python warning')
    logging.error('Python Error')
    logging.critical('Python critical')

test_logging()
WARNING:root:Python warning
ERROR:root:Python Error
CRITICAL:root:Python critical

当指定一个日志级别之后会记录大于或等于这个日志级别的日志信息小于的将会被丢弃 默认情况下日志打印只显示大于等于 WARNING 级别[critical > error > warning]的日志。


通过==logging.basicConfig()==可以设置 root 的日志级别和日志输出格式。

import logging

# 打印日志级别
def test():
 logging.basicConfig(level=logging.DEBUG) # 需要在开头就设置在中间设置并无作用
 logging.debug('Python debug')
 logging.info('Python info')
 logging.warning('Python warning')
 logging.error('Python Error')
 logging.critical('Python critical')
 logging.log(2,'test')
test()

pdb

第4种方式是启动Python的调试器pdb让程序以单步方式运行可以随时查看运行状态。

这种通过pdb在命令行调试的方法理论上是万能的但实在是太麻烦了如果有一千行代码要运行到第999行得敲多少命令啊。还好我们还有另一种调试方法。

pdb.set_trace()

这个方法也是用pdb但是不需要单步执行我们只需要import pdb然后在可能出错的地方放一个pdb.set_trace()就可以设置一个断点

# err.py
import pdb

s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)

运行代码程序会自动在pdb.set_trace()暂停并进入pdb调试环境可以用命令p查看变量或者用命令c继续运行

$ python err.py 
> /Users/michael/Github/learn-python3/samples/debug/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
  File "err.py", line 7, in <module>
    print(10 / n)
ZeroDivisionError: division by zero

这个方式比直接启动pdb单步调试效率要高很多但也高不到哪去

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

单元测试通过后有什么意义呢如果我们对abs()函数代码做了修改只需要再跑一遍单元测试如果通过说明我们的修改不会对abs()函数原有的行为造成影响如果测试不通过说明我们的修改与原有行为不一致要么修改代码要么修改测试。

这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候可以极大程度地保证该模块行为仍然是正确的。

另外我们需要明确一下单元测试的编写规则

1、测试文件名须以“test_”开头或者以”_test”结尾

2、编写单元测试时我们需要编写一个测试类从 unittest.TestCase 继承

3、以 test 开头的方法就是测试方法不以 test 开头的方法不被认为是测试方法测试的时候不会被执行。

4、对每一类测试都需要编写一个 test_xxx() 方法使用assert断言

至此即可完成项目基础的单元测试单元测试很重要不仅可以帮我们快速定位为题也方便我们重构测试需要我们开发人员足够重视。

import unittest

class Dict(dict):

    def __init__(self, **kw):
        super().__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value


class TestDict(unittest.TestCase):

    def test_init(self):
        d = Dict(a=1, b='test')
        self.assertEqual(d.a, 1)
        self.assertEqual(d.b, 'test')
        self.assertTrue(isinstance(d, dict))

    def test_key(self):
        d = Dict()
        d['key'] = 'value'
        self.assertEqual(d.key, 'value')

    def test_attr(self):
        d = Dict()
        d.key = 'value'
        self.assertTrue('key' in d)
        self.assertEqual(d['key'], 'value')

    def test_keyerror(self):
        d = Dict()
        with self.assertRaises(KeyError):
            value = d['empty']

    def test_attrerror(self):
        d = Dict()
        with self.assertRaises(AttributeError):
            value = d.empty

if __name__ == '__main__':
    unittest.main()

编写单元测试时我们需要编写一个测试类unittest.TestCase继承

test开头的方法就是测试方法不以test开头的方法不被认为是测试方法测试的时候不会被执行。


对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual()

self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等

另一种重要的断言就是期待抛出指定类型的Error比如通过d['empty']访问不存在的key时断言会抛出KeyError

with self.assertRaises(KeyError):
    value = d['empty']

而通过d.empty访问不存在的key时我们期待抛出AttributeError

with self.assertRaises(AttributeError):
    value = d.empty

image-20230113212718103

当然也会有其他用途的断言比如抛错、日志等

image-20230113212734226

setUp与tearDown

可以在单元测试中编写两个特殊的setUp()tearDown()方法。这两个方法会分别在==每调用一个测试方法的前后分别被执行==。

setUp()tearDown()方法有什么用呢设想你的测试需要启动一个数据库这时就可以在setUp()方法中连接数据库在tearDown()方法中关闭数据库这样不必在每个测试方法中重复相同的代码

class TestDict(unittest.TestCase):

    def setUp(self):
        print('setUp...')

    def tearDown(self):
        print('tearDown...')

可以再次运行测试看看每个测试方法调用前后是否会打印出setUp...tearDown...

with open("/tmp/foo.txt") as file:
    data = file.read()

with用法

https://blog.csdn.net/bitcarmanlee/article/details/52745676

1.With语句是什么?

​ 有一些任务可能事先需要设置事后做清理工作。对于这种场景Python的with语句提供了一种非常方便的处理方式。一个很好的例子是文件处理你需要获取一个文件句柄从文件中读取数据然后关闭文件句柄。

如果不用with语句代码如下

file = open("/tmp/foo.txt")
data = file.read()
file.close()

这里有两个问题:

一是可能忘记关闭文件句柄
二是文件读取数据发生异常没有进行任何处理。

下面是处理异常的加强版本

try:
    f = open('xxx')
except:
    print 'fail to open'
    exit(-1)
try:
    do something
except:
    do something
finally:
     f.close()
1234567891011

虽然这段代码运行良好但是太冗长了

这时候就是with一展身手的时候了。除了有更优雅的语法with还可以很好的处理上下文环境产生的异常。

下面是with版本的代码

with open("/tmp/foo.txt") as file:
    data = file.read()

2.with如何工作?

紧跟with后面的语句被求值后返回**对象的 _enter_() 方法被调用这个方法的返回值将被赋值给as后面的变量**。
当with后面的代码块全部被执行完之后将调用前面返回**对象的 _exit_() 方法**。

下面例子可以具体说明with如何工作

#!/usr/bin/env python
# with_example01.py
class Sample:
    def __enter__(self):
        print "In __enter__()"
        return "Foo"
    def __exit__(self, type, value, trace):
        print "In __exit__()"
def get_sample():
    return Sample()
with get_sample() as sample:
    print "sample:", sample

运行代码输出如下

bash-3.2$ ./with_example01.py
In __enter__()
sample: Foo
In __exit__()

正如你看到的: 1. _enter_()方法被执行 2. _enter_()方法返回的值 - 这个例子中是”Foo”赋值给变量’sample’ 3. 执行代码块打印变量”sample”的值为 “Foo” 4. _exit_()方法被调用


​ with真正强大之处是它**可以处理异常**。可能你已经注意到Sample类的 _exit_()方法有三个参数 val, type 和 trace。 这些参数在异常处理中相当有用。我们来改一下代码看看具体如何工作的

#!/usr/bin/env python
# with_example02.py
class Sample:
    def __enter__(self):
        return self
    def __exit__(self, type, value, trace):
        print "type:", type
        print "value:", value
        print "trace:", trace
    def do_something(self):
        bar = 1/0
        return bar + 10
with Sample() as sample:
    sample.do_something()

​ 这个例子中with后面的get_sample()变成了Sample()。这没有任何关系只要紧跟with后面的语句所返回的对象有 _enter_()_exit_()方法即可。此例中Sample()的 _enter_()方法返回新创建的Sample对象并赋值给变量sample。

代码执行后

Traceback (most recent call last):
  File "C:\Users\23972\PycharmProjects\python_class\demo.py", line 14, in <module>
    sample.do_something()
  File "C:\Users\23972\PycharmProjects\python_class\demo.py", line 11, in do_something
    bar = 1/0
ZeroDivisionError: division by zero
type: <class 'ZeroDivisionError'>
value: division by zero
trace: <traceback object at 0x000001D441DD8F40>

实际上在with后面的代码块抛出任何异常时_exit_()方法被执行。正如例子所示异常抛出时与之关联的typevalue和stack trace传给_exit_()方法因此抛出的ZeroDivisionError异常被打印出来了。开发库时清理资源关闭文件等等操作都可以放在 _exit_()方法当中。

正如你看到的: 1. _enter_()方法被执行 2. _enter_()方法返回的值 - 这个例子中是对象自己self赋值给变量’sample’ 3. 执行代码块sample.do_something() 4.sample.do_something()执行过程中发生异常则抛出异常 5. _exit_()方法被调用

另外_exit_()除了用于tear things down还可以进行异常的监控和处理注意后几个参数。要跳过一个异常只需要返回该函数True即可。

文档测试 doctest

doctest非常有用不但可以用来测试还可以直接作为示例代码。通过某些文档生成工具就可以自动把包含doctest的注释提取出来。用户看文档的时候同时也看到了doctest。

Python内置的“文档测试”doctest模块可以直接提取注释中的代码并执行测试。

doctest严格按照Python交互式命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候可以用...表示中间一大段烦人的输出

# mydict2.py
class Dict(dict):
    '''
    Simple dict but also support access as x.y style.

    >>> d1 = Dict()
    >>> d1['x'] = 100
    >>> d1.x
    100
    >>> d1.y = 200
    >>> d1['y']
    200
    >>> d2 = Dict(a=1, b=2, c='3')
    >>> d2.c
    '3'
    >>> d2['empty']
    Traceback (most recent call last):
        ...
    KeyError: 'empty'
    >>> d2.empty
    Traceback (most recent call last):
        ...
    AttributeError: 'Dict' object has no attribute 'empty'
    '''
    def __init__(self, **kw):
        super(Dict, self).__init__(**kw)

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    def __setattr__(self, key, value):
        self[key] = value

if __name__=='__main__':
    import doctest
    doctest.testmod()

运行python mydict2.py

$ python mydict2.py

什么输出也没有。这说明我们编写的doctest运行都是正确的。如果程序有问题比如把__getattr__()方法注释掉再运行就会报错

$ python mydict2.py
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
Failed example:
    d1.x
Exception raised:
    Traceback (most recent call last):
      ...
    AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
Failed example:
    d2.c
Exception raised:
    Traceback (most recent call last):
      ...
    AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
   2 of   9 in __main__.Dict
***Test Failed*** 2 failures.

注意到最后3行代码。当模块正常导入时doctest不会被执行。只有在命令行直接运行时才执行doctest。所以不必担心doctest会在非测试环境下执行。

Demo

对函数fact(n)编写doctest并执行

def fact(n):
    '''
    Calculate 1*2*...*n

    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
    ...
    ValueError
    '''
    if n < 1:
        raise ValueError()
    if n == 1:
        return 1
    return n * fact(n - 1)

if __name__ == '__main__':
    import doctest
    doctest.testmod()
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: python

“python学习笔记---错误、调试和测试【廖雪峰】” 的相关文章