我心中的王者:Python-第15章 程序除错与异常处理

我心中的王者:Python-第15章 程序除错与异常处理

15-1 程序异常

有时也可以将程序错误(error)称作程序异常(exception),相信每一位写程序的人一定会常常碰上程序错误,过去碰上这类情况程序将终止执行,同时出现错误信息,错误信息内容通常是显示Traceback,然后列出异常报告。Python提供功能可以让我们捕捉异常和撰写异常处理程序,当发生异常被我们捕捉时会去执行异常处理程序,然后程序可以继续执行。

15-1-1 一个除数为0的错误

本节将以一个除数为0的错误开始说明。

程序实例ch15_1.py:建立一个除法运算的函数,这个函数将接受2个参数,然后执行第一个参数除以第二个参数。

# ch15_1.py
def division(x, y):
    return x / y

print(division(10, 2))      # 列出10/2
print(division(5, 0))       # 列出5/0
print(division(6, 3))       # 列出6/3

执行结果

5.0
Traceback (most recent call last):
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_1.py", line 6, in <module>
    print(division(5, 0))       # 列出5/0
          ^^^^^^^^^^^^^^
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_1.py", line 3, in division
    return x / y
           ~~^~~
ZeroDivisionError: division by zero

上述程序在执行第5行时,一切还是正常。但是到了执行第6行时,因为第2个参数是0,导致发生ZeroDivisionError: division by zero的错误,所以整个程序就执行终止了。其实对于上述程序而言,若是程序可以执行第7行,是可以正常得到执行结果的,可是程序第6行已经造成程序终止了,所以无法执行第7行。

15-1-2 撰写异常处理程序try - except

这一小节笔者将讲解如何捕捉异常与设计异常处理程序,发生异常被捕捉时程序会执行异常处理程序,然后跳开异常位置,再继续往下执行。这时要使用try - except指令,它的语法格式如下:
在这里插入图片描述

上述会执行try:下面的指令,如果正常则跳离except部分,如果指令有错误异常,则检查此异常是否是异常对象所指的错误,如果是代表异常被捕捉了,则执行此异常对象下面的异常处理程序。

程序实例ch15_2.py:重新设计ch15_1.py,增加异常处理程序。

# ch15_2.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except ZeroDivisionError:   # 除数为0时执行
        print("除数不可为0")

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division(6, 3))           # 列出6/3

执行结果

5.0
除数不可为0
None
2.0

上述程序执行第8行时,会将参数(10, 2)带入division( )函数,由于执行try的指令的“x / y”没有问题,所以可以执行“return x / y”,这时Python将跳过except的指令。当程序执行第9行时,会将参数(5, 0)带入division( )函数,由于执行try的指令的“x / y”产生了除数为0的ZeroDivisionError异常,这时Python会找寻是否有处理这类异常的except ZeroDivisionError存在,如果有就表示此异常被捕捉,就去执行相关的错误处理程序,此例是执行第6行,印出“除数不可为0”的错误。函数返回然后印出结果None,None是一个对象表示结果不存在,最后返回程序第10行,继续执行相关指令。

从上述可以看到,程序增加了try - except后,若是异常被except捕捉,出现的异常信息比较友善了,同时不会有程序中断的情况发生。

特别需留意的是在try - except的使用中,如果在try:后面的指令产生异常时,这个异常不是我们设计的except 异常对象,表示异常没被捕捉到,这时程序依旧会像ch15_1.py一样,直接出现错误信息,然后程序终止。

程序实例ch15_2_1.py:重新设计ch12_2.py,但是程序第9行使用字符调用除法运算,造成程序异常。

# ch15_2_1.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except ZeroDivisionError:   # 除数为0时执行
        print("除数不可为0")

print(division(10, 2))          # 列出10/2
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果

5.0
Traceback (most recent call last):
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_2_1.py", line 9, in <module>
    print(division('a', 'b'))       # 列出'a' / 'b'
          ^^^^^^^^^^^^^^^^^^
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_2_1.py", line 4, in division
    return x / y
           ~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'

由上述执行结果可以看到异常原因是TypeError,由于我们在程序中没有设计except TypeError的异常处理程序,所以程序会终止执行。更多相关处理将在15-2节说明。

15-1-3 try - except - else

Python在try - except中又增加了else指令,这个指令存放的主要目的是try内的指令正确时,可以执行else内的指令区块,我们可以将这部分指令区块称正确处理程序,这样可以增加程序的可读性。此时语法格式如下:

程序实例ch15_3.py:使用try - except - else重新设计ch15_2.py。
在这里插入图片描述

# ch15_3.py
def division(x, y):
    try:                        # try - except指令
        ans =  x / y
    except ZeroDivisionError:   # 除数为0时执行
        print("除数不可为0")
    else:
        return ans              # 传回正确的执行结果

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division(6, 3))           # 列出6/3

执行结果 :与ch15_2.py相同。

15-1-4 找不到文件的错误FileNotFoundError

程序设计时另一个常常发生的异常是打开文件时找不到文件,这时会产生FileNotFoundError异常。

程序实例ch15_4.py:打开一个不存在的文件ch15_4.txt产生异常的实例,这个程序会有一个异常处理程序,列出文件不存在。如果文件存在则打印文件内容。

# ch15_4.py

