Monklof思考和写字的地方

创建高质量Python工程(2)-Python代码检查与调试技巧

2016-01-23

这篇文章介绍一些Python下一些常用的代码检查工具和调试方法。

静态代码分析

Python作为一门动态语言,解释器只会在运行之前检查代码的基本语法,不能进行更加详细的错误分析。例如,"引用未定义的变量", "传递了错误的参数"。这些错误只能在运行的时候被抛出。 静态代码分析 就是用来解决这样问题的技术,它试图在程序未运行之前对代码分析来检查一些可能的错误,避免在运行之后才发现,提升开发效率。

一般而言,静态代码分析工具会提供以下两个功能:

  1. 检查程序中的错误: 如"缩进错误", "少些了冒号", "变量不存在", "实例方法定义未包含self参数", "函数传参数量不对"等等
  2. 检查代码风格: 如"代码单行字符数过多"等等

这两个功能对帮助你解决大部分的低级错误、统一团队编码风格,是非常有用的。

在Python环境之下,常见的静态代码分析工具有:

  1. Pylint: 提供基本语法检查、逻辑错误检查、代码风格分析等功能,是一个非常强大的静态分析工具。(2003年诞生,至今依然处于一个很好的维护状态。)
  2. PyChecker: 提供基本的错误检查。(现在基本处于停止维护状态)
  3. flake8 (=Pyflakes+pep8): Pyflakes只进行一些简单的逻辑检查,不做代码风格分析。pep8则只进行 pep8 的风格检查。而flake8则将pep8和pyflakes的功能整合在一起了。

我偏向于使用Pylint,原因是:

  1. 支持基础语法检查、覆盖大量常见错误检查。
  2. PyChecker的检查方式是通过导入模块,再对模块里面的函数、类和方法进行错误检查。所以会真正运行模块内部的代码,是有副作用的(比如在你的脚本里写了一段插入数据到DB中代码,PyCheker检查语法的时候,就会将这些数据插入DB中)。而Pylint和flake8则只是进行词法分析,不造成任何的影响。另外,PyChecker已经处于无人维护的状态。
  3. Pylint的错误类型覆盖面比flake8更广, 提供更加详细的分析报告。

(注,关于这三者的比较,Stack Overflow上有个有意思的问题: PyLint, PyChecker or PyFlakes?,你可以亲自试试得票最高的jgritty的回答里面的测试样例)

Pylint的使用也很简单,以下面这个例子为例,这个例子想要实现一个线程安全的先进先出队列。

fifoqueue.py:

class FifoQueue(object):

    def __init__(self, max_size=100):
        self.max_size = max_size
        self.lock = threading.Lock()

    def enqueue(self, obj):
        with self.lock:
            if len(self.queue) >= self.max_size:
                raise Exception("queue full")
            self.queue.append(obj)

    def dequeue(self):
        with self.lock:
            if len(self.queue) == 0:
                raise Exception("queue empty")

            obj = queue[0]
            self.queue = self.queue[1:]

        return obj

你能看出这个模块代码有什么问题吗?

我们用Pylint来看看这里面存在什么问题。

安装Pylint

pip install pylint

使用Pylint

默认条件下,Pylint会同时分析错误信息和编码风格问题,这里就不讨论编码风格了,使用-E选项来只找出错误。

pylint -E fifoqueue.py

结果:

************* Module fifoqueue_error
E:  5,20: Undefined variable 'threading' (undefined-variable)
E: 18,18: Undefined variable 'queue' (undefined-variable)
E: 15,19: Access to member 'queue' before its definition line 19 (access-member-before-definition)

准确的找出了,上段代码的问题:

  1. 没有导入threading模块
  2. 第18行中,dequeue函数里面引用了未定义的queue变量,实际上是应该使用self.queue
  3. self.queue变量未在构造函数__init__内初始化。

所以,正确的代码应该是这样的:

fifoqueue.py:

import threading # notice the change here

class FifoQueue(object):

    def __init__(self, max_size=100):
        self.max_size = max_size
        self.lock = threading.Lock()
        self.queue = [] # notice the change here

    def enqueue(self, obj):
        with self.lock:
            if len(self.queue) >= self.max_size:
                raise Exception("queue full")
            self.queue.append(obj)

    def dequeue(self):
        with self.lock:
            if len(self.queue) == 0:
                raise Exception("queue empty")

            obj = self.queue[0] # notice the change here
            self.queue = self.queue[1:]

        return obj

这些错误,其实在编写代码的时候都是很容易发生的,Pylint都很好的发现了,不必等到运行时再发现。

(注: 你可以使用flake8来检查一下以上代码,你会发现flake8无法检查到第三个错误)

调试技巧

Python中,根据我的观察,应用最广的调试方式是这个:

使用print语句来打印运行数据,哪里需要打哪里。通过获得这些运行数据,来判断出异常的位置。

所以,有些同志写的代码里面要么很多还未来得及擦去的print语句,要么是有很多这样的代码: # print XXX

