OceaBase开发者大会落地上海!4月20日共同探索数据库前沿趋势!报名戳 了解详情
写点什么

Python 终极调试指南

  • 2020-07-17
  • 本文字数:5115 字

    阅读完需:约 17 分钟

Python终极调试指南

本文介绍了一些 Python 调试的高级技巧。如果你还在像新手一样无脑 print 调试,那么赶紧学习一下如何优雅地调试 Python 代码吧。


本文最初发布于 martinheinz.dev 网站,经原作者授权由 InfoQ 中文站翻译并分享。


作为经验丰富的开发人员,即便你编写了清晰易读的代码,并对代码进行了全方位的测试,但在某些时候程序还是会不可避免地出现一些奇怪的 Bug,这时候你就需要以某种方式 Debug。不少程序员喜欢使用一堆 print 语句来查看代码运行情况。这种方法有点低级,太傻瓜了;实际上有很多更好的方法来帮你定位代码中的问题,我们将在本文中介绍这些方法。

使用 Logging 模块

如果你编写的应用程序没有使用日志功能,那你终究会后悔没有及时用它的。如果应用程序中没有打印任何运行日志,就很难对程序错误进行故障定位及排除。幸运的是在 Python 中,我们很容易配置基本的日志模块:


import logginglogging.basicConfig(    filename='application.log',    level=logging.WARNING,    format= '[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s',    datefmt='%H:%M:%S')logging.error("Some serious error occurred.")logging.warning('Function you are using is deprecated.')
复制代码


这就是开始将日志写入文件所需的全部操作,使用时,你可以通过 logging.getLoggerClass().root.handlers[0].baseFilename 找到文件的路径:


[12:52:35] {<stdin>:1} ERROR - Some serious error occurred.[12:52:35] {<stdin>:1} WARNING - Function you are using is deprecated.
复制代码


这种设置看起来似乎已经足够好了(通常是这样),但是配置合理、格式清晰、可读性强的日志可以让你 Debug 起来更加轻松。优化日志配置的一种方法是使用.ini 或.yaml 配置文件。下面给你推荐一种配置示例:


version: 1disable_existing_loggers: trueformatters:  standard:    format: "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s"    datefmt: '%H:%M:%S'handlers:  console:  # handler which will log into stdout    class: logging.StreamHandler    level: DEBUG    formatter: standard  # Use formatter defined above    stream: ext://sys.stdout  file:  # handler which will log into file    class: logging.handlers.RotatingFileHandler    level: WARNING    formatter: standard  # Use formatter defined above    filename: /tmp/warnings.log    maxBytes: 10485760 # 10MB    backupCount: 10    encoding: utf8root:  # Loggers are organized in hierarchy - this is the root logger config  level: ERROR  handlers: [console, file]  # Attaches both handler defined aboveloggers:  # Defines descendants of root logger  mymodule:  # Logger for "mymodule"    level: INFO    handlers: [file]  # Will only use "file" handler defined above    propagate: no  # Will not propagate logs to "root" logger
复制代码


在 python 代码中使用这种通用的配置将很难编辑和维护。将配置内容保存在 YAML 文件中,通过加载配置文件的形式,我们就可以避免上述问题,后续也可以很轻松地修改日志配置。


如果你想知道所有这些配置字段的含义,可以查看这篇文档,它们中的大多数只是关键字参数,如上面的示例所示。


我们已经在配置文件中定义好了日志组件的相关配置,接下来我们需要以某种方式加载该配置。如果使用的是 YAML 配置文件,最简单地加载配置的方法如下所示:


import yamlfrom logging import configwith open("config.yaml", 'rt') as f:    config_data = yaml.safe_load(f.read())    config.dictConfig(config_data)
复制代码


Python logger 实际上并不直接支持 YAML 文件,但它支持字典配置,可以使用 yaml.safe_load 从 YAML 文件轻松创建字典配置。如果你倾向于使用.ini 文件,那么我只想指出,对于新应用程序,很多文档都建议使用字典配置。有关更多示例,可以查看使用手册

