写点什么

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:005697

评论 2 条评论

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

人工智能 | 视觉场景中的相应时间分析与弹窗检测技术

测吧(北京)科技有限公司

测试

人工智能 | 掌握有参照的 UIDiff 检测技术:优化用户界面的关键工具

测吧(北京)科技有限公司

测试

强大的Kubernetes工具的完整指南

互联网工科生

Kubernetes

数据库安全运维系统厂家在深圳的有哪些?咨询电话多少?

行云管家

数据库 大数据 数据安全 数据库安全 数据安全运维

🚀Flutter应用程序加固的问题及解决方案

雪奈椰子

“大+小模型”赋能油气行业高质量发展

九章云极DataCanvas

让人恶心的多线程代码,真心建议你别用!

伤感汤姆布利柏

Java 低代码 多线程代码

监守自盗,如何防范员工窃密?

尚思卓越

网络安全 信息安全

CodeiumAI单元测试生成和覆盖率统计实践

lklmyy

单元测试 pytest AIGC CodiumAI

为什么要在项目中使用TypeScript?

树上有只程序猿

typescript

回归算法全解析!一文读懂机器学习中的回归模型

不在线第一只蜗牛

机器学习 算法 回归算法

3D模型渲染太耗电脑性能怎么办?

3D建模设计

3D渲染 GPU渲染 渲染调优 CPU渲染

Ableton Live 12 for Mac(音乐制作工具)v12.0b20中文激活版

iMac小白

图像批量处理软件:Retrobatch for mac v2.0.2激活版

iMac小白

C/C++ Zlib库封装MyZip压缩类

不在线第一只蜗牛

c c++ 开源 编程语言

浩鲸科技:为什么要用雪花ID替代数据库自增ID?

王磊

Java 面试

白鸦11周年分享:把有赞做成智能化系统运营商

ToB行业头条

JavaScript 的发展史你真的了解吗?

伤感汤姆布利柏

JavaScript 低代码 js

[开源]MIT开源协议,前后端分离、后台通配权限管理系统

小狗围观科幻

软件定义汽车时代,代码质量先行!

安势信息

车联网 代码质量 汽车软件安全 代码质量检测工具

外贸人必须知道的20个WhatsApp实用技巧

九凌网络

iZotope RX 10 for mac(音频修复和增强软件) 10.4.2完美激活版

mac

苹果mac Windows软件 iZotope RX 10 音频修复和增强软件

打开浏览器,线上玩转C++

庄岩

c++ 在线编程 onlinegdb

两台电脑如何快速传输几百G文件,这款文件传输软件真快

镭速

大文件传输 传输大文件 文件传输工具

NFTScan 正式上线 Starknet NFTScan 浏览器和 NFT API 数据服务

NFT Research

NFT\ NFTScan nft工具

DAPP公排互助项目系统开发丨智能合约技术开发

l8l259l3365

增加外贸订单的实用外贸工具

九凌网络

软件测试 | 基于无监督深度特征的视觉识别技术:人工智能的前沿探索

测吧(北京)科技有限公司

测试

用友助力浙江省国贸供应链企业数智化管理水平提升!

用友BIP

企业数智化

人工智能 | 自然语言处理技术原理介绍

测吧(北京)科技有限公司

测试

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