这类同学一般的调试方式是这样的(心理描述):

  1. "这个函数结果为什么不对呢?在这个地方打印一下这个变量A的信息吧"
  2. "变量A的信息还是不够用来定位啊,再打印变量B、C的信息吧"
  3. "为什么B、C变量的值会是这样的数据呢?不应该啊,再把之前的过程也打印出来吧"
  4. 最后他折腾了许久,终于把整个执行流程都打印下来,弄明白了原因。
  5. "删掉这些print语句吧,太烦人了。"
  6. 之后,又出现了这样的问题,于是重复1-6步。

能肯定的是,这种调试方式是能够帮你逐步定位问题原因的。但是效率太低。虽然是动态语言,但是Python还是有挺多能满足日常需求调试工具的。 使用这些调试工具,绝对能提升你的调试效率。

常见的调试工具:

  1. pdb(The Python Debugger): 官方调试器,内置在Python标准模块中,不用安装即可使用。常见的设置断点、打印变量、设置条件、单步运行、显示源代码这些功能都有。
  2. pudb: 一个有"图形界面"的调试器。提供的功能和pdb类似,但是它的图形界面,可以更方便的查看源代码、变量值、断点这些内容。
  3. ipdb: 一个基于 ipython 的pdb。基本功能和pdb一样,只是调试时的shell利用了ipython的一些功能。如有自动补全、语法高亮等功能。是一个增强版的pdb。

(注: ipython是Python的另一种交互式REPL shell。ipython解决了官方REPL没有的自动补全、自动缩进功能,同时添加了一些有用的特性,如:支持一部分常用的bash命令、内置了许多有用的功能和函数。受许多开发者的青睐。)

所以,这三个的核心调试功能其实是一样的,这里就不逐个介绍了。这里给出一些学习方法吧:

  1. pdb: IBM Develpper Works的 Python 代码调试技巧
  2. ipdb: 基本功能和pdb一样,想要学ipython的命令行工具,可以看Using IPython for interactive work,这个是介绍ipython的交互式功能的文档。
  3. pudb: 直接上手试试就能知道怎么用。

pudb的优点就是,可以提供类似于IDE的体验,在一个全屏的界面看到全局信息,这个在记不清代码内容的时候是非常有用的。pdb和ipdb则更适合进行简单的调试,不必要每次调试都要进入一个全屏的界面。那到底使用哪个调试工具?我的建议是: 都学会,根据情况选择调试工具。因为这几个调试工具都挺简单的,用不着一个小时,你就能掌握这三个工具。但是pdb最好能掌握,因为在任何环境下,这个调试器都是可以用的,通过python -m pdb foo.py你就能快速进入调试界面。

最后,介绍一下pudb这个调试工具,个人觉得挺酷炫的。

pudb

还是之前那个先进先出的队列,现在我使用之前有错误的那段代码,来进行调试:

fifoqueue_error.py

class FifoQueue(object):

    def __init__(self, max_size=100):
        self.max_size = max_size
        self.lock = threading.Lock()

    def enqueue(self, obj):
        with self.lock:
            if len(self.queue) >= self.max_size:
                raise Exception("queue full")
            self.queue.append(obj)

    def dequeue(self):
        with self.lock:
            if len(self.queue) == 0:
                raise Exception("queue empty")

            obj = queue[0]
            self.queue = self.queue[1:]

        return obj

if __name__ == "__main__":
    q = FifoQueue()
    q.enqueue(1)
    print q.dequeue()

安装pudb

pip install pudb

进入调试界面

pudb fifoqueue_error.py

进入pudb之后,pudb暂停在程序的第一行代码处。"图形界面"有五个区域: 代码区、命令行区、变量区、栈区、断点区。

出错界面

按下c字符(Continue),pudb会继续运行程序。在这里,由于代码错误,会抛出错误:

可以显示运行时异常的代码位置、本地变量值、栈。你可以在Command line中输入一些调试命令看到更多的你想要看的信息。

总结

其实还有一个神器, Pycharm,一个非常好用的Python IDE。如果有这个工具,上面的一些工具你都可以不用学习了,这个IDE都集成了。

不过如果像我一样,不喜欢用IDE,喜欢编辑器的极速。那么以上这些工具,是提升代码质量和提升调试能力都很有用的工具。

最后,在Python这个动物园里,尽情的畅游吧 :)

(PS. 欢迎在评论区讨论;觉得我写得还行,也可以手动点个赞支持一下~)

同系列更多其他的文章: 《创建高质量的Python项目》

参考

  1. Debugging Python Like a Boss

参与讨论

#1   Wah 2016-01-27 20:48 评论

好极

#2   ibear 2016-02-06 14:51 评论

Pycharm 在 archlinux 下速度奇慢

#3   san 2016-08-12 04:39 评论

不错怎么没有续集了呢,谢谢

#4   monklof 2016-08-14 22:41 评论

@[san] 因为前段时间工作太忙了 :( 后面有时间逐步补上。

#5   leoxiaofei 2017-01-17 09:15 评论

支持,很有用。

#6   ocean0_0 2017-04-06 01:57 评论

希望有新作,将这个系列继续下去\(^o^)/~

#7   张力元 2017-04-20 01:40 评论

太赞了,顶一个

#8   刘好 2017-07-09 08:32 评论

太赞了

评论一下

Captcha