fn = 'ch15_4.txt'               # 设定欲开启的档案
try:
    with open(fn) as file_Obj:  # 用默认mode=r开启档案,传回调案对象file_Obj
        data = file_Obj.read()  # 读取档案到变量data
except FileNotFoundError:
    print("找不到 %s 档案" % fn)
else:
    print(data)                 # 输出变量data相当于输出档案

执行结果

找不到 ch15_4.txt 档案

本文件夹ch15内有ch15_5.txt,相同的程序只是第4行打开的文件不同,将可以获得打印出ch15_5.txt。

程序实例ch15_5.py:与ch15_4.py内容基本上相同,只是打开的文件不同。
在这里插入图片描述

# ch15_5.py

fn = 'ch15_5.txt'               # 设定欲开启的档案
try:
    with open(fn) as file_Obj:  # 用默认mode=r开启档案,传回调案对象file_Obj
        data = file_Obj.read()  # 读取档案到变量data
except FileNotFoundError:
    print("找不到 %s 档案" % fn)
else:
    print(data)                 # 输出变量data相当于输出档案

执行结果

深石数位科技
深度学习滴水穿石
Deep Learning

15-1-5 分析单一文件的字数

有时候在读一篇文章时,可能会想知道这篇文章的字数,这时我们可以采用下列方式分析。在正式分析前,可以先来看一个简单的程序应用。如果忘记split( )方法,可重新温习6-9-6节。

程序实例ch15_6.py:分析一个文件内有多少个单字。

# ch15_6.py

fn = 'ch15_6.txt'               # 设定欲开启的档案
try:
    with open(fn) as file_Obj:  # 用默认mode=r开启档案,传回调案对象file_Obj
        data = file_Obj.read()  # 读取档案到变量data
except FileNotFoundError:
    print("找不到 %s 档案" % fn)
else:
    wordList = data.split()     # 将文章转成列表
    print(fn, " 文章的字数是 ", len(wordList))    # 打印文章字数

执行结果

ch15_6.txt  文章的字数是  43

如果程序设计时常常有需要计算某篇文章的字数,可以考虑将上述计算文章的字数处理成一个函数,这个函数的参数是文章的文件名,然后函数直接打印出文章的字数。

程序实例ch15_7.py:设计一个计算文章字数的函数wordsNum,只要传递文章文件名,就可以获得此篇文章的字数。

# ch15_7.py
def wordsNum(fn):
    """适用英文文件, 输入文章的文件名,可以计算此文章的字数"""
    try:
        with open(fn) as file_Obj:  # 用默认"r"传回调案物件file_Obj
            data = file_Obj.read()  # 读取档案到变量data
    except FileNotFoundError:
        print("找不到 %s 档案" % fn)
    else:
        wordList = data.split()     # 将文章转成列表
        print(fn, " 文章的字数是 ", len(wordList))    # 打印文章字数

file = 'ch15_6.txt'                 # 设定欲开启的档案
wordsNum(file)

执行结果 与ch15_6.py相同。

15-1-6 分析多个文件的字数

程序设计时你可能需设计读取许多文件做分析,部分文件可能存在,部分文件可能不存在,这时就可以使用本节的观念做设计了。在接下来的程序实例分析中,笔者将欲读取的文件名放在列表内,然后使用循环将文件分次传给程序实例ch15_7.py建立的wordsNum函数,如果文件存在将打印出字数,如果文件不存在将列出找不到此文件。

程序实例ch15_8.py:分析data1.txt、data2.txt、data3.txt这3个文件的字数,同时笔者在ch15文件夹没有放置data2.txt,所以程序遇到分析此文件时,将列出找不到此文件。

# ch15_8.py
def wordsNum(fn):
    """适用英文文件, 输入文章的文件名,可以计算此文章的字数"""
    try:
        with open(fn) as file_Obj:  # 用默认"r"传回调案物件file_Obj
            data = file_Obj.read()  # 读取档案到变量data
    except FileNotFoundError:
        print("找不到 %s 档案" % fn)
    else:
        wordList = data.split()     # 将文章转成列表
        print(fn, " 文章的字数是 ", len(wordList))    # 打印文章字数

files = ['data1.txt', 'data2.txt', 'data3.txt']       # 档案列表
for file in files:
    wordsNum(file)

执行结果

data1.txt  文章的字数是  43
找不到 data2.txt 档案
data3.txt  文章的字数是  39

15-2 设计多组异常处理程序

在程序实例ch15_1.py、ch15_2.py和ch15_2_1.py的实例中,我们很清楚地了解了程序设计中有太多各种不可预期的异常发生,所以我们知道设计程序时可能需要同时设计多个异常处理程序。

15-2-1 常见的异常对象

在这里插入图片描述

在ch15_2_1.py的程序应用中可以发现,异常发生时如果except设定的异常对象不是发生的异常,相当于except没有捕捉到异常,所设计的异常处理程序变成无效的异常处理程序。Python提供了一个通用型的异常对象Exception,它可以捕捉各式的基础异常。

程序实例ch15_9.py:重新设计ch15_2_1.py,异常对象设为Exception。

# ch15_9.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except Exception:           # 通用错误使用
        print("通用错误发生")

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果

5.0
通用错误发生
None
通用错误发生
None
2.0

从上述可以看到第9行除数为0或是第10行字符相除所产生的异常皆可以使用except Exception予以捕捉,然后执行异常处理程序。甚至这个通用型的异常对象也可以应用在取代FileNotFoundError异常对象。