使用日志装饰器

继续前面讲到的日志模块技巧。你可能会遇到这么一种情况,就是想 debug 函数调用执行的情况。你可以使用日志装饰器,无需修改函数主体代码即可实现:


from functools import wraps, partialimport loggingdef attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object    if func is None:        return partial(attach_wrapper, obj)    setattr(obj, func.__name__, func)    return funcdef log(level, message):  # Actual decorator    def decorate(func):        logger = logging.getLogger(func.__module__)  # Setup logger        formatter = logging.Formatter(            '%(asctime)s - %(name)s - %(levelname)s - %(message)s')        handler = logging.StreamHandler()        handler.setFormatter(formatter)        logger.addHandler(handler)        log_message = f"{func.__name__} - {message}"        @wraps(func)        def wrapper(*args, **kwargs):  # Logs the message and before executing the decorated function            logger.log(level, log_message)            return func(*args, **kwargs)        @attach_wrapper(wrapper)  # Attaches "set_level" to "wrapper" as attribute        def set_level(new_level):  # Function that allows us to set log level            nonlocal level            level = new_level        @attach_wrapper(wrapper)  # Attaches "set_message" to "wrapper" as attribute        def set_message(new_message):  # Function that allows us to set message            nonlocal log_message            log_message = f"{func.__name__} - {new_message}"        return wrapper    return decorate# Example Usage@log(logging.WARN, "example-param")def somefunc(args):    return argssomefunc("some args")somefunc.set_level(logging.CRITICAL)  # Change log level by accessing internal decorator functionsomefunc.set_message("new-message")  # Change log message by accessing internal decorator functionsomefunc("some args")
复制代码


说实话,这可能需要花一些时间来装饰被调用函数(实际上,你需要做的仅仅是复制粘贴一下就好了)。它的巧妙之处在于通过 log 函数设置参数,并将参数用于内部 wrapper 函数。然后,通过添加附加到装饰器的访问器函数使这些参数可调。至于 functools.wraps 装饰器,如果我们在这里不使用它,被装饰的函数的名称(func .__ name__)将被装饰器的名称所覆盖。在这里我们需要 functools.wraps 装饰器,因为我们 debug 时要使用真实的函数名称。它的原理是拷贝原始函数名称、函数文档描述以及参数列表到装饰器函数上。


下面就是上面代码的输出。看起来很整洁吧?


2020-05-01 14:42:10,289 - __main__ - WARNING - somefunc - example-param2020-05-01 14:42:10,289 - __main__ - CRITICAL - somefunc - new-message
复制代码

重写对象的__repr__

可以在类中添加__repr__方法来改进一下代码,使其更易于调试。它的功能就是返回类实例的字符串表示形式。__repr__方法的最佳实践是输出可用于重新创建实例的文本。例如:


class Circle:    def __init__(self, x, y, radius):        self.x = x        self.y = y        self.radius = radius    def __repr__(self):        return f"Rectangle({self.x}, {self.y}, {self.radius})"...c = Circle(100, 80, 30)repr(c)# Circle(100, 80, 30)
复制代码


如果不希望或不能像上面那样表示对象,另一个好的方法是使用<…>表示,例如<_io.TextIOWrapper name=‘somefile.txt’ mode=‘w’ encoding=‘UTF-8’>。


除了__repr__以外,重写__str__方法也是一个好方法,该方法在使用 print(instance)时被默认调用。使用这两种方法,你只需打印变量即可获得很多信息。

重写字典类的__missing__方法

如果出于某种原因你需要实现自定义字典类,那么当你尝试访问实际上不存在的键时,可能会因 KeyErrors 引起一些错误。为了避免在 debug 代码时没有头绪,可以实现__missing__这一特殊方法,该方法在每次引发 KeyError 时都会被调用。


