Python终极调试指南

2020 年 7 月 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 年 7 月 17 日 16:003941

评论 2 条评论

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

交易所开发技术,数字货币交易系统源码搭建

135深圳3055源中瑞8032

区块链数字货币钱包开发模式,深圳数字钱包开发

135深圳3055源中瑞8032

《零成本实现Web性能测试——基于Apache JMeter》.pdf

田维常

电子书

《MySQL开发者SQL权威指南》.pdf

田维常

电子书

《Python源码剖析》.pdf

田维常

电子书

《Java Web企业项目实战》.pdf

田维常

电子书

架构师第一期作业(第 7 周)

Cheer

课程作业

渣渣2本学历CRUD一年半,决定改变现状,努力学习两个月成功拿到美团30k offer

Java成神之路

Java 程序员 架构 面试 编程语言

Alibaba技术大牛丢给我一份Spring Cloud笔记,在GitHub的热度居然高达81.6k标星,太强了!

Java成神之路

Java 程序员 架构 面试 编程语言

面试大厂被算法难倒惨遭滑铁卢?这份字节内部大佬整理的《数据结构与算法》学习笔记你一定要看看!

Geek_71bb95

Java 程序员 架构 面试 编程语言

阿里云视频云技术专家 LVS 演讲全文:《“云端一体”的智能媒体生产制作演进之路》

阿里云视频云

媒体 音视频

「混合云」会是云计算的下一个战场吗?

ToB行业头条

阿里云

《循序渐进Linux (第2版)》.pdf

田维常

电子书

聆听无声的话语:手把手教你用ModelArts实现手语识别

华为云开发者社区

AI 图像识别 手语

Nginx-技术专题-入门教程

李浩宇/Alex

架构师训练营第一期 - week7

习习

《人人都是架构师:分布式系统架构落地与瓶颈突破》.pdf

田维常

电子书

深入理解Java虚拟机第三版,通俗易懂,大牛带你轻松搞懂JVM性能调优

Geek_71bb95

Java 程序员 架构 面试 编程语言

《一线架构师实践指南》.pdf

田维常

电子书

大数据处理黑科技:揭秘PB级数仓GaussDB(DWS) 并行计算技术

华为云开发者社区

数据库 并行算子 计算

直播预告 | 应用加固防破解,4.1折就够了

蚂蚁集团移动开发平台 mPaaS

安全攻防 App风险 mPaaS

《用户网络行为画像》.pdf

田维常

电子书

阿里P8Java大神给迷茫的程序员一些中肯建议:“请不要再虚度光阴了!”

Java成神之路

Java 程序员 架构 面试 编程语言

还在为算法烦恼?那你应该还没看过这份Git上70k标星的笔记

Java架构师迁哥

《自己动手写网络爬虫》.pdf

田维常

电子书

银行数字化转型:需建立起以体验为核心、数据为基础、技术为驱动的架构体系

CECBC区块链专委会

银行 数字经济

央行数字货币为人民币国际化之路提供推动力

CECBC区块链专委会

数字货币

阿里技术四面+交叉面+HR面成功拿到offer,谁说双非本科进不了大厂?

Java成神之路

Java 程序员 架构 面试 编程语言

配置企业管理系统,什么样的工作流才有用

雯雯写代码

工作流 企业管理系统

英特尔顶级技术专家合力缔造精品:Linux开源网络全栈详解

周老师

Java 编程 程序员 架构 面试

区块链+能源 大放异彩

CECBC区块链专委会

区块链 能源

Python终极调试指南-InfoQ