Python Debug(调试)的终极指南

开发 后端
很多人使用一堆print语句来查看代码中发生了什么。这种方法远不是理想的,有更好的方法可以找出代码的错误所在,本文将探讨其中一些问题和应对方法。

即使您编写了清晰可读的代码,即使您是非常有经验的开发人员,奇怪的bug也不可避免地会出现,您将需要以某种方式调试它们。很多人使用一堆print语句来查看代码中发生了什么。这种方法远不是理想的,有更好的方法可以找出代码的错误所在,本文将探讨其中一些问题和应对方法。

日志是必须的

如果在编写应用程序时没有设置日志记录,那么您最终会后悔的。应用程序中没有任何日志会使故障排除变得非常困难。幸运的是,在Python中,建立基本的日志程序非常简单:

  1. import logging 
  2. logging.basicConfig( 
  3.     filename='application.log'
  4.     level=logging.WARNING, 
  5.     format'[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s'
  6.     datefmt='%H:%M:%S' 
  7.  
  8. logging.error("Some serious error occurred.") 
  9. logging.warning('Function you are using is deprecated.') 

这就是所有你需要开始写日志的文件,它看起来像这样,你可以找到文件的路径使用logger . getloggerclass ().root.handlers[0].baseFilename):

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

这种设置看起来似乎已经足够好了(通常情况下也是如此),但是拥有配置良好、格式化、可读的日志可以使您的工作变得更加容易。改进和扩展配置的一种方法是使用被logger读取的.ini或.yaml文件。举个例子,你可以在配置中做什么:

  1. version: 1 
  2. disable_existing_loggers: true 
  3.  
  4. formatters: 
  5.   standard: 
  6.     format: "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s" 
  7.     datefmt: '%H:%M:%S' 
  8.  
  9. handlers: 
  10.   console:  # handler which will log into stdout 
  11.     class: logging.StreamHandler 
  12.     level: DEBUG 
  13.     formatter: standard  # Use formatter defined above 
  14.     stream: ext://sys.stdout 
  15.   file:  # handler which will log into file 
  16.     class: logging.handlers.RotatingFileHandler 
  17.     level: WARNING 
  18.     formatter: standard  # Use formatter defined above 
  19.     filename: /tmp/warnings.log 
  20.     maxBytes: 10485760 # 10MB 
  21.     backupCount: 10 
  22.     encoding: utf8 
  23.  
  24. root:  # Loggers are organized in hierarchy - this is the root logger config 
  25.   level: ERROR 
  26.   handlers: [console, file]  # Attaches both handler defined above 
  27.  
  28. loggers:  # Defines descendants of root logger 
  29.   mymodule:  # Logger for "mymodule" 
  30.     level: INFO 
  31.     handlers: [file]  # Will only use "file" handler defined above 
  32.     propagate: no  # Will not propagate logs to "root" logger 

在python代码中使用这种扩展的配置将很难导航、编辑和维护。将内容保存在YAML文件中,可以通过非常特定的设置(如上面的设置)更容易地设置和调整多个日志记录器。

在文件中有了配置,意味着我们需要加载。最简单的方法做与YAML文件:

  1. import yaml 
  2. from logging import config 
  3.  
  4. with open("config.yaml", 'rt') as f: 
  5.     config_data = yaml.safe_load(f.read()) 
  6.     config.dictConfig(config_data) 

Python logger实际上并不直接支持YAML文件,但是它支持字典配置,可以使用YAML .safe_load轻松地从YAML创建字典配置。如果您更倾向于使用旧的.ini文件,那么我只想指出,对于新应用程序,根据文档,推荐使用字典configs。

__repr__ 可读的日志

对代码进行简单的改进,使其更具可调试性,可以在类中添加__repr__方法。如果你不熟悉这个方法-它所做的只是返回一个类实例的字符串表示。使用__repr__方法的最佳实践是输出可用于重新创建实例的文本。例如:

  1. class Circle: 
  2.     def __init__(self, x, y, radius): 
  3.         self.x = x 
  4.         self.y = y 
  5.         self.radius = radius 
  6.  
  7.     def __repr__(self): 
  8.         return f"Rectangle({self.x}, {self.y}, {self.radius})" 
  9.  
  10. ... 
  11. c = Circle(100, 80, 30) 
  12. repr(c) 
  13. # Circle(100, 80, 30) 

除了__repr__,在调用print(实例)时,执行__str__方法也是一个好主意。有了这两种方法,你可以通过打印你的变量得到很多信息。

针对字典的__missing__方法

如果出于某种原因需要实现自定义dictionary类,那么在尝试访问一些实际上不存在的密钥时,您可能会遇到一些由keyerror引起的错误。为了避免在代码中到处查看丢失了哪个键(key),你可以实现特殊的__miss__方法,每次KeyError被提出时调用。

  1. class MyDict(dict): 
  2.     def __missing__(self, key): 
  3.         message = f'{key} not present in the dictionary!' 
  4.         logging.warning(message) 
  5.         return message  # Or raise some error instead 