class MyDict(dict):    def __missing__(self, key):        message = f'{key} not present in the dictionary!'        logging.warning(message)        return message  # Or raise some error instead
复制代码


上面的实现非常简单,仅返回并记录缺少键的消息,但是你也可以记录其他有价值的信息,以便在代码出问题时给你提供更多上下文参考。

调试崩溃的应用程序

如果应用程序崩溃后你才有机会查看其中发生的情况,那么你可能会发现下面这个技巧非常有用。


你需要使用-i 参数(python3 -i app.py)运行应用程序,该参数会使程序在退出后立即启动并进入交互式 shell。 此时,你可以检查当前环境下的变量和函数。


如果这还不够好,那么你可以使用更厉害的 pdb,即 Python Debugger。pdb 具有很多功能,这些功能可以撰写一篇长文来介绍了。下面给出一个示例,我只摘抄了最重要的部分。首先让我们看一下崩溃的脚本:


# crashing_app.pySOME_VAR = 42class SomeError(Exception):    passdef func():    raise SomeError("Something went wrong...")func()
复制代码


现在,如果我们使用-i 参数运行它,我们将有机会对其进行调试:


# Run crashing application~ $ python3 -i crashing_app.pyTraceback (most recent call last):  File "crashing_app.py", line 9, in <module>    func()  File "crashing_app.py", line 7, in func    raise SomeError("Something went wrong...")__main__.SomeError: Something went wrong...>>> # We are interactive shell>>> import pdb>>> pdb.pm()  # start Post-Mortem debugger> .../crashing_app.py(7)func()-> raise SomeError("Something went wrong...")(Pdb) # Now we are in debugger and can poke around and run some commands:(Pdb) p SOME_VAR  # Print value of variable42(Pdb) l  # List surrounding code we are working with  2  3   class SomeError(Exception):  4       pass  5  6   def func():  7  ->     raise SomeError("Something went wrong...")  8  9   func()[EOF](Pdb)  # Continue debugging... set breakpoints, step through the code, etc.
复制代码


上面的调试会话非常清晰地显示了可以使用 pdb 进行的操作。程序终止后,我们进入交互式调试会话。 首先,我们导入 pdb 并启动调试器。此时我们可以使用所有的 pdb 命令。 在上面的示例中,我们使用 p 命令打印变量,并使用 l 命令列出代码。 大多数时候,你可能希望设置断点,可以使用 b LINE_NO 来设置断点,然后运行程序直到断点(c)被暂停,然后继续使用 s 逐步执行该函数,还可以选择使用 w 打印堆栈信息。有关命令的完整列表,可以查阅pdb使用文档

检查堆栈信息

假设你的代码是在远程服务器上运行的 Flask 或 Django 应用程序,你是无法获得交互式调试会话的。 在这种情况下,你可以借助 traceback 和 sys 软件包来更深入地了解代码中发生的异常:


import tracebackimport sysdef func():    try:        raise SomeError("Something went wrong...")    except:        traceback.print_exc(file=sys.stderr)
复制代码


运行后,上面的代码将打印最后引发的异常。 除了打印异常信息,还可以使用 traceback 包打印堆栈信息(traceback.print_stack())或提取原始堆栈帧,对其格式化并进一步检查(traceback.format_list(traceback.extract_stack()))。

调试过程中重新加载模块

有时你可能正在调试或在交互式 Shell 中对一些方法函数进行测试,并对其进行一些修改。 为了简化代码的运行/测试和修改过程,可以运行 importlib.reload(module)以避免每次更改后都必须重新启动交互式会话:


>>> import func from module>>> func()"This is result..."# Make some changes to "func">>> func()"This is result..."  # Outdated result>>> from importlib import reload; reload(module)  # Reload "module" after changes made to "func">>> func()"New result..."
复制代码


