第16 章 测试基础
编辑和运行程序
16.1 先测试再编码
避免代码在开发途中被淘汰,必须能够应对变化并具备一定的灵活性,因此为程序的各个部分编写测试至关重要(这称为单元测试),“测试一点点,再编写一点点代码”。
测试在先,编码在后。这也称为测试驱动的编程。
16.1.1 准确的需求说明
需求:假设你要编写一个模块,其中只包含一个根据矩形的宽度和高度计算面积的函数。
简单的程序测试:
from area import rect_area
height = 3
width = 4
correct_answer = 12
answer = rect_area(height, width)
if answer == correct_answer:
print('Test passed')
else:
print('Test failed')
16.1.2 做好应对变化的准备
代码覆盖率工具:Python自带的程序trace.py。
16.1.3 测试四步曲
(1)确定需要实现的新功能。可将其记录下来,再为之编写一个测试。
(2)编写实现功能的测试框架代码,让程序能够运行(不存在语法错误之类的问题),但测试依然无法通过。测试是失败很重要。如果测试有错误,导致在任何情况下都能成功,那么实际上什么都没有测试。不断重复这个过程:确定测试失败后,再试图让它成功。
(3)编写让测试刚好能够通过的代码。在这个阶段,无需完全实现所需的功能,而只要让测试能够通过即可。这样,在整个开发阶段,都能够让所有的测试通过(首次运行测试时除外),即便是刚着手实现的功能时亦如此。
(4)改进(重构)代码以全面而准确地实现所需的功能,同时确保测试依然能够成功。
提交代码时,必须确保它们处于健康状态,即没有任何测试是失败的。测试驱动编程倡导者都是这么说的。
16.2 测试工具
- unittest:一个通用的测试框架
- doctest:一个更简单的模块,是为检查文档而设计的,但也非常适合用来编写单元测试。
16.2.1 doctest
def square(x):
'''
计算平方并返回结果
>>>square(2)
4
>>>square(3)
9
'''
return x * x
if __name__=='__main__':
import doctest,my_math
doctest.testmod(my_math)
16.2.2 unittest
import unittest,my_math
class ProductTestCase(unittest.TestCase):
def test_integers(self):
for x in range(-10, 10):
for y in range(-10, 10):
p = my_math.product(x, y)
self.assertEqual(p, x*y, 'Integer multiplication failed')
def test_floats(self):
for x in range(-10, 10):
for y in range(-10, 10):
x = x / 10
y = y / 10
p = my_math.product(x, y)
self.assertEqual(p, x * y, 'Float multiplication failed')
if __name__ == '__main__': unittest.main()
函数unittest.main负责替你运行测试:实例化所有的TestCase子类,并运行所有名称以test打头的方法。
# def product(x,y):
# pass
# def product(x,y):
# return x * y
def product(x,y):
if x == 7 and y == 9:
return 'An insidious bug has surfaced!'
else:
return x * y
16.3 超越单元测试
源代码检查:是一种发现代码中常见错误或问题的方式(有点像静态类型语言中边疫情的作用,但做的事情更多)。
性能分析:是搞清楚程序的运行速度到底有多快。规则:使其管用,使其更好,使其更快。
16.3.1 使用PyChecker和PyLint 检查源代码
PyChecker可以找出诸如给函数提供的参数不对等错误
要使用PyChecker来检查文件,可运行这个脚本并将文件名作为参数
pychecker file1.py file2.py ...
使用PyLint 检查文件时,需要将模块(或包)名作为参数:
pylint module
import unittest, my_math
from subprocess import Popen,PIPE
class ProductTestCase(unittest.TestCase):
def test_integers(self):
for x in range(-10, 10):
for y in range(-10, 10):
p = my_math.product(x, y)
self.assertEqual(p, x*y, 'Integer multiplication failed')
def test_floats(self):
for x in range(-10, 10):
for y in range(-10, 10):
x = x / 10
y = y / 10
p = my_math.product(x, y)
self.assertEqual(p, x * y, 'Float multiplication failed')
def test_with_PyChecker(self):
cmd = 'PyChecker', '-Q', my_math.__file__.rstrip('c')
pychecker = Popen(cmd, stdout=PIPE, stderr=PIPE)
self.assertEqual(pychecker.stdout.read(), '')
def test_with_PyLint(self):
cmd = 'pylint', '-rn', 'my_math'
pylint = Popen(cmd, stdout=PIPE, stderr=PIPE)
self.assertEqual(pylint.stdout.read(), '')
if __name__ == '__main__': unittest.main()
调用检查器叫本事,指定了一些命令行开关,以免无关的输出干扰测试。对于pychecker,指定了开关-Q(quiet,意为静默);对于pylint,指定了开关-rn(其中n表示no)以关闭 报告,这意味着将只显示警告和错误。
"""
一个简单的数学模块
"""
__revision__ = '0.1'
def product(factor1, factor2 ):
'The product of two numbers'
return factor1 * factor2
16.3.2 性能分析
标准库包含一个卓越的性能分析模块profile,还有一个速度更快的C语言版本,名为CProfile。
import cProfile
from my_math import product
cProfile.run('product(1,2)')
import pstats
p = pstats.Stats('my_math.profile')
16.4 小结
- 测试驱动编程:大致而言,测试驱动编程意味着先测试再编码。有了测试,你就能信心慢慢地修改代码,这让开发和维护工作更加灵活。
- 模块doctest和unittest:需要在Python中进行单元测试时,这些工具必不可少。模块doctest设计用于检查文档字符串中的示例,但也轻松地使用它来设计测试套件。为让测试套件更灵活、机构化程度更高,框架unittest很有帮助。
- PyChecker和PyLint:这两个工具查看源代码并指出潜在(和实际)的问题。它们检查代码的方方面面——从变量名太短到永远不会执行的代码段。你只需要编写少量的代码,就可能它们加入测试套件,从而确保所有修改和重构都遵循了你采用的编码标准。
- 性能分析:如果你很在乎速度,并想对程序进行优化(仅当绝对必要时才这样做),应首先进行性能分析:使用模块profile或cProfile来找出代码中的瓶颈。
16.4.1 新函数
函数 描述
doctest.testmod(module) 检查文档字符串中的示例(还接受很多其他的参数)
unittest.main() 运行当前模块中的单元测试
profile.run(stmt[,filename]) 执行语句并对其进行性能分析;可将分析结果保存到参数filename指定的文件中
第17章 扩展Python
17.1 鱼和熊掌兼得
(1)使用Python开发原型
(2)对程序进行性能分析以找出瓶颈
(3)使用C(或者C++、C#、Java、Fortran等)扩展重写瓶颈部分。
扩展Python的经典C语言实现(为此可手工编写所有的代码,也可使用工具SWIG),以及如何扩展其他两种实现:Jython和IronPython。
17.2 简单易行的方式:Jython 和 IronPython
在Jython 中,可直接访问Java标准库;而在IronPython中,可直接访问C#标准库。
一个简单的Java类(JythonTest.java)
public class JythonTest{
public void greeting(){
System.out.println("Hello, world!");
}
}
可使用Java编译器(如javac)来编译这个类。
$ javac JythonTest.java
启动Jython
$ CLASSPATH=JythonTest.class jython
一个简单的C#类(IronPythonTest.cs)
using System;
namespace FePyTest {
public class IronPythonTest {
public void greeting() {
Console.writeLine("Hello, world!");
}
}
}
编译
csc.exe /t:library IronPythonTest.cs
要在IronPython中使用这个类,一种方法是将其编译为动态链接库(DLL),并根据需要修改相关的环境变量(如PATH),然后使用
import clr
clr.AddReferenceToFile("IronPythonTest.dll")
import FePyTest
f = FePyTest.IronPythonTest()
f.greeting()
17.3 编写 C 语言扩展
使用C语言编写Python扩展时,必须遵循严格的API。
其他方法:提高程序的速度的工具。
- Cython(http://cython.org):一个Python编译器!它还提供了扩展的Cython语言,该语言基于Greg Ewing开发的项目Pyrex,使用类似于Python的语法添加类型声明和定义C类型。
- PyPy(http://pypy.org):核心是RPython——一种受限的Python方言。RPython擅长自动类型推断等,可转换为静态语言、机器码和其他动态语言(如JavaScript)。
- Weave(http://scipy.org):在代码中以字符串的方式直接包含C或C++代码,并无缝地编译和执行这些代码。
- NumPy(http://numpy.org):让你能使用数字数组。
- ctypes(https://docs.python.org/library/ctypes.html):让你能够导入既有(共享)的C语言库。
- subprocess(https://docs.python.org/3/library/subprocess.htm):让你在Python中运行外部程序,并通过命令行参数以及标准输入、输出和错误流与它们通信。
- PyCXX(http://cxx.sourceforge.net):帮助使用C++编写Python扩展的工具。
- SIP(http://www.riverbankcomputing.co.uk/software/sip):包含一个代码生成器和一个Python模块。他想swig那样使用规范文件。
- Boost.Python(http://www.boost.org/libs/python/doc):让python和C++无缝地互操作,解决引用计数和在C++中操作Python对象。
17.3.1 SWIG
http://www.swig.org
指的是简单包装器和接口生成器(simple wrapper and interface generator),是一个适用于多种语言的工具。
安装步骤:
- 官网http://www.swig.org下载SWIG.
- 很多UNIX/Linux 发布版都包含SWIG;很多包管理器都能够让你直接安装它。
- 有用于Windows的二进制安装程序。
- 自己编译源代码也很简单,只需调用configure和make install即可。
1、用法
(1)为代码编写一个接口文件。这很像C语言头文件。
(2)对接口文件运行SWIG,以自动生成一些额外的C语言代码(包装器代码)。
(3)将原来的C语言代码和生成的包装器代码一起编译,以生成共享库。
2、回文
回文(palindrome;如 I prefer pi)是忽略空格、标点等后正着读和反着读一样的句子。
一个简单的检测回文的C语言函数(palindrome.c)
#include <string.h>
int is_palindrome(char *text){
int i, n=strlen(text);
for(i = 0; I <= n/2; ++i){
if (text[i] != text[n-i-1]) return 0;
}
return 1;
}
检测回文的Python函数
def is_palindrome(text):
n= len(text)
for i in range(len(text) // 2):
if text[i] != text[n-i-1]:
return False
return True
3、接口文件
在接口文件中,你只是声明要导出的函数(和变量),就像在头文件中一样。另外,在接口文件的开头,有一个由%{和%}界定的部分,可在其中指定要包含的头文件(这里为string.h)。在这个部分的前面,还有一个%module声明,用于指定模块名。
回文检测库的接口(palindrome.i)
%module palindrome
%{
#include<string.h>
%}
extern int is_palindrome(char *text);
4、运行SWIG
使用开关-python就可让SWIG对C语言代码进行包装,以便能够在Python中使用。另一个可能很有用的开关是-c++,可用于包装C++库。运行SWIG时,需要将接口文件(也可以是头文件)作为参数
$ swig -python palindrom.i
5、编译、连接和使用
正确的编译需要知道Python源代码(至少是头文件pyconfig.h和Python.h)的存储位置。根据选择的C语言编译器,使用正确的开关将代码编译成共享库。
在Solaris系统中使用编译器cc示例:
$ cc -c palindrome.c
$ cc -I$PYTHON_HOME -I$PYTHON_HOME/Include -c palindrome_wrap.c
$ cc -G palindrome.o palindrome_wrap.o -o _palindrome.so
在Linux中使用编译器gcc的示例:
$ gcc -c palindrome.c
$ gcc -I$PYTHON_HOME -I$PYTHON_HOME/Include -c palindrome_wrap.c
$ gcc -shared palindrome.o palindrome_wrap.o -o _palindrome.so
可能所有必要的包含文件都在一个地方,如/usr/include/python3.5
$ gcc -c palindrome.c
$ gcc -I/usr/include/python3.5 -c palindrome_wrap.c
$ gcc -shared palindrome.o palindrome_wrap.o -o _palindrome.so
在Windows中
$ gcc -shared palindrome.o palindrome_wrap.o C:/Python25/libs/libpython25.a -o _palindrome.dll
在macOS中,(如果使用的是Python官方安装,PYTHON_HOME将为/Library/Frameworks/Python.framework/Versions/Current):
$ gcc -dynamic -I$PYTHON_HOME/Include/python3.5 -c palindrome.c
$ gcc -dynamic -I$PYTHON_HOME/Include/python3.5 -c palindrome_wrap.c
$ gcc -dynamiclib palindrome_wrap.o palindrome.o -o _palindrome.so -wl, -undefined, dynamic_lookup
得到文件_palindrome.so,它就是共享库,可直接导入到Python中(条件是他位于PYTHONPAH包含的目录中):
>>>import _palindrome
>>>dir(_palindrome)
['__doc__', '__file__', '__name__', 'is_palindrome']
>>> _palindrome.is_palindrome('ipreferpi')
1
>>> _palindrome.is_palindrome('notlob')
0
较新的SWIG版本还会生成一些Python报装代码,它导入模块_palindrome并执行一些 检查工作
>>>import palindrome
>>>from palindrome import is_palindrome
>>>if is_palindrome('abba'):
... print('wow -- that never occurred to me ...')
...
wow -- that never occurred to me ...
6、穿越编译器“魔法森林”的捷径
如果自动化编译过程【如使用生成文件(makefile)】,就需要进行配置:指定Python安装位置、要使用的编译器和选项等。
17.3.2 手工编写扩展
1、应用计数
在python中,内存管理是自动完成的:你只管创建对象,当你不再使用时它们就会消失。在C语言中,你必须显式地释放不再使用的对象(更准确地说是内存块),否则程序占用的内存将越来越多,这称为内存泄漏(memory leak)。
要点:
- 对象不归你所有,但指向它的引用归你所有。一个对象的引用计数时指向它的引用的数量。
- 对于归你所有的引用,你必须负责在不在需要它时调用Py_DECREF.
- 对于你暂时借用的引用,不应在借用完后调用Py_DECREF,因为这是引用所有者的职责。
- 可通过调用Py_INCREF将借来的引用变成自己的。这将创建一个新引用,而借来的引用依然归原来的所有者所有。
- 通过参数收到对象后,要转移所有权(如将其存储起来)还是仅仅借用由你来决定,但应清楚地说明。如果函数将在Python中调用,完全可以只借用,因为对象在整个函数调用期间都存在。然而,如果函数将在C语言中调用,就无法保证对象在函数调用期间都存在,因此可能应该创建自己的引用,并在使用完毕后将其释放。
再谈垃圾收集
引用计数是一种垃圾收集方式,其中的术语“垃圾”指的是程序不再使用的对象。python还使用一种更尖端的算法来检测循环垃圾,即两个对象相互引用对方(导致它们的引用计数不为0),但没有其他的对象引用它们。
2、扩展框架
必须先包含头文件Python.h,再包含其他标准头文件。这是因为在有些平台上,Python.h可能会做些重新定义,而其他头文件需要用到这些新定义。
#include<Python.h>
static PyObject *somename(PyObject *self, PyObject *args) {
PyObject *result;
/* 在这里执行操作,包括分配result*/
Py_INCREF(result); /* 仅当需要时才这样做!*/
return result;
}
3、回文
#include<Python.h>
static PyObject *is_palindrome(PyObject *self, PyObject *args){
int i,n;
const char *text;
int result;
/* "s"表示一个字符串:*/
if (!PyArg_ParseTuple(args, "s", &text)){
return NULL;
}
/* 与旧版的代码大致相同:*/
n=strlen(text);
result = 1;
for (i = 0; i <= n/2; ++i){
if (text[i] != text[n-i-1]){
result = o;
break;
}
}
/* "i"表示一个整数:*/
return Py_BuildValue("i", result);
}
/* 方法/函数列表:*/
static PyMethodDef PalindromeMethods[] = {
/* 名称、函数、参数类型、文档字符串 */
{"is_palindrome", is_palindrome, METH_VARARGS, "Detect palindromes"},
/* 列表结束标志:*/
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef palindrome =
{
PyModuleDef_HEAD_INIT,
"palindrome",/* 模块名 */
"", /* 文档字符串 */
-1, /* 存储在全局变量中的信号状态 */
PalindromeNethods
};
/* 初始化模块的函数: */
PyMODINIT_FUNC PyInit_palindrome(void)
{
return PyModulle_Create(&palindrome);
}
17.4 小结
- 扩展理念:Python扩展的主要用途有两个——利用既有(遗留)代码和提高瓶颈部分的速度。从头开始编写代码时,请尝试使用Python建立原型,找出其中的瓶颈并在需要时使用扩展来替换它们。预先将潜在的瓶颈封装起来大有裨益。
- Jython和IronPython:对这些Python实现进行扩展很容易,使用底层语言(对于Jython,为Java;对于IronPython,为C#和其他.NET语言)以库的方式实现扩展后,就可在Python中使用它们了。
- 扩展方法:有很多用于扩展代码或提高其速度的工具,有的让你更轻松地在Python程序中嵌入C语言代码,有的可提高数字数组操作等常见运算的速度,有的 可提高Python本身的速度。这样的工具包括SWIG、Cython 、Weave、NumPy、ctypes和subprocess。
- SWIG:SWIG是一款自动为C语言库生成包装代码的工具。包装代码自动处理Python CAPI,使你不必自己去做这样的工作。使用SWIG是最简单、最流行的扩展Python的方式之一。
- 使用Python/C API:可手工编写可作为共享库直接导入到Python中的C语言代码。为此,必须遵循Python/C API:对于每个函数,你都需要负责完成引用计数、提取参数以及创建返回值等工作;另外,还需编写将C语言库转换为模块的代码,包括列出模块中的函数以及创建模块初始化函数。
17.4.1
函数 描述
Py_INCREF(obj) 将obj的引用计数加1
Py_DECREF(obj) 将obj的引用计数减1
PyArg_ParseTuple(args, fmt, ...) 提取位置参数
PyArg_ParseTupleAndKeywords(args, kws,fmt,kwlist) 提取位置参数和关键字参数
PyBuildValue(fmt, value) 根据C语言值创建PyObject
第18章 程序打包
18.1 Setuptools基础
from setuptools import setup
setup(name = 'Hello',
version='1.0',
description='A simple example',
author='Magnus Lie Hetland',
Py_modules=['hello'])