Python 秘籍手册(三)
十四、测试和调试
这本书主要是给你一些如何编写 Python 代码的技巧和诀窍。很少的篇幅花在使这段代码尽可能好上。在这一章中,你将会看到分析代码性能的技术。您还将看到在您编写的代码中出现错误时调试程序的方法。
14-1.为一段代码计时
问题
你想对一段代码计时,看看它运行了多长时间。
解决办法
Python 标准库包括一个名为timeit的包,它可以多次运行代码并获得平均运行时间。
它是如何工作的
如果您有 Python 语句,您可以从命令行通过timeit包运行它们,如清单 14-1 所示。
python -m timeit 'print(42)'
10000000 loops, best of 3: 0.035 usec per loop
Listing 14-1.Using the timeit Command
清单 14-2 展示了如何在 Python 解释器中完成类似的任务。
>>> import timeit
>>> timeit.timeit('42*42', number=1000)
0.0001980264466965309
Listing 14-2.Timing Python Code with timeit
如您所见,您需要明确地告诉timeit运行 Python 语句的次数。
14-2.分析代码
问题
您希望分析您的代码,看看性能瓶颈在哪里。
解决办法
Python 标准库包括两个包,可以用来分析您的代码:profile和cProfile。
它是如何工作的
profile和cProfile都为分析工具提供了一个公共接口来查看代码的性能。这两个包的主要区别在于它们在分析代码时各自的性能。包cProfile是用 C 写的,所以它对你自己代码的运行时间影响很小。然而,为了获得这样的速度,无论您在哪个系统上分析代码,都需要对它进行编译。package profile是用纯 Python 写的,所以运行起来会比cProfile慢一些。然而,优点是profile将在您的代码运行的任何地方运行,并且很容易扩展profile的功能以添加额外的特性。
有几种方法可以将分析包用于您自己的代码。如果您的代码已经捆绑在一组函数中,您可以简单地在分析器下运行它,如清单 14-3 所示。
>>> def my_func():
… return 42
>>> import profile
>>> profile.run('my_func')
4 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :0(exec)
1 0.000 0.000 0.000 0.000 :0(setprofile)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 profile:0(my_func)
0 0.000 0.000 profile:0(profiler)
Listing 14-3.Running the Profiler
如您所见,您可以获得相当多的关于代码中每个函数调用所花费时间的信息。通过分析器运行您自己的代码还有其他几种方法。如果您想要分析的程序已经打包成一个脚本文件,您可以选择从命令行通过分析器运行它,如清单 14-4 所示。
python -m profile -o myscript.out myscript.py
Listing 14-4.Running the Profiler from the Command Line
该命令将把二进制版本的分析结果转储到文件myscript.out中。如果您需要在远程机器上运行概要分析步骤,但想在以后查看结果,这是很方便的。使用pstats包可以看到结果。清单 14-5 展示了如何从这个二进制文件中获得基本的统计数据。
>>> import pstats
>>> p = pstats.Stats('myscript.out')
>>> p.print_stats()
Sun Sep 11 20:39:14 2016 myscript.out
9 function calls in 0.000 seconds
Random listing order was used
ncalls tottime percall cumtime percall filename:lineno(function)
2 0.000 0.000 0.000 0.000 C:\Users\berna_000\Anaconda3_4\lib\encodings\cp850.py:18(encode)
1 0.000 0.000 0.000 0.000 :0(print)
2 0.000 0.000 0.000 0.000 :0(charmap_encode)
1 0.000 0.000 0.000 0.000 :0(setprofile)
1 0.000 0.000 0.000 0.000 myscript.py:1(<module>)
1 0.000 0.000 0.000 0.000 profile:0(<code object <module> at 0x000001F1B82040C0, file "myscript.py", line 1>)
0 0.000 0.000 profile:0(profiler)
1 0.000 0.000 0.000 0.000 :0(exec)
<pstats.Stats object at 0x000001F1B8208550>
Listing 14-5.Reading a Profiling Run
在pstats中有很多选项可以帮助你挖掘结果输出文件。
14-3.跟踪子程序
问题
您需要跟踪您的代码使用了哪些子例程,以查看您的程序还使用了哪些其他函数。
解决办法
Python 标准库包括一个名为trace的包,它可以为您提供覆盖列表、调用者/被调用者关系以及所有被调用函数的列表。
它是如何工作的
可以从命令行使用trace模块,如果您已经将代码捆绑在脚本文件中,这将非常有用。清单 14-6 展示了你如何做来追踪你的脚本。
python -m trace --trace myscript.py
--- modulename: myscript, funcname: <module>
myscript.py(1): print('42')
--- modulename: cp850, funcname: encode
cp850.py(19): return codecs.charmap_encode(input,self.errors,encoding_map)[0]
42 --- modulename: cp850, funcname: encode
cp850.py(19): return codecs.charmap_encode(input,self.errors,encoding_map)[0]
--- modulename: trace, funcname: _unsettrace
trace.py(77): sys.settrace(None)
Listing 14-6.Tracing a Program
您可以使用以下跟踪选项来收集不同种类的数据:
| `--count` | 计算每条语句执行的次数 | | `--trace` | 执行时显示行 | | `--listfuncs` | 执行时显示功能 | | `--trackcalls` | 显示调用关系 |您也可以在 Python 代码中使用trace。它包括两个主要类别:Trace和CoverageResults。清单 14-7 展示了如何通过一个命令进行跟踪。
>>> import trace
>>> tracer = trace.Trace()
>>> tracer.run('print("Hello World")')
Hello World
--- modulename: trace, funcname: _unsettrace
trace.py(80): sys.settrace(None)
Listing 14-7.Tracing a Command in Python Code
14-4.跟踪内存分配
问题
您需要跟踪程序中的内存分配,以了解内存的使用情况。
解决办法
Python 标准库包括一个名为tracemalloc的模块,它可以跟踪内存分配和内存使用统计。
它是如何工作的
要使用tracemalloc,您需要启动它,以便它可以随着时间的推移收集内存信息。清单 14-8 展示了如何获得内存使用中的前 10 名违规者。
import tracemalloc
tracemalloc.start()
# Run you code here
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for curr_stat in top_stats:
print(curr_stat)
Listing 14-8.Getting Memory Statistics
您还可以通过拍摄多个快照来查看内存使用如何随时间变化。有益的是,快照对象有一个名为compare_to()的方法,允许您查看它们之间的区别,如清单 14-9 所示。
import tracemalloc
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
# run your code
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
Listing 14-9.Comparing Two Memory Snapshots
然后您可以查看top_stats对象,看看内存使用是如何随时间变化的。
14-5.执行单元测试
问题
您希望对代码运行单元测试来验证程序行为。
解决办法
Python 标准库包括一个名为unittest的模块,可用于为您的代码构建测试用例。
它是如何工作的
为了创建单元测试,你需要子类化TestCase类并添加测试用例来验证你的代码。每个测试用例必须以 test 作为前缀来命名。然后使用assertEqual()、assertTrue()、assertFalse()和assertRaises()来验证条件。清单 14-10 中显示了一个简单的例子。
import unittest
class MyTestCase(unittest.TestCase):
def test_the_answer(self):
assertEqual(self.curr_val, 42)
if __name__ == '__main__':
unittest.main()
Listing 14-10.A Simple Test Case
这只是对在 Python 中使用测试用例的简短介绍。有整本书致力于组织和设计测试驱动代码。
14-6.调试代码
问题
您需要调试代码中出现的问题。
解决办法
Python 标准库包括一个名为pdb的包,它为代码的操作提供了一个调试接口。
它是如何工作的
如果你想交互式地使用它,你可以在你的 Python 解释器中导入pdb模块,并用它来运行你的代码,如清单 14-11 所示。
>>> import pdb
>>> pdb.run('my_func()')
Listing 14-11.Running Code Under the Debugger
这将进入调试器的交互模式。这通过提示(pdb)突出显示。接口类似于其他基于文本的调试器,比如gdb。如果你想在调试器中运行整个脚本,你可以从命令行执行,如清单 14-12 所示。
python -m pdb myscript.py
Listing 14-12.Debugging a Script File
如果您的脚本异常退出,这将使您进入调试器的事后分析会话。如果您大致知道问题可能出在哪里,您可以添加一行代码来中断调试器,如清单 14-13 所示。
import pdb; pdb.set_trace()
Listing 14-13.Dropping into the Debugger
然后,您可以逐句通过代码来定位代码中问题的根源。如果您处于交互式会话中,可以通过调试器手动运行代码。清单 14-14 展示了一个单步执行函数的例子。
>>> import pdb
>>> def myfunc():
.... print("Hello World")
>>> pdb.run('myfunc()')
> <string>(1)<module>()->None
(Pdb) step
--Call--
> <ipython-input-11-b0e3e2c712c8>(1)myfunc()
-> def myfunc():
(Pdb) step
> <ipython-input-11-b0e3e2c712c8>(2)myfunc()
-> print("Hello World")
(Pdb) step
Hello World
--Return--
> <ipython-input-11-b0e3e2c712c8>(2)myfunc()->None
-> print("Hello World")
(Pdb) step
--Return--
> <string>(1)<module>()->None
(Pdb) step
> /usr/lib/python3.4/bdb.py(435)run()
-> self.quitting = True
(Pdb) step
Listing 14-14.Stepping Through a Function with pdb
十五、C 和其他扩展
Python 的一大优点是它不是每项工作的最佳工具,更重要的是,它知道它不是每项工作的最佳工具。由于这种自我意识,Python 从一开始就被设计成可以用 c 语言编写的代码进行扩展。这种能力是由一个名为 Cython 的模块提供的,该模块可从 http://cython.org 获得。在这一章中,你将看到一些不同的方法,你可以把 Cython 包含在你自己的 Python 程序中,以提高它的性能或者增加额外的功能。
15-1.编译 Python 代码
问题
你想把你的 Python 代码编译成 C 以获得加速。
解决办法
Cython 包提供了一种混合编译 C 代码和 Python 的机制。
它是如何工作的
初始设置适用于本章中的以下所有示例。你的系统上需要有一个 C 编译器。如果您运行的是 Linux 或 Mac OS,那么您可以使用 gcc 作为所需的编译器。要在 Mac OS 上获得编译器,您需要安装 XCode 包。至于 Windows,涉及的步骤更多。Cython 文档中有一个完整的附录,专门用于指导如何设置 Windows。同样,您需要安装 Cython。您可以从源代码安装它,也可以使用 pip 安装它,如清单 15-1 所示。
pip install --user cython
Listing 15-1.Installing Cython with pip
一旦所有的东西都安装好了,你就需要编写将要编译成 c 的 Python 代码。这些代码保存在一个以.pyx结尾的文件中,而不是以.py结尾。然后,这个 Cython 源文件以几种不同的方式之一进行编译。对于较大的项目,处理编译的最灵活和健壮的方法是编写一个setup.py文件并使用distutils。在这么短的篇幅里介绍这个方法有点太复杂了。令人高兴的是,有一些其他更简单的方法可以让您立即在代码中使用 Cython。
清单 15-2 展示了一个例子。只有一个功能的文件。
def print_msg():
print("Hello World")
Listing 15-2.HelloWorld.pyx File
Cython 包含一个名为pyximport的模块,它将编译。pyx当您尝试导入文件时,它们会在后台运行。清单 15-3 展示了如何在交互式 Python 会话中使用它。
>>> import pyximport
>>> pyximport.install()
>>> import HelloWorld
>>> print_msg()
Hello World
Listing 15-3.Using pyximport
这适用于整个源文件。如果您希望编译的代码段更短,您可以让 Cython 直接在 Python 源代码的中间进行内联编译。清单 15-4 给出了一个内联编译代码的例子。
>>> import cython
>>> def my_adder(a, b):
... ret = cython.inline("return a+b")
...
Listing 15-4.Using Inlined Cython Code
为了提高效率,内联代码的编译版本被缓存。
15-2.使用静态类型
问题
你想通过给对象一个类型来加速对它们的访问。
解决办法
您可以安装 Cython 模块以及支持的 C 编译器,来定义新类型,这些新类型的访问和处理速度比 Python 对象快得多。
它是如何工作的
为了使用静态类型,Cython 引入了一个名为cdef的新关键字。当使用这种方法时,您可以获得比用 Cython 编译 Python 代码更快的速度。清单 15-5 展示了一个集成问题的例子。
def f(x):
return x**2-42
def integrate_f(a, b, N):
s = 0
dx = (b-a)/N
for I in range(N):
s += f(a+i*dx)
return s*dx
Listing 15-5.Pure Python Integration Problem
在 Cython 下编译它将提供一定程度的加速,但是仍然会发生类型检查。这在循环中尤其昂贵,因为变量被多次访问。清单 15-6 展示了相同的例子,除了使用了cdef关键字。
def f(double x):
return x**2-42
def integrate_f(double a, double b, int N):
cdef int i
cdef double s, dx
s = 0
dx = (b-a)/N
for I in range(N):
s += f(a+i*dx)
return s*dx
Listing 15-6.Integration Problem Using Static Typing
这段代码去掉了所有那些代价高昂的类型检查,在试图优化代码时会是一个很大的帮助。为了编译这些文件,您可以使用 Cython 命令行工具来生成一个 C 源文件,该文件可以被编译成一个共享对象,并导入到 Python 中。假设上面的例子保存在一个名为mycode.pyx的文件中,清单 15-7 展示了如何使用 GCC。
cython myfile.pyx
gcc -shared -o myfile.so myfile.c `python3-config --includes`
Listing 15-7.Compiling Cython Code Manually
然后,您可以从 Python 中导入这个新编译的共享对象。
15-3.从 C 中调用 Python
问题
您希望能够从 C 程序中调用 Python 代码。
解决办法
标准库包括名为Python.h的头文件,这使得 Python 可以从 c 中调用。
它是如何工作的
当您想从 C 中调用 Python 代码时,有两个主要的函数可用:Py_Initialize()和Py_Finalize()。第一个函数启动 Python 解释器,第二个函数再次关闭它。在两次函数调用之间,您可以运行您的 Python 代码。清单 15-8 展示了一个可以执行一串 Python 代码的例子。
#include "Python.h"
void run_pycode(const char* code) {
Py_Initialize();
PyRun_SimpleString(code);
Py_Finalize();
}
Listing 15-8.Running Python Code from C
这对于较短的代码来说很好,但是如果你有一个完整的脚本,你可以从你的 C 程序中运行它,如清单 15-9 所示。
#include "Python.h"
Int main() {
Py_Initialize();
FILE* file = fopen("./my_script.py", "r");
PyRun_SimpleFile(file, "./my_script.py");
Py_Finalize();
}
Listing 15-9.Running a Python Script from C
15-4.从 Python 调用 C
问题
您希望从 Python 程序中调用外部 C 代码。
解决办法
标准的 Python API 包括帮助连接 Python 和 c 的代码,Cython 包使这种通信更加容易。
它是如何工作的
关键字cdef extern from告诉 Cython 导入 C 函数的位置。清单 15-10 显示了一个.pyx文件的例子。
cdef extern from "hello_world.c":
void print_msg()
Listing 15-10.Importing External C Code
清单 15-11 显示了相关的 C 源代码文件。
static void print_msg() {
printf("Hello World");
}
Listing 15-11.Imported C Code
这是一个比 Python 中包含的标准 API 更简单的导入 C 代码的接口。
十六、Arduino 和 RPi 秘籍
对于业余发明家来说,Raspberry PI 和 Arduino 是设计和构建自己技术的巨大资源。Raspberry Pi 已经成为事实上的单板计算机(或 SBC ), Python 已经成为 Pi 上使用的事实上的语言。在其他情况下,您可能实际上需要一个微控制器来提供与计算机的接口,或者作为一个更有限的控制单元。在这些情况下,Arduino 的所有变体都成为了标准。虽然在这一章中你将只探索 Arduino 和 Raspberry Pi,但是还有其他几个选项可供你选择。
16-1.向 Arduino 发送数据
问题
您想要向 Arduino 发送数据或指令。
解决办法
您可以使用 Python 模块pyserial通过串行连接与 Arduino 通信。
它是如何工作的
可以用pip安装pyserial,如清单 16-1 所示。
pip install --user pyserial
Listing 16-1.Installing pyserial
您需要在 Arduino 上预装一个程序,该程序可以接受指令或数据。完成后,使用串行电缆将 Arduino 连接到运行 Python 的计算机。然后你可以让你的 Python 代码向 Arduino 发送信息,比如清单 16-2 中的代码。
>>> import serial
>>> ser = serial.Serial('/dev/tty.usbserial', 9600)
>>> ser.write(b'5')
Listing 16-2.Sending Data to an Arduino
在第二行中,您需要将设备条目/dev/tty.usbserial替换为适合您的系统的位置。在write()方法中,需要前面的b将 Unicode 字符串转换成简单的字节字符串。
16-2.从 Arduino 读取数据
问题
你想从 Arduino 读取数据。
解决办法
同样,您可以使用pyserial模块从 Arduino 的串行连接中读取数据。
它是如何工作的
假设预先加载在 Arduino 上的代码期望通过串行连接发送数据,您可以使用 Python 模块pyserial来读取这些数据。清单 16-3 给出了一个例子。
>>> import serial
>>> ser = serial.Serial('/dev/tty.usbserial', 9600)
>>> data = ser.readline()
Listing 16-3.Reading Data from an Arduino
readline()方法希望在发送的数据末尾有一个新行,所以确保 Arduino 代码在每次输出数据时都发送这个新行。
16-3.写入 Raspberry Pi 的 GPIO 总线
问题
你想在 Raspberry Pi 的 GPIO 总线上写输出。
解决办法
RPi.GPIO模块提供了 Python 代码和物理 GPIO 总线之间的接口。
它是如何工作的
模块RPi.GPIO包含在 Raspbian 的包库中。清单 16-4 展示了如何在你的树莓 Pi 上安装它。
sudo apt-get install python-dev python-rpi.gpio
Listing 16-4.Installing the RPi.GPIO Module
为了使用RPi.GPIO模块,需要几个步骤来设置 GPIO 总线和处理通信。清单 16-5 显示了一个将 GPIO 引脚设置为高电平的例子。
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
GPIO.setup(1, GPIO.OUT, initial=GPIO.LOW)
GPIO.output(1, GPIO.HIGH)
Listing 16-5.Setting a GPIO Pin to High
setmode()方法设置要使用的管脚编号方案。GPIO.BOARD使用电路板上的引脚编号。setup()方法将一个特定的管脚设置为输入或输出,并可选地设置管脚的初始值。然后,您可以使用output()方法将输出发送到有问题的管脚。如您所见,GPIO 总线输出二进制数据,要么为低电平,要么为高电平。
16-4.从 Raspberry Pi 的 GPIO 总线读取
问题
你想从 Raspberry Pi 的 GPIO 总线读取输入。
解决办法
您可以使用RPi.GPIO Python 模块从 GPIO 总线读取数据。
它是如何工作的
清单 16-6 提供了如何从一个 GPIO 引脚读入数据的基本示例。
import RPi.GPIO as GPIO
GPIO.setup(1, GPIO.IN)
if GPIO.input(1):
print('Input was HIGH')
else:
print('Input was LOW')
Listing 16-6.Reading Data from the GPIO Bus
如您所见,输入以二进制高电平或低电平接收。