上面的实现非常简单,只返回和记录丢失键的消息,但是您还可以记录其他有价值的信息,以便了解代码中出现了什么问题。

调试崩溃的应用程序

如果您的应用程序在您有机会了解其中发生了什么之前就崩溃了,那么您可能会发现这个技巧非常有用。

使用-i参数运行应用程序(python3 -i app.py)会导致程序一退出就启动交互式shell。此时,您可以检查变量和函数。

如果这还不够好,您可以带一个更强大的工具 - pdb - Python调试器。pdb有很多特性,可以单独写一篇文章来说明。但这里有一个例子和最重要的部分的纲要。让我们先看看崩溃脚本:

  1. # crashing_app.py 
  2. SOME_VAR = 42 
  3.  
  4. class SomeError(Exception): 
  5.     pass 
  6.  
  7. def func(): 
  8.     raise SomeError("Something went wrong...") 
  9.  
  10. func() 

现在,如果我们用-i参数运行它,我们就有机会调试它:

  1. # Run crashing application 
  2. ~ $ python3 -i crashing_app.py 
  3. Traceback (most recent call last): 
  4.   File "crashing_app.py", line 9, in <module> 
  5.     func() 
  6.   File "crashing_app.py", line 7, in func 
  7.     raise SomeError("Something went wrong...") 
  8. __main__.SomeError: Something went wrong... 
  9. >>> # We are interactive shell 
  10. >>> import pdb 
  11. >>> pdb.pm()  # start Post-Mortem debugger 
  12. > .../crashing_app.py(7)func() 
  13. -> raise SomeError("Something went wrong...") 
  14. (Pdb) # Now we are in debugger and can poke around and run some commands: 
  15. (Pdb) p SOME_VAR  # Print value of variable 
  16. 42 
  17. (Pdb) l  # List surrounding code we are working with 
  18.   2      
  19.   3     class SomeError(Exception): 
  20.   4         pass 
  21.   5      
  22.   6     def func(): 
  23.   7  ->     raise SomeError("Something went wrong...") 
  24.   8      
  25.   9     func() 
  26. [EOF] 
  27. (Pdb)  # Continue debugging... set breakpoints, step through the code, etc. 

上面的调试会话非常简单地展示了使用pdb可以做什么。程序结束后,我们进入交互式调试会话。首先,导入pdb并启动调试器。此时,我们可以使用所有pdb命令。作为上面的示例,我们使用p命令打印变量,使用l命令列出代码。大部分时间你可能会想要设置断点,可以与b LINE_NO和运行程序,直到断点(c),然后继续与年代,逐页浏览功能的选择可能与w。

堆栈跟踪

假设您的代码是运行在远程服务器上的Flask或Django应用程序,在那里您无法获得交互式调试会话。在这种情况下,你可以使用traceback和sys包来了解你的代码中失败的地方:

  1. import traceback 
  2. import sys 
  3.  
  4. def func(): 
  5.     try: 
  6.         raise SomeError("Something went wrong...") 
  7.     except: 
  8.         traceback.print_exc(file=sys.stderr) 

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

在调试期间重新加载模块

有时,您可能在交互式shell中调试或试验某些函数,并经常对其进行更改。为了使运行/测试和修改的循环更容易,您可以运行importlib.reload(模块),以避免在每次更改后重新启动交互会话:

  1. >>> import func from module 
  2. >>> func() 
  3. "This is result..." 
  4.  
  5. # Make some changes to "func" 
  6. >>> func() 
  7. "This is result..."  # Outdated result 
  8. >>> from importlib import reload; reload(module)  # Reload "module" after changes made to "func" 
  9. >>> func() 
  10. "New result..." 

这个技巧更多的是关于效率而不是调试。能够跳过一些不必要的步骤,使您的工作流程更快、更高效总是很好的。一般来说,不时地重新加载模块是一个好主意,因为它可以帮助您避免调试已经被修改了很多次的代码。

 

责任编辑:赵宁宁 来源: 今日头条
相关推荐

2023-05-05 17:20:04

2012-08-21 06:53:00

测试软件测试

2015-07-20 09:39:41

Java日志终极指南

2017-03-27 21:14:32

Linux日志指南

2022-06-30 08:00:00

MySQL关系数据库开发

2023-05-23 18:31:14

Rust编程

2020-06-24 12:26:28

企业网络IT管理

2015-11-08 14:44:48

2015-03-05 11:28:51

Linux桌面环境终极指南

2022-02-09 09:00:00

云计算BigQuerySnowflake

2024-02-23 18:59:32

Python函数编程

2013-12-18 09:36:08

企业移动指南

2022-07-22 13:14:57

TypeScript指南

2019-08-16 09:22:38

技术调试互联网

2024-04-12 07:50:40

Python监控利器Time 模块

2011-04-01 10:55:29

OSPFDebug

2022-04-28 10:29:38

数据数据收集

2023-11-14 18:07:44

Python字典项目

2017-03-13 14:30:38

Android开发库指南

2023-11-30 15:53:43

点赞
收藏

51CTO技术栈公众号