程序实例ch15_10.py:使用Exception取代FileNotFoundError,重新设计ch15_8.py。
在这里插入图片描述

# ch15_10.py
def wordsNum(fn):
    """适用英文文件, 输入文章的文件名,可以计算此文章的字数"""
    try:
        with open(fn) as file_Obj:  # 用默认"r"传回调案物件file_Obj
            data = file_Obj.read()  # 读取档案到变量data
    except Exception:
        print("Exception找不到 %s 档案" % fn)
    else:
        wordList = data.split()     # 将文章转成列表
        print(fn, " 文章的字数是 ", len(wordList))    # 打印文章字数

files = ['data1.txt', 'data2.txt', 'data3.txt']       # 档案列表
for file in files:
    wordsNum(file)

执行结果
在这里插入图片描述

15-2-2 设计捕捉多个异常

在try: - except的使用中,可以设计多个except捕捉多种异常,此时语法如下:
在这里插入图片描述

当然也可以视情况设计更多异常处理程序。

程序实例ch15_11.py:重新设计ch15_9.py设计捕捉2个异常对象,可参考第5行和第7行。

# ch15_11.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except ZeroDivisionError:   # 除数为0使用
        print("除数为0发生")
    except TypeError:           # 数据型别错误
        print("使用字符做除法运算异常")

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果 与ch15_9.py相同。

15-2-3 使用一个except捕捉多个异常

Python也允许设计一个except,捕捉多个异常,此时语法如下:
在这里插入图片描述

程序实例ch15_12.py:重新设计ch15_11.py,用一个except捕捉2个异常对象,下列程序读者需留意第5行的except的写法。

# ch15_12.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except (ZeroDivisionError, TypeError):   # 2个异常
        print("除数为0发生 或 使用字符做除法运算异常")

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果

5.0
除数为0发生 或 使用字符做除法运算异常
None
除数为0发生 或 使用字符做除法运算异常
None
2.0

15-2-4 处理异常但是使用Python内置的错误信息

在先前所有实例,发生异常同时被捕捉皆是使用我们自建的异常处理程序,Python也支持发生异常时使用系统内置的异常处理信息。此时语法格式如下:
在这里插入图片描述

上述e是系统内置的异常处理信息,e可以是任意字符,笔者此处使用e是因为代表error的内涵。当然上述except语法也接受同时处理多个异常对象,可参考下列程序实例第5行。

程序实例ch15_13.py:重新设计ch15_12.py,使用Python内置的错误信息。

# ch15_13.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except (ZeroDivisionError, TypeError) as e:   # 2个异常
        print(e)

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果

5.0
division by zero
None
unsupported operand type(s) for /: 'str' and 'str'
None
2.0

上述执行结果的错误信息皆是Python内部的错误信息。

15-2-5 捕捉所有异常

程序设计许多异常是我们不可预期的,很难一次设想周到,Python提供语法让我们可以一次捕捉所有异常,此时try - except语法如下:

程序实例ch15_14.py:一次捕捉所有异常的设计。

在这里插入图片描述

# ch15_14.py
def division(x, y):
    try:                        # try - except指令
        return x / y
    except:                     # 捕捉所有异常
        print("异常发生")

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果

5.0
异常发生
None
异常发生
None
2.0

15-3 丢出异常

前面所介绍的异常皆是Python直译器发现异常时,自行丢出异常对象,如果我们不处理程序就终止执行,如果我们使用try - except处理程序可以在异常中继续执行。这一节要探讨的是,我们设计程序时如果发生某些状况,我们自己将它定义为异常然后丢出异常信息,程序停止正常往下执行,同时让程序跳到自己设计的except去执行。它的语法如下:
在这里插入图片描述

程序实例ch15_15.py:目前有些金融机构在客户建立网络账号时,会要求密码长度必须在5到8个字符间,接下来我们设计一个程序,这个程序内有passWord( )函数,这个函数会检查密码长度,如果长度小于5或是长度大于8皆抛出异常。在第11行会有一系列密码供测试,然后以循环方式执行检查。

# ch15_15.py
def passWord(pwd):
    """检查密码长度必须是5到8个字符"""
    pwdlen = len(pwd)                       # 密码长度
    if pwdlen < 5:                          # 密码长度不足            
        raise Exception('密码长度不足')
    if pwdlen > 8:                          # 密码长度太长
        raise Exception('密码长度太长')
    print('密码长度正确')

for pwd in ('aaabbbccc', 'aaa', 'aaabbb'):  # 测试系列密码值
    try:
        passWord(pwd)
    except Exception as err:
        print("密码长度检查异常发生: ", str(err))

执行结果

密码长度检查异常发生:  密码长度太长
密码长度检查异常发生:  密码长度不足
密码长度正确

上述当密码长度不足或密码长度太长,皆会抛出异常,这时passWord( )函数返回的是Exception对象(第6和8行),这时原先Exception( )内的字符串(‘密码长度不足’或‘密码长度太长’)会通过第14行传给err变量,然后执行第15行内容。

15-4 记录Traceback字符串

相信读者学习至今,已经经历了许多程序设计的错误,每次错误屏幕皆出现Traceback字符串,在这个字符串中指出程序错误的原因。例如,请参考程序实例ch15_2_1.py的执行结果,该程序使用Traceback列出了错误。

