一、前言
本小节介绍了错误的调试和处理,包含了寻找出现bug的代码的方法,以及处理bug的方法,另外还附加了一些错误类型。
环境说明:Python 3.6、windows11 64位
二、调试:找出错误📌
之前看到一句话,很在理:出错并不可怕,可怕的是不知道哪里出错了。为了找到这个错误,有时候需要花很长时间去调试,费时费力,不过这无可避免。当然,在经验越发丰富之后,debug的能力也会更加的强,处理起来会更加得心应手。熟能生巧是亘古不变的真理!多写多练。
2.1 看懂报错
其实在熟悉Python的报错之后,根据报错提示,能够解决绝大部分的bug。Python的报错会一级一级地打印出来告诉你错误的行。
如下截图,一个简单的代码,运行之后报错了。这个报错类型,说的就是除数为0的错误。然后看上面的代码索引,从上往下看,首先是第4行错了,这一行调用了func()
函数。下一个提示是第2行,就是func()
函数里返回1/num
,所以就定位到了问题。1/0
引发了ZeroDivisionError
错误。
2.2 print()+注释
这类报错一般是出现那种不明显的错误才会用到,比如说少了个标点符号、爬虫时一开始没有问题,后面被反爬了等。
具体操作方式就是注释掉报错之后的代码,然后往上加几个print()
打印一些变量。
比如以下代码,我给aa赋值的时候,少敲了一个]
,但报错并不会指向第一行,而是指向第二行,初学者可能会盯着第二行纳闷,但实际错误是在上一行。这是因为在第一行找不到]
时,会去第二行找,但是第二行却是一个新的赋值语句,所以就报错。
aa = [1,2,3
bb = 0
print(aa,bb)
按照print()+注释
的方法,就是将第2~3行注释,然后print(aa)
看结果,这时会发现报错变成了print(aa)
,但是print(aa)
没问题,这是可断定是第一行出现了问题,所以检查一下第一行便可发现是少了一个]
。
2.3 assert 断言
语法:assert [条件],'条件不成立的提示语句'
断言可以捕获错误,但是它也会报错,只是以另外一种提示返回,返回的提示可以进行自定义,错误类型都是AssertionError
。
如下例子,在执行1/num
前加上一个断言语句。
def func(num):
assert num != 0, 'num is zero!'
return 1 / num
func(0)
2.4 raise 自定义异常
语法:raise [异常类型]('[返回信息]')
raise
和assert
有点相似,它也会报错,只是以另外一种提示返回,返回的错误类型和提示可以自行定义。
如下代码,当num==0
时,返回一个报错ValueError: Invalid value: 0.
def func(num):
if num == 0:
raise ValueError("Invalid value: %s."% num)
# raise ValueError("Invalid value!", num)
# raise Exception("Invalid value!", num)
return 1 / num
func(0)
2.5 IDE的debug功能
设置断点,然后右键执行debug。在断点之前的代码,会立马执行,断点之后的代码,会暂停执行,手动点击运行下一步便可观察每一步的执行情况。
下面以Pycharm为例,我把断点设置在函数的定义一行,执行debug后,先执行了func(0)
函数调用语句,到了def func(num):
便停下来给我调试代码。Pycharm有两种执行下一步代码的选项,一种是执行所有的代码,包括Python内置的库或第三方库代码;另一种是只执行我的代码。可根据需求选择。
2.6 logging
logging
模块的日志有六个级别,按从低到高分别是NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。
级别 | 数值 | 说明 |
---|---|---|
NOTSET | 0 | 不设置 |
DEBUG | 10 | 细节信息,仅当诊断问题时适用。 |
INFO | 20 | 确认程序按预期运行 |
WARNING | 30 | 表明有已经或即将发生的意外(例如:磁盘空间不足)。程序仍按预期进行 |
ERROR | 40 | 由于严重的问题,程序的某些功能已经不能正常执行 |
CRITICAL | 50 | 严重的错误,表明程序已不能继续执行 |
注意:
logging
只会追踪大于设定级别及以上的报错,级别以下的会被忽略。如级别WARNING
只会追踪WARNING
、ERROR
和CRITICAL
,不会追踪INFO
、DEBUG
和NOTSET
;- 根记录器的默认级别为
WARNING
,可通过logging.basicConfig(level=logging.DEBUG)
的level
参数更改错误级别; - 所追踪事件可以以不同形式处理,最简单的方式是输出到控制台,另一种常用的方式是写入磁盘文件。给
logging.basicConfig(filename=r"D:\test.log")
的filename
指定文件路径写入磁盘。
如下例子,指定了filename
之后,运行结果不会在控制台打印任何东西,相关的报错记录写入了D:\test.log
,内容如下图,自定义测试部分都会先写入到文件中,后续跑代码的应用部分,报错之后,会紧接着在后面写入。
import logging
#使用basicConfig()来指定日志级别和相关信息
logging.basicConfig(level=logging.DEBUG # 设置日志输出级别
,filename=r"D:\test.log" # log日志输出的文件位置和文件名
,filemode="w" # 文件的写入格式,w为重新写入文件,默认是追加
# 日志输出的格式
# %(levelname)-9s:表示字符串格式化 %s,(levelname)为指定的键,值的传递如{levelname:[值]},-表示左对齐,8表示8个符号,不够空格填充
,format="%(asctime)s - %(name)s - %(levelname)-9s - %(filename)-8s : %(lineno)s line - %(message)s"
,datefmt="%Y-%m-%d %H:%M:%S" # 时间输出的格式
)
# 自定义错误信息测试,可去掉
logging.debug("Custom message: DEBUG.")
logging.info("Custom message: INFO.")
logging.warning("Custom message: WARNING.")
logging.error("Custom message: ERROR.")
logging.critical("Custom message: CRITICAL.")
# 实际应用
def func(num):
return 1 / num
try:
func(0)
except Exception as e:
logging.exception(e)
logging.error("The value is 0.")
注:关于字符串格式化,可参考:《Python 基础合集2:字符串格式化》
除了上述logging.basicConfig()
的应用,logging
还可以通过模块化的组件来处理日志,示例如下:
import logging
# 记录器,创建2个记录器
log1 = logging.getLogger("log1")
log1.setLevel(logging.DEBUG)
print(log1)
log2 = logging.getLogger("log2")
log2.setLevel(logging.INFO)
print(log2)
# 处理器,创建4个处理器,2个标准输出,2个文件输出
## 1.标准输出
# 没有设置输出级别,或者设置的输出级别小于记录器输出级别,将用记录器的输出级别
# 设置了高于记录器的输出级别则使用处理器设置的级别
sh1 = logging.StreamHandler()
sh1.setLevel(logging.WARNING) # 级别大于log1设置的级别,使用该级别
sh2 = logging.StreamHandler() # 使用log2级别
## 2.文件输出
# 没有设置输出级别,或者设置的输出级别小于记录器输出级别,将用记录器的输出级别
# 设置了高于记录器的输出级别则使用文件输出设置的级别
fh1 = logging.FileHandler(filename=r"D:\test.log",mode='w') # 使用log1级别
fh2 = logging.FileHandler(filename=r"D:\test.log",mode='a')
fh2.setLevel(logging.WARNING) # 级别大于log2设置的级别,使用该级别
# 格式器,创建2种格式,只是时间格式不同
fmt1 = logging.Formatter(fmt="%(asctime)s - %(name)s - %(levelname)-9s - %(filename)-8s : %(lineno)s line - %(message)s")
fmt2 = logging.Formatter(fmt="%(asctime)s - %(name)s - %(levelname)-9s - %(filename)-8s : %(lineno)s line - %(message)s"
,datefmt="%Y/%m/%d %H:%M:%S")
# 处理器配置格式
sh1.setFormatter(fmt1) # sh1--fmt1,标准1使用格式1
fh1.setFormatter(fmt2) # fh1--fmt2,文件1使用格式2
sh2.setFormatter(fmt2) # sh2--fmt1,标准2使用格式1
fh2.setFormatter(fmt1) # fh2--fmt2,文件2使用格式2
# 记录器设置处理器
log1.addHandler(sh1) # log1--sh1,log1使用标准1
log1.addHandler(fh1) # log1--fh1,log1使用文件1
log2.addHandler(sh2) # log2--sh2,log2使用标准2
log2.addHandler(fh2) # log2--fh2,log2使用文件2
# 打印日志代码
log1.debug("log1's DEBUG.")
log1.info("log1's INFO.")
log1.warning("log1's WARNING.")
log1.error("log1's ERROR.")
log1.critical("log1's CRITICAL.")
log2.debug("log2's DEBUG.")
log2.info("log2's INFO.")
log2.warning("log2's WARNING.")
log2.error("log2's ERROR.")
log2.critical("log2's CRITICAL.")
三、处理:避开错误✅
找到错误之后,就需要我们进行debug,一方面是改正错误,另外一方面是避开错误。
- 改正错误:这个好理解,就是改为正确的逻辑;
- 避开错误:这个是指允许错误,但是我通过一个判断逻辑对进行识别分类处理。如果正常执行,则顺流程继续跑,如果报错,分一支流处理错误,处理完再并入正常流程继续执行。
3.1 try…except…else…finally…
语法:
try: # 正常情况下执行的代码
print('try…')
except [异常类型]: # 出现异常情况,执行的代码
print('error!')
else: # 没异常发生则执行
print('else:not error.')
finally: # 都会执行
print('finally.')
注:异常类型的表示方式
- 不指定或者使用
Exception
:表示任意错误,如except Exception:
- 指定具体错误:只捕获指定的错误,如
except KeyError:
- 元组形式传入多个错误:只捕获指定的多种错误,如
except (ZeroDivisionError,KeyError):
- 使用别名:如
except Exception as e:
、except (ZeroDivisionError,KeyError) as e:
还是针对上一小节的例子,0不能作为除数的问题,现在来捕获并处理错误,保证代码正常运行。
def func(num):
return 1 / num
try:
func(0)
except:
print('Error!')
可以指定错误类型,并把错误返回的内容打印出来:
def func(num):
return 1 / num
try:
func(0)
except ZeroDivisionError as e:
print('Error:%s.'% e)
3.2 if 判断
其实像这种可预见且情况较少的错误,也可以通过if
条件句来处理,如下例子,不过这里或需要一个约定,就是如果除数为0,返回值是什么,这里我是取None
。
# 代码一
def func(num):
if num == 0:
return None
return 1 / num
func(0)
# 代码二
def func(num):
if num != 0:
return 1 / num
func(0)
3.3 拓展案例:处理KeyError
之前遇到一个问题,就是有一个ID列表和一个列ID与列名的映射表,现需要按列ID的顺序获取列名,以便使用列名替换列ID,如果映射表没有,则以ID:一个随机数
给映射表添加相关映射。
下面抽象为在一个字典中查值,如果值不存在则新增键值对。
dic = {}
def get_value():
print(dic['101'])
get_value()
我们知道KeyError
的报错中会返回对应的key值,所以需要获取该值。
获取该值很简单,使用str(KeyError)
即可获取到,具体如下:
dic = {}
def get_value():
print(dic['101'])
try:
get_value()
except KeyError as e:
print(str(e)) # 结果为:'101'
打印结果是'101'
,看着似乎没错,但实际上两个引号也是字符串的一部分,所以需要加一步去除引号——通过切片str(e)[1:-1]
解决。
另外一个注意点是str(e)
并不是Python内置的用于转化为字符串类型的函数,而是类的一个__str__()
方法,该方法返回报错冒号后面的内容(其他的错误类型亦同)。
下面看看KeyError的类型,并打印几个方法坐下对比:
dic = {}
def get_value():
print(dic['101'])
try:
get_value()
except KeyError as e:
print(type(e)) # 结果为:<class 'KeyError'>
print(e) # 结果为:'101'
print(e.__str__()) # 结果为:'101'
print(str(e)) # 结果为:'101'
print(repr(e)) # 结果为:KeyError('101')
print(str(e)[1:-1]) # 结果为:101
将查不到的键添加到列表中,采用random
生成随机数。
import random
dic = {}
def get_value():
print(dic['101'])
try:
get_value()
except KeyError as e:
dic[str(e)[1:-1]] = '随机%d_%d'% (random.randint(1,100),random.randint(1,100))
get_value()
以上代码适合一次调用,如果使用不同的键调用了两次,则还是会报错!
解决方案:通过闭包函数解决。将以上代码的try…except…
通过函数deal_append_key()
封装,然后在出发except
时,加上对封装函数deal_append_key()
的调用。
import random
dic = {}
def get_value():
print(dic['101'])
print(dic['102'])
def deal_append_key():
try:
get_value()
except KeyError as e:
dic[str(e)[1:-1]] = '随机%d_%d'% (random.randint(1,100),random.randint(1,100))
deal_append_key()
deal_append_key()
四、错误类型
异常名称 | 描述 | 备注 |
---|---|---|
ArithmeticError | 所有数值计算错误的基类 | |
AssertionError | 断言语句失败 | |
AttributeError | 对象没有这个属性 | |
BaseException | 所有异常的基类 | |
DeprecationWarning | 关于被弃用的特征的警告 | |
EnvironmentError | 操作系统错误的基类 | |
EOFError | 没有内建输入,到达EOF 标记 | |
Exception | 常规错误的基类 | |
FloatingPointError | 浮点计算错误 | |
FutureWarning | 关于构造将来语义会有改变的警告 | |
GeneratorExit | 生成器(generator)发生异常来通知退出 | |
ImportError | 导入模块/对象失败 | 没有安装相关模块,导致导入失败 |
IndentationError | 缩进错误 | |
IndexError | 序列中没有此索引(index) | 常见于列表索引报错,一般是超出列表索引 |
IOError | 输入/输出操作失败 | 读或写文件时发生错误 |
KeyboardInterrupt | 用户中断执行(通常是输入^C) | |
KeyError | 映射中没有这个键 | |
LookupError | 无效数据查询的基类 | |
MemoryError | 内存溢出错误(对于Python 解释器不是致命的) | |
NameError | 未声明/初始化对象 (没有属性) | |
NotImplementedError | 尚未实现的方法 | |
OSError | 操作系统错误 | |
OverflowError | 数值运算超出最大限制 | |
OverflowWarning | 旧的关于自动提升为长整型(long)的警告 | |
PendingDeprecationWarning | 关于特性将会被废弃的警告 | |
ReferenceError | 弱引用(Weak reference)试图访问已经垃圾回收了的对象 | |
RuntimeError | 一般的运行时错误 | |
RuntimeWarning | 可疑的运行时行为(runtime behavior)的警告 | |
StandardError | 所有的内建标准异常的基类 | |
StopIteration | 迭代器没有更多的值 | |
SyntaxError | Python 语法错误 | 初学者或更多出现在使用中文符号上 |
SyntaxWarning | 可疑的语法的警告 | |
SystemError | 一般的解释器系统错误 | |
SystemExit | 解释器请求退出 | |
TabError | Tab 和空格混用 | |
TypeError | 对类型无效的操作 | |
UnboundLocalError | 访问未初始化的本地变量 | |
UnicodeDecodeError | Unicode 解码时的错误 | |
UnicodeEncodeError | Unicode 编码时错误 | |
UnicodeError | Unicode 相关的错误 | 编码问题,注意gbk、utf-8、ASCII等编码的使用 |
UnicodeTranslateError | Unicode 转换时错误 | |
UserWarning | 用户代码生成的警告 | |
ValueError | 传入无效的参数 | |
Warning | 警告的基类 | |
WindowsError | 系统调用失败 | |
ZeroDivisionError | 除(或取模)零 (所有数据类型) |
五、小结
<下节预告:迭代器和生成器>
- End -