python 基础教程(第三版)(3)

第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。

其他方法:提高程序的速度的工具。

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'])
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容