如果我们导入traceback模块,就可以使用traceback.format_exc( )记录这个Traceback字符串。

程序实例ch15_16.py:重新设计程序实例ch15_15.py,增加记录Traceback字符串,这个记录将被记录在errch15_16.txt内。

# ch15_16.py
import traceback                            # 导入taceback

def passWord(pwd):
    """检查密码长度必须是5到8个字符"""
    pwdlen = len(pwd)                       # 密码长度
    if pwdlen < 5:                          # 密码长度不足            
        raise Exception('The length of pwd is too short')
    if pwdlen > 8:                          # 密码长度太长
        raise Exception('The length of pwd is too long')
    print('密码长度正确')

for pwd in ('aaabbbccc', 'aaa', 'aaabbb'):  # 测试系列密码值
    try:
        passWord(pwd)
    except Exception as err:
        errlog = open('errch15_16.txt', 'a')   # 开启错误档案
        errlog.write(traceback.format_exc())   # 写入错误档案
        errlog.close()                         # 关闭错误档案
        print("将Traceback写入错误档案errch15_16.txt完成")
        print("密码长度检查异常发生: ", str(err))

执行结果

将Traceback写入错误档案errch15_16.txt完成
密码长度检查异常发生:  The length of pwd is too long
将Traceback写入错误档案errch15_16.txt完成
密码长度检查异常发生:  The length of pwd is too short
密码长度正确

如果使用记事本打开errch15_16.txt,可以得到下列结果。
在这里插入图片描述

上述程序第17行笔者使用‘a’附加文件方式打开文件,主要是程序执行期间可能有多个错误,为了记录所有错误所以使用这种方式打开文件。上述程序最关键的地方是第17至19行,在这里我们打开了记录错误的errch15_17.txt文件,然后将错误写入此文件,最后关闭此文件。这个程序记录的错误是我们抛出的异常错误,其实在15-1和15-2节中我们设计了异常处理程序,避免错误造成程序中断,实际上Python还是有记录错误,可参考下一个实例。

程序实例ch15_17.py:重新设计ch15_14.py,主要是将程序异常的信息保存在errch15_17.txt文件内,本程序的重点是第8至10行。

# ch15_17.py
import traceback

def division(x, y):
    try:                        # try - except指令
        return x / y
    except:                     # 捕捉所有异常
        errlog = open('errch15_17.txt', 'a')   # 开启错误档案
        errlog.write(traceback.format_exc())   # 写入错误档案
        errlog.close()                         # 关闭错误档案
        print("将Traceback写入错误档案errch15_17.txt完成")
        print("异常发生")

print(division(10, 2))          # 列出10/2
print(division(5, 0))           # 列出5/0
print(division('a', 'b'))       # 列出'a' / 'b'
print(division(6, 3))           # 列出6/3

执行结果

5.0
将Traceback写入错误档案errch15_17.txt完成
异常发生
None
将Traceback写入错误档案errch15_17.txt完成
异常发生
None
2.0

如果使用记事本打开errch15_17.txt,可以得到下列结果。

在这里插入图片描述

15-5 finally

Python的关键词finally功能是和try配合使用,在try之后可以有except或else,这个finally关键词必须放在except和else之后,同时不论是否有异常发生一定会执行这个finally内的程序代码。这个功能主要是用在Python程序与数据库连接时,输出连接相关信息。

程序实例ch15_18.py:重新设计ch15_14.py,增加finally关键词。

# ch15_18.py
def division(x, y):
    try:                             # try - except指令
        return x / y
    except:                          # 捕捉所有异常
        print("异常发生")
    finally:                         # 离开函数前先执行此程序代码
        print("阶段任务完成")

print(division(10, 2),"\n")          # 列出10/2
print(division(5, 0),"\n")           # 列出5/0
print(division('a', 'b'),"\n")       # 列出'a' / 'b'
print(division(6, 3),"\n")           # 列出6/3

执行结果

阶段任务完成
5.0

异常发生
阶段任务完成
None

异常发生
阶段任务完成
None

阶段任务完成
2.0

上述程序执行时,如果没有发生异常,程序会先输出字符串“阶段任务完成”,然后返回主程序,输出division( )的返回值。如果程序有异常会先输出字符串“异常发生”,再执行finally的程序代码输出字符串“阶段任务完成”然后返回主程序输出“None”。

15-6 程序断言assert

15-6-1 设计断言

Python的assert关键词主要功能是协助程序设计师在程序设计阶段,对整个程序的执行状态做一个全面性的安全检查,以确保程序不会发生语意上的错误。例如,我们在第12章设计银行的存款程序时,我们没有考虑到存款或提款是负值的问题,我们也没有考虑到如果提款金额大于存款金额的情况。

程序实例ch15_19.py:重新设计ch12_4.py,这个程序主要是将第22行的存款金额改为-300和第24行提款金额大于存款金额,接着观察执行结果。

# ch15_19.py
class Banks():
    # 定义银行类别
    title = 'Taipei Bank'                   # 定义属性
    def __init__(self, uname, money):       # 初始化方法
        self.name = uname                   # 设定存款者名字
        self.balance = money                # 设定所存的钱

    def save_money(self, money):            # 设计存款方法
        self.balance += money               # 执行存款
        print("存款 ", money, " 完成")      # 打印存款完成

    def withdraw_money(self, money):        # 设计提款方法
        self.balance -= money               # 执行提款
        print("提款 ", money, " 完成")      # 打印提款完成

    def get_balance(self):                  # 获得存款余额
        print(self.name.title(), " 目前余额: ", self.balance)

