Python 基础合集13:错误的调试和处理

一、前言

本小节介绍了错误的调试和处理,包含了寻找出现bug的代码的方法,以及处理bug的方法,另外还附加了一些错误类型。

环境说明:Python 3.6、windows11 64位

二、调试:找出错误📌

之前看到一句话,很在理:出错并不可怕,可怕的是不知道哪里出错了。为了找到这个错误,有时候需要花很长时间去调试,费时费力,不过这无可避免。当然,在经验越发丰富之后,debug的能力也会更加的强,处理起来会更加得心应手。熟能生巧是亘古不变的真理!多写多练。

2.1 看懂报错

其实在熟悉Python的报错之后,根据报错提示,能够解决绝大部分的bug。Python的报错会一级一级地打印出来告诉你错误的行。
如下截图,一个简单的代码,运行之后报错了。这个报错类型,说的就是除数为0的错误。然后看上面的代码索引,从上往下看,首先是第4行错了,这一行调用了func()函数。下一个提示是第2行,就是func()函数里返回1/num,所以就定位到了问题。1/0引发了ZeroDivisionError错误。
image.png

2.2 print()+注释

这类报错一般是出现那种不明显的错误才会用到,比如说少了个标点符号、爬虫时一开始没有问题,后面被反爬了等。
具体操作方式就是注释掉报错之后的代码,然后往上加几个print()打印一些变量。
比如以下代码,我给aa赋值的时候,少敲了一个],但报错并不会指向第一行,而是指向第二行,初学者可能会盯着第二行纳闷,但实际错误是在上一行。这是因为在第一行找不到]时,会去第二行找,但是第二行却是一个新的赋值语句,所以就报错。

aa = [1,2,3
bb = 0
print(aa,bb)

image.png
按照print()+注释的方法,就是将第2~3行注释,然后print(aa)看结果,这时会发现报错变成了print(aa),但是print(aa)没问题,这是可断定是第一行出现了问题,所以检查一下第一行便可发现是少了一个]
image.png

2.3 assert 断言

语法:assert [条件],'条件不成立的提示语句'
断言可以捕获错误,但是它也会报错,只是以另外一种提示返回,返回的提示可以进行自定义,错误类型都是AssertionError
如下例子,在执行1/num前加上一个断言语句。

def func(num):
    assert num != 0, 'num is zero!'
    return 1 / num

func(0)

image.png

2.4 raise 自定义异常

语法:raise [异常类型]('[返回信息]')
raiseassert有点相似,它也会报错,只是以另外一种提示返回,返回的错误类型和提示可以自行定义。
如下代码,当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内置的库或第三方库代码;另一种是只执行我的代码。可根据需求选择。
image.png

2.6 logging

logging模块的日志有六个级别,按从低到高分别是NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。

级别数值说明
NOTSET0不设置
DEBUG10细节信息,仅当诊断问题时适用。
INFO20确认程序按预期运行
WARNING30表明有已经或即将发生的意外(例如:磁盘空间不足)。程序仍按预期进行
ERROR40由于严重的问题,程序的某些功能已经不能正常执行
CRITICAL50严重的错误,表明程序已不能继续执行

注意:

  • logging只会追踪大于设定级别及以上的报错,级别以下的会被忽略。如级别WARNING只会追踪WARNINGERRORCRITICAL,不会追踪INFODEBUGNOTSET
  • 根记录器的默认级别为WARNING,可通过logging.basicConfig(level=logging.DEBUG)level参数更改错误级别;
  • 所追踪事件可以以不同形式处理,最简单的方式是输出到控制台,另一种常用的方式是写入磁盘文件。给logging.basicConfig(filename=r"D:\test.log")filename指定文件路径写入磁盘。

如下例子,指定了filename之后,运行结果不会在控制台打印任何东西,相关的报错记录写入了D:\test.log,内容如下图,自定义测试部分都会先写入到文件中,后续跑代码的应用部分,报错之后,会紧接着在后面写入。
image.png

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,一方面是改正错误,另外一方面是避开错误。

  • 改正错误:这个好理解,就是改为正确的逻辑;
  • 避开错误:这个是指允许错误,但是我通过一个判断逻辑对进行识别分类处理。如果正常执行,则顺流程继续跑,如果报错,分一支流处理错误,处理完再并入正常流程继续执行。image.png

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!')

image.png
可以指定错误类型,并把错误返回的内容打印出来:

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()

image.png
我们知道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

image.png

将查不到的键添加到列表中,采用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迭代器没有更多的值
SyntaxErrorPython 语法错误初学者或更多出现在使用中文符号上
SyntaxWarning可疑的语法的警告
SystemError一般的解释器系统错误
SystemExit解释器请求退出
TabErrorTab 和空格混用
TypeError对类型无效的操作
UnboundLocalError访问未初始化的本地变量
UnicodeDecodeErrorUnicode 解码时的错误
UnicodeEncodeErrorUnicode 编码时错误
UnicodeErrorUnicode 相关的错误编码问题,注意gbk、utf-8、ASCII等编码的使用
UnicodeTranslateErrorUnicode 转换时错误
UserWarning用户代码生成的警告
ValueError传入无效的参数
Warning警告的基类
WindowsError系统调用失败
ZeroDivisionError除(或取模)零 (所有数据类型)

五、小结

错误的调试和处理.png




<下节预告:迭代器和生成器>


- End -

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xin学数据

为你点亮一盏灯,愿你前进无阻。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值