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

导读:本篇文章讲解 Python 基础合集13:错误的调试和处理,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、前言

本小节介绍了错误的调试和处理,包含了寻找出现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。

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

注意:

  • 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 迭代器没有更多的值
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 除(或取模)零 (所有数据类型)

五、小结

错误的调试和处理.png

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

– End –

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/66930.html

(0)
小半的头像小半

相关推荐

极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!