hungbank = Banks('hung', 100)               # 定义对象hungbank
hungbank.get_balance()                      # 获得存款余额                
hungbank.save_money(-300)                   # 存款-300元
hungbank.get_balance()                      # 获得存款余额
hungbank.withdraw_money(700)                # 提款700元
hungbank.get_balance()                      # 获得存款余额

执行结果

Hung  目前余额:  100
存款  -300  完成
Hung  目前余额:  -200
提款  700  完成
Hung  目前余额:  -900

上述程序语法上是没有错误,但是犯了2个程序语意上的设计错误,分别是存款金额出现了负值和提款金额大于存款金额的问题。所以我们发现存款余额出现了负值-200和-900的情况。接下来笔者将讲解如何解决上述问题。

断言(assert)主要功能是确保程序执行的某个阶段,必须符合一定的条件,如果不符合这个条件时程序主动抛出异常,让程序终止同时主动打印出异常原因,方便程序设计师侦错。它的语法格式如下:

 assert 条件, ‘字符串'

上述意义是程序执行至此阶段时测试条件,如果条件响应是True,程序不理会逗号“,”右边的字符串正常往下执行。如果条件响应是False,程序终止同时将逗号“,”右边的字符串输出到Traceback的字符串内。对上述程序ch15_19.py而言,很明显我们重新设计ch15_20.py时必须让assert关键词做下列2件事:

①确保存款与提款金额是正值,否则输出错误,可参考第10和15行。

②确保提款金额小于等于存款金额,否则输出错误,可参考第16行。

程序实例ch15_20.py:重新设计ch15_19.py,在这个程序我们先测试存款金额小于0的状况,第27行。

# ch15_20.py
class Banks():
    # 定义银行类别
    title = 'Taipei Bank'                   # 定义属性
    def __init__(self, uname, money):       # 初始化方法
        self.name = uname                   # 设定存款者名字
        self.balance = money                # 设定所存的钱

    def save_money(self, money):            # 设计存款方法
        assert money > 0, '存款money必需大于0'
        self.balance += money               # 执行存款
        print("存款 ", money, " 完成")      # 打印存款完成

    def withdraw_money(self, money):        # 设计提款方法
        assert money > 0, '提款money必需大于0'
        assert money <= self.balance, '存款金额不足'
        self.balance -= money               # 执行提款
        print("提款 ", money, " 完成")      # 打印提款完成

    def get_balance(self):                  # 获得存款余额
        print(self.name.title(), " 目前余额: ", self.balance)

hungbank = Banks('hung', 100)               # 定义对象hungbank
hungbank.get_balance()                      # 获得存款余额                
hungbank.save_money(300)                    # 存款300元
hungbank.get_balance()                      # 获得存款余额
hungbank.save_money(-300)                   # 存款-300元
hungbank.get_balance()                      # 获得存款余额

执行结果

Hung  目前余额:  100
存款  300  完成
Hung  目前余额:  400
Traceback (most recent call last):
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_20.py", line 27, in <module>
    hungbank.save_money(-300)                   # 存款-300元
    ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_20.py", line 10, in save_money       
    assert money > 0, '存款money必需大于0'
           ^^^^^^^^^
AssertionError: 存款money必需大于0

上述执行结果很清楚,当程序第27行将存款金额设为负值-300时,调用save_money( )方法,结果在第10行的assert断言地方出现False,所以设定的错误信息‘存款必需大余0’的字符串被打印出来,这种设计方便我们在真实的环境做最后程序语意检查。

程序实例ch15_21.py:重新设计ch15_20.py,这个程序我们测试了当提款金额大于存款金额的状况,可参考第27行,下列只列出主程序内容。
在这里插入图片描述

# ch15_21.py
class Banks():
    # 定义银行类别
    title = 'Taipei Bank'                   # 定义属性
    def __init__(self, uname, money):       # 初始化方法
        self.name = uname                   # 设定存款者名字
        self.balance = money                # 设定所存的钱

    def save_money(self, money):            # 设计存款方法
        assert money > 0, '存款money必需大于0'
        self.balance += money               # 执行存款
        print("存款 ", money, " 完成")      # 打印存款完成

    def withdraw_money(self, money):        # 设计提款方法
        assert money > 0, '提款money必需大于0'
        assert money <= self.balance, '存款金额不足'
        self.balance -= money               # 执行提款
        print("提款 ", money, " 完成")      # 打印提款完成

    def get_balance(self):                  # 获得存款余额
        print(self.name.title(), " 目前余额: ", self.balance)

hungbank = Banks('hung', 100)               # 定义对象hungbank
hungbank.get_balance()                      # 获得存款余额                
hungbank.save_money(300)                    # 存款300元
hungbank.get_balance()                      # 获得存款余额
hungbank.withdraw_money(700)                # 提款700元
hungbank.get_balance()                      # 获得存款余额

执行结果