这一技巧更多是为了提高效率。 它可以帮助你跳过一些不必要的步骤,让你的工作更快、更高效。 实时重新加载模块这一功能经常很好用,因为它可以帮助你避免调试已经修改过很多次的代码,节省宝贵时间。


作者介绍:


Martin Heinz,开发运维工程师,现就职与 IBM。


原文链接:


Ultimate Guide to Python Debugging


2020-07-17 16:005302

评论 2 条评论

发布
用户头像
您好,最后一句作者简介,应是:现就职于 IBM,不是现就职与 IBM。
2020-07-17 17:22
回复
确有疏忽,感谢指正!
2020-07-17 23:38
回复
没有更多了
发现更多内容

百度App Objective-C/Swift 组件化混编之路(二)- 工程化

百度开发者中心

全方位构建信创生态体系,焱融科技完成海光 CPU 生态兼容性认证

焱融科技

云计算 分布式 高性能 文件存储 生态

官宣 | 极狐GitLab SaaS来了

极狐GitLab

剖析react核心设计原理--异步执行调度

有道技术团队

百度App Objective-C/Swift 组件化混编之路(一)

百度开发者中心

深入浅出特征工程 – 基于 OpenMLDB 的实践指南(上)

第四范式开发者社区

机器学习 数据库 大数据 OpenMLDB

前端培训:Vue3 的自定义指令

@零度

Vue 前端开发

【Python训练营】Python每日一练----第23天:字符计数

是Dream呀

2月月更

【堡垒机】堡垒机是啥?一线品牌有哪些?

行云管家

网络安全 数据安全 堡垒机 IT运维

作业7

施正威

深入解析 Flink 细粒度资源管理

Apache Flink

大数据 flink 开源 编程 实时计算

跨项目度量,CTO、PMO们的好帮手

阿里云云效

阿里云 云原生 敏捷开发 CTO 研发度量

英特尔至强单月总出货量超其他厂商全年服务器CPU总出货量

科技新消息

注意!这种笔试方式正在逐渐被取代……

ShowMeBug

笔试 在线面试

DevSecOps端到端的安全能力构建为什么重要

极狐GitLab

安全 DevSecOps

为冬奥加油——利用贝塞尔曲线实现冰墩墩

战场小包

前端 canvas 冬奥会 2月月更

OpenHarmony移植案例与原理:startup子系统之syspara_lite系统属性部件

华为云开发者联盟

Token OpenHarmony startup子系统 syspara_lite系统

关于极狐GitLab SaaS,你应该知道这些!

极狐GitLab

DevSecOps SaaS平台

用UML来描述领域模型吧

蜜糖的代码注释

UML 领域建模 2月月更

人才短缺、成本高昂,制造企业智能化转型路径如何破局?

百度开发者中心

在线YAML转XML工具

入门小站

工具

美景本天成,妙笔偶得之——“妙笔”是怎样炼成的?

百度开发者中心

虎符Hoo交易所开启全新生态布局 完成HOO首次回购

区块链前沿News

Hoo 虎符交易所 HOO回购

HTTP流量神器Goreplay核心源码详解

华为云开发者联盟

Go 流量 GOREPLAY TCP/HTTP

优酷 IPv6 演进和实践指南

阿里巴巴终端技术

ipv6 移动网络

Mysql数据库表中有索引为什么还是查询慢?

慕枫技术笔记

数据库 2月月更

【网络安全】知名网络安全企业有哪些?

行云管家

网络安全 数据安全 堡垒机

直播预告|一线专家邀你共话:数据科学赋能多元应用场景价值

MobTech袤博科技

算法 数据 商业

大数据培训:Flink 快照分析

@零度

flink 大数据开发

2021年第4季度中国网络零售B2C市场交易规模达23593.9亿元

易观分析

B2C 网络零售

教你一个快速视频处理的神器:Python moviepy

华为云开发者联盟

Python 视频 音频 视频处理 Moviepy

Python终极调试指南_语言 & 开发_Martin Heinz_InfoQ精选文章