Hung  目前余额:  100
存款  300  完成
Hung  目前余额:  400
Traceback (most recent call last):
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_21.py", line 27, in <module>
    hungbank.withdraw_money(700)                # 提款700元
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "e:\桌面Desktop\Python王者归来\代码\ch15\ch15_21.py", line 16, in withdraw_money   
    assert money <= self.balance, '存款金额不足'
           ^^^^^^^^^^^^^^^^^^^^^
AssertionError: 存款金额不足

上述当提款金额大于存款金额时,这个程序将造成第16行的assert断言条件是False,所以触发了打印‘存款金额不足’的信息。由上述的执行结果,我们就可以依据需要修正程序的内容。

15-6-2 停用断言

断言assert一般是用在程序开发阶段,如果整个程序设计好了以后,想要停用断言assert,可以在Windows的命令提示环境(可参考附录B-2-1),执行程序时使用“-O”选项停用断言。笔者在Windows 8操作系统安装Python 3.62版本,在这个版本的Python安装路径内~\Python\Python36-32内有python.exe可以执行所设计的Python程序,若以ch15_21.py为实例,如果我们要停用断言可以使用下列指令。

 ~\python.exe -O D:\Python\ch15\ch15_21.py

上述“~”代表安装Python的路径,若是以ch15_21.py为例,采用停用断言选项“-O”后,执行结果将不再有Traceback错误信息产生,因为断言被停用了。
在这里插入图片描述

15-7 程序日志模块logging

程序设计阶段难免会有错误产生,没有得到预期的结果,在产生错误期间到底发生什么事情?程序代码执行顺序是否有误或变量值如何变化?这些都是程序设计师想知道的事情。笔者过去碰上这方面的问题,常常是在程序代码几个重要节点增加print( )函数输出关键变量,以了解程序的变化,程序修订完成后再将这几个print( )删除,坦白说是有一点麻烦。

Python有程序日志logging功能,这个功能可以协助我们执行程序的除错,有了这个功能我们可以自行设定关键变量在每一个程序阶段的变化,由这个关键变量的变化可方便我们执行程序的除错,同时未来不想要显示这些关键变量数据时,可以不用删除,只要适度加上指令就可隐藏它们,这将是本节的主题。

15-7-1 logging模块

Python内有提供logging模块,这个模块有提供方法可以让我们使用程序日志logging功能,在使用前须先使用import导入此模块。

import logging

15-7-2 logging的等级

logging模块共分5个等级,从最低到最高等级顺序如下:

  • DEBUG等级使用logging.debug( )显示程序日志内容,所显示的内容是程序的小细节,最低层级的内容,感觉程序有问题时可使用它追踪关键变量的变化过程。
  • INFO等级使用logging.info( )显示程序日志内容,所显示的内容是记录程序一般发生的事件。
  • WARNING等级使用logging.warning(
    )显示程序日志内容,所显示的内容虽然不会影响程序的执行,但是未来可能导致问题的发生。
  • ERROR等级使用logging.error( )显示程序日志内容,通常显示程序在某些状态将引发错误的缘由。
  • CRITICAL等级使用logging.critical( )显示程序日志内容,这是最重要的等级,通常是显示将让整个系统当掉或中断的错误。

程序设计时,可以使用下列函数设定显示信息的等级:

logging.basicConfig(level=logging.DEBUG)  # 假设是设定DEBUG等级

当设定logging为某一等级时,未来只有此等级或更高等级的logging会被显示。

程序实例ch15_22.py:显示所有等级的logging信息。

# ch15_22.py
import logging

logging.basicConfig(level=logging.DEBUG)    # 等级是DEBUG
logging.debug('logging message, DEBUG')
logging.info('logging message, INFO')
logging.warning('logging message, WARNING')
logging.error('logging message, ERROR')
logging.critical('logging message, CRITICAL')

执行结果

DEBUG:root:logging message, DEBUG
INFO:root:logging message, INFO
WARNING:root:logging message, WARNING
ERROR:root:logging message, ERROR
CRITICAL:root:logging message, CRITICAL

上述每一个输出前方有DEBUG:root:(其他依次类推)前导信息,这是该logging输出模式默认的输出信息注明输出logging模式。

程序实例ch15_23.py:显示WARNING等级或更高等级的输出。

# ch15_23.py
import logging

logging.basicConfig(level=logging.WARNING)    # 等级是WARNING
logging.debug('logging message, DEBUG')
logging.info('logging message, INFO')
logging.warning('logging message, WARNING')
logging.error('logging message, ERROR')
logging.critical('logging message, CRITICAL')

执行结果

WARNING:root:logging message, WARNING
ERROR:root:logging message, ERROR
CRITICAL:root:logging message, CRITICAL

当我们设定logging的输出等级是WARNING时,较低等级的logging输出就被隐藏了。当了解了上述logging输出等级的特性后,笔者通常在设计大型程序时,程序设计初期阶段会将logging等级设为DEBUG,如果确定程序大致没问题,就将logging等级设为WARNING,最后再设为CRITICAL。这样就可以不用再像过去一样,在程序设计初期使用print( )记录关键变量的变化,当程序确定完成后,还需要一个一个检查print( )然后将它删除。

15-7-3 格式化logging信息输出format

从ch15_22.py和ch15_23.py可以看到输出信息前方有前导输出信息,我们可以使用在logging.basicConfig( )方法内增加format格式化输出信息为空字符串‘’的方式,取消显示前导输出信息。

 logging.basicConfig(level=logging.DEBUG, format = ‘ ‘)

程序实例ch15_24.py:重新设计ch15_22.py,取消显示logging的前导输出信息。

# ch15_24.py
import logging

logging.basicConfig(level=logging.DEBUG, format='')
logging.debug('logging message, DEBUG')
logging.info('logging message, INFO')
logging.warning('logging message, WARNING')
logging.error('logging message, ERROR')
logging.critical('logging message, CRITICAL')

执行结果

logging message, DEBUG
logging message, INFO
logging message, WARNING
logging message, ERROR
logging message, CRITICAL

从上述执行结果很明显看到,模式前导的输出信息没有了。

15-7-4 时间信息asctime
我们可以在format内配合asctime列出系统时间,这样可以列出每一重要阶段关键变量发生的时间。

程序实例ch15_25.py:列出每一个logging输出时的时间。

# ch15_25.py
import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s')
logging.debug('logging message, DEBUG')
logging.info('logging message, INFO')
logging.warning('logging message, WARNING')
logging.error('logging message, ERROR')
logging.critical('logging message, CRITICAL')

执行结果

2024-07-29 13:18:07,000
2024-07-29 13:18:07,000
2024-07-29 13:18:07,002
2024-07-29 13:18:07,002
2024-07-29 13:18:07,003

我们的确获得了每一个logging的输出时间,但是经过format处理后原先logging.xxx( )内的输出信息却没有了,这是因为我们在format内只有留时间字符串信息。

15-7-5 format内的message

如果想要输出原先logging.xxx( )的输出信息,必须在format内增加message格式。

程序实例ch15_26.py:增加logging.xxx( )的输出信息。

# ch15_26.py
import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s : %(message)s')
logging.debug('logging message, DEBUG')
logging.info('logging message, INFO')
logging.warning('logging message, WARNING')
logging.error('logging message, ERROR')
logging.critical('logging message, CRITICAL')

执行结果

2024-07-29 13:19:02,416 : logging message, DEBUG
2024-07-29 13:19:02,416 : logging message, INFO
2024-07-29 13:19:02,416 : logging message, WARNING
2024-07-29 13:19:02,417 : logging message, ERROR
2024-07-29 13:19:02,417 : logging message, CRITICAL

15-7-6 列出levelname

levelname属性是记载目前logging的显示层级是哪一个等级。

程序实例ch15_27.py:列出目前level所设定的等级。

# ch15_27.py
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s : %(message)s')
logging.debug('logging message.')
logging.info('logging message.')
logging.warning('logging message')
logging.error('logging message')
logging.critical('logging message')

执行结果

2024-07-29 13:20:53,166 - DEBUG : logging message.
2024-07-29 13:20:53,166 - INFO : logging message.
2024-07-29 13:20:53,166 - WARNING : logging message
2024-07-29 13:20:53,167 - ERROR : logging message
2024-07-29 13:20:53,167 - CRITICAL : logging message

15-7-7 使用logging列出变量变化的应用

这一节开始笔者将正式使用logging追踪变量的变化,下列是简单追踪索引值变化的程序。

程序实例ch15_28.py:追踪索引值变化的实例。

# ch15_28.py
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s : %(message)s')
logging.debug('程序开始')
for i in range(5):
    logging.debug('目前索引 %s ' % i)
logging.debug('程序结束')

执行结果

2024-07-29 13:21:33,537 - DEBUG : 程序开始
2024-07-29 13:21:33,538 - DEBUG : 目前索引 0
2024-07-29 13:21:33,538 - DEBUG : 目前索引 1
2024-07-29 13:21:33,539 - DEBUG : 目前索引 2
2024-07-29 13:21:33,539 - DEBUG : 目前索引 3
2024-07-29 13:21:33,539 - DEBUG : 目前索引 4
2024-07-29 13:21:33,541 - DEBUG : 程序结束

上述程序记录了整个索引值的变化过程,读者需留意第8行的输出,它的输出结果是在%(message)s定义。

15-7-8 正式追踪factorial数值的应用

在程序ch11_26.py笔者曾经使用递归函数计算阶乘factorial,接下来笔者想用一般循环方式追踪阶乘计算的过程。

程序实例ch15_29.py:使用logging追踪factorial阶乘计算的过程。

# ch15_29.py
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s : %(message)s')
logging.debug('程序开始')

def factorial(n):
    logging.debug('factorial %s 计算开始' % n)
    ans = 1
    for i in range(n + 1):
        ans *= i
        logging.debug('i = ' + str(i) + ', ans = ' + str(ans))
    logging.debug('factorial %s 计算结束' % n)
    return ans

num = 5
print("factorial(%d) = %d" % (num, factorial(num)))
logging.debug('程序结束')

执行结果

2024-07-29 13:22:26,792 - DEBUG : 程序开始
2024-07-29 13:22:26,793 - DEBUG : factorial 5 计算开始
2024-07-29 13:22:26,793 - DEBUG : i = 0, ans = 0
2024-07-29 13:22:26,793 - DEBUG : i = 1, ans = 0
2024-07-29 13:22:26,795 - DEBUG : i = 2, ans = 0
2024-07-29 13:22:26,795 - DEBUG : i = 3, ans = 0
2024-07-29 13:22:26,796 - DEBUG : i = 4, ans = 0
2024-07-29 13:22:26,796 - DEBUG : i = 5, ans = 0
2024-07-29 13:22:26,797 - DEBUG : factorial 5 计算结束
factorial(5) = 0
2024-07-29 13:22:26,798 - DEBUG : 程序结束

在上述使用logging的DEBUG过程可以发现阶乘数从0开始,造成所有阶段的执行结果皆是0,程序的错误,下列程序第11行,笔者更改此项设定为从1开始。

程序实例ch15_30.py:修订ch15_29.py的错误,让阶乘从1开始。

# ch15_30.py
import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s : %(message)s')
logging.debug('程序开始')

def factorial(n):
    logging.debug('factorial %s 计算开始' % n)
    ans = 1
    for i in range(1, n + 1):
        ans *= i
        logging.debug('i = ' + str(i) + ', ans = ' + str(ans))
    logging.debug('factorial %s 计算结束' % n)
    return ans

num = 5
print("factorial(%d) = %d" % (num, factorial(num)))
logging.debug('程序结束')

执行结果

2024-07-29 13:23:15,172 - DEBUG : 程序开始
2024-07-29 13:23:15,172 - DEBUG : factorial 5 计算开始
2024-07-29 13:23:15,173 - DEBUG : i = 1, ans = 1
2024-07-29 13:23:15,173 - DEBUG : i = 2, ans = 2
2024-07-29 13:23:15,174 - DEBUG : i = 3, ans = 6
2024-07-29 13:23:15,175 - DEBUG : i = 4, ans = 24
2024-07-29 13:23:15,175 - DEBUG : i = 5, ans = 120
2024-07-29 13:23:15,176 - DEBUG : factorial 5 计算结束
factorial(5) = 120
2024-07-29 13:23:15,176 - DEBUG : 程序结束

15-7-9 将程序日志logging输出到文件

程序很长时,若将logging输出在屏幕,其实不太方便逐一核对关键变量值的变化,此时我们可以考虑将logging输出到文件,方法是在logging.basicConfig( )增加filename=“文件名”,这样就可以将logging输出到指定的文件内。

程序实例ch15_31.py:将程序实例的logging输出到out15_31.txt。

在这里插入图片描述

# ch15_31.py
import logging

logging.basicConfig(filename='out15_31.txt', level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s : %(message)s')
logging.debug('Program start')

def factorial(n):
    logging.debug('factorial %s counting begin' % n)
    ans = 1
    for i in range(1, n + 1):
        ans *= i
        logging.debug('i = ' + str(i) + ', ans = ' + str(ans))
    logging.debug('factorial %s end of counting' % n)
    return ans

num = 5
print("factorial(%d) = %d" % (num, factorial(num)))
logging.debug('End of Program')

执行结果

factorial(5) = 120

这时在当前工作文件夹可以看到out15_31.txt,打开后可以得到下列结果。
在这里插入图片描述

15-7-10 隐藏程序日志logging的DEBUG等级使用CRITICAL

先前笔者有说明logging有许多等级,只要设定高等级,Python就会忽略低等级的输出,所以如果我们程序设计完成,也确定没有错误,其实可以将logging等级设为最高等级,所有较低等级的输出将被隐藏。

程序实例ch15_32.py:重新设计ch15_30.py,将程序内DEBUG等级的logging隐藏。
在这里插入图片描述

# ch15_32.py
import logging

logging.basicConfig(level=logging.CRITICAL,
                    format='%(asctime)s - %(levelname)s : %(message)s')
logging.debug('程式開始')

def factorial(n):
    logging.debug('factorial %s 計算開始' % n)
    ans = 1
    for i in range(1, n + 1):
        ans *= i
        logging.debug('i = ' + str(i) + ', ans = ' + str(ans))
    logging.debug('factorial %s 計算結束' % n)
    return ans

num = 5
print("factorial(%d) = %d" % (num, factorial(num)))
logging.debug('程式結束')

执行结果

factorial(5) = 120

15-7-11 停用程序日志logging

可以使用下列方法停用日志logging。

logging.disable(level)  # level是停用logging的等级

上述可以停用该程序代码后指定等级以下的所有等级,如果想停用全部参数可以使用logging.CRITICAL等级,这个方法一般是放在import下方,这样就可以停用所有的logging。

程序实例ch15_33.py:重新设计ch15_30.py,这个程序只是在原先第3行空白行加上下列程序代码。
在这里插入图片描述

执行结果 与ch15_32.py相同。

15-8 程序除错的典故

通常我们又将程序除错称Debug,De是除去的意思,bug是指小虫,其实这是有典故的。1944年IBM和哈佛大学联合开发了Mark I计算机,此计算机重5吨,有8英尺高,51英尺长,内部线路加总长是500英里,没有中断使用了15年,下列是此计算机图片。

在当时有一位女性程序设计师Grace Hopper,发现了第一个计算机虫(bug),一只死的蛾(moth)的双翅卡在继电器(relay),促使数据读取失败,下列是当时Grace Hopper记录此事件的数据。

当时Grace Hopper写下了下列两句话。

Relay #70 Panel F (moth) in relay.

First actual case of bug being found.

大意是编号70的继电器出问题(因为蛾),这是真实计算机上所发现的第一只虫。自此,计算机界认定用debug描述“找出及删除程序错误”应归功于Grace Hopper。

在这里插入图片描述

本图片转载自http://www.computersciencelab.com

在这里插入图片描述

  • 20
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值