Python Cookbook总结

14-15 章

Posted by Pelhans on August 30, 2018

包含第14章的测试、调试以及异常和第15章C语言扩展的内容。对于C语言扩展由多种方法,个人比较倾向于韦易笑老师的选取标准“不要求性能ctypes或者cffi,需要性能cython或者手写module,其它都是邪路。最好的方法是全部写成ctypes,或者cffi,送上线跑,有空再把最慢的一两个接口换成cython”

第十四章 并发

14.1 在单元测试中为对象打补丁

unuttest.mock.path() 函数的用法较多,可以当做装饰器、上下文管理器或者单独使用。如:

from unuttest.mock import patch
import example

# 做装饰器用
@patch('example_func')
def test1(x, mock_func):
    example.func(x)
    mock_func.assert_called_with(x)

# 做上下文管理器用
with patch('example.func') as mock_func:
    example.func(x)
    mock_func.assert_called_with(x)

# 用来手动打补丁
p = patch('example.func')
mock_func = p.start()
example.func(x)
mock_func.assert_called_with(x)
p.stop()

patch()接受一个已有对象的完全限定名称并将其替换为一个新值,在装饰器函数或者上下文管理器结束执行后会将对象恢复为原始值。默认情况下,对象会被替换为 MagicMock实例。

14.2 在单元测试中检测异常情况

使用 assertRaise() 方法。

import unittest

def parse_int(s):
    return int(s)

class TestConversion(unittest.TestCase):
    def test_bad_int(self):
        self.assertRaise(ValueError, parse_int, 'N/A')

14.3 跳过测试或者预计测试结果为失败

unittest模块中由一些装饰器渴作用域所选的测试方法上,以此控制它们的处理行为。

import unittest
import os
import platform

class Tests(unittest.TestCase):
    def test_0(self):
        self.assertTrue(True)

    @unittest.skip('skipped test')
    def test_1(self):
        self.fail('should have failed!')

    @unittest.skipIf(os.name=='posix', 'Not supported on Unix')
    def test_2(self):
        import winreg

    @unittest.skipUnless(paltform.system() == 'Drawin', 'Max specific test')
    def test_3(self):
        self.assertTrue(True)

    @unittest.expectedFailure
    def test_4(self):
        self.assertEqual(2+2, 5)

if __name__ == '__main__':
    unittest.main()

14.4 创建自定义的异常

创建自定义的异常是非常简单的,只要将它们定义成继承自Exception 的类即可(也可以继承自其他已有的异常类型,如果这么做更有道理的话)。自定义的类应该总是继承自内建的Exception类,或者继承自一些本地定义的基类,而这个基类本身又是继承自Exception 的。虽然所有的异常也都继承自 BaseException,但不应该将它作为基类来产生新的异常。BaseException 是预留给系统退出异常的,比如 KeyboardInterrupt。因此捕获这些异常并不适用于它们本来的用途。

class NetworkError(Exception):
    pass

class HostnameError(NetworkError):
    pass

# when used
try:
    msg = s.s.recv()
except HostnameError as e:
    ...

如果打算定义一个新的异常并且改写 Exception 的 init()方法,请确保总是用所有传递过来的参数调用 Exception.init()。

class CustomError(Exception):
    def __init__(self, message, status):
        super().__init__(message, status)
        self.message = message
        self.status = status

14.5 通过引发异常来响应另一个异常

要将异常串联起来,可以用 raise from 语句来代替普通的 raise。还可以通过查看异常对象的 __cause__属性来跟踪所希望的异常链。

def example():
    try:
        int('N/A')
    except ValueError as e:
        raise RuntimeError('A PARSING ERROR OCCURED') from e...

14.6 让你的程序运行的更快

下面列出一些常见简单的优化策略:

  • 有选择的消除属性访问:每次使用句点操作符(.)来访问属性时都会带来开销。在底层,这会触发调用特殊方法,比如 getattribute() 和 getattr(),而调用这些方法常常会导致字典查询操作。
  • 理解变量所处的位置:通常来说,访问局部变量要比全局变量要快。对于需要频繁访问的名称,想要提高运行速度,可以通过让这些名称尽可能成为局部变量来达成。
  • 避免不必要的抽象:任何时候当使用额外的处理层比如装饰器、属性或者描述符来包装代码时,代码的速度就会变慢。
  • 使用内建的容器:内建的数据类型处理速度一般要比自己写的快的多。
  • 避免产生不必要的数据结构或者拷贝操作

第15章 C语言扩展

15.1 利用Ctypes来访问C代码

对于C语言编写的小程序,使用Python标准库中的ctypes模块来访问通常是非常容易的。要使用ctypes,必须首先确保想要访问的C代码已经被编译为与Python解释器兼容(即采用同样的体系结构,字长,编译器等)的共享库了。

import ctypes
import os

_file = 'libsample.so'
_path = os.path.join(*(os.path.split(__file__)[:-1] + (_file,)))
_mod = ctypes.cdll.LoadLibrary(_path)

# int gcd(int, int)
gcd = _mod.gcd
gcd.argtypes = (ctypes.c_int, ctypes.c_int)
gcd.restype = ctypes.c_int

# int divide(int, int, int *)
_divide = _mod.divide
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
_divide.restype = ctypes.c_int

def divide(x, y):
    rem = ctypes.c_int()
    quot = _divide(x, y, rem)
    return quot, rem.value

# in c code
int gcd(int x, int y){
    int g = y;
    while (x > 0){
        g = x;
        x = y % x;
        y = g;
    }
    return g;
}

# Divide two numbers
int divide(int a, int b, int *remainder){
    int quot = a / b;
    *remainder = a % b;
    return quot
}

15.2 编写简单的C语言扩展模块

不依赖任何其他工具直接使用 Python 的扩展 API编写一个简单的C 语言扩展模块。对于简单的 C代码,手工创建一个简单的扩展模块是很简单直接的。

第一步,确保自己的C代码有一个合适的头文件。通常这个头文件会对应于一个单独编译好的库。

/* sample.h */
#include <math.h>
extern int gcd(int, int)
extern int divide(int a, int b, int *remainder)

typeder struct Point(
    double x, y;
    )Point;

下面是一个 C语言扩展模块的样例,用来说明编写扩展函数的基础:

#include "Python.h"
#include "sample.h"

/* int gcd(int, int) */
// PyObject 是一个C数据类型,表示任意的python 对象
static Pyobject *py_gcd(PyObject *self, PyObject *aegs){
    int x, y, retult;
    //PyArg_ParseTuple()用来将值从python转换为C语言中的表示
    if (!PyArg_ParseTuple(args, "ii", &x, &y)){
        return NULL;
    }
    retult = gcd(x, y);
    // 函数 Py_BuildValue()用来从C数据类型创建出对应的python对象
    return Py_BuildValue("i", result);
}
...

为了构建扩展模块,需要创建一个 setup.py 文件:

```python
# setup.py
setup(name='sample',
      ext_modules=[
          Extension('sample'
                    '[pysample.c]',
                    include_dirs = ['/some/dir'],
                    define_macros = [('FOO', '1')],
                    undef_macros = ['BAR'],
                    ;ibrary_dirs = ['/usr/local/lib'],
                    libraries = ['sample']
              )
        ]
     )
...

现在要构建出目标库,只需要用 python3 buildlib.py build_ext –inplace即可。这样就创建了一个名为 sample.so 的共享库。编译结束后,应该就可以开始将其当做一个Python模块来导入了。

import sample
sample.gcd(35, 35)
out: 7

15.3 用Swig来包装C代码

Swig 可以解析C头文件并自动创建出扩展代码来。要使用这个工具,首先要有一个C头文件。而后下一步就是编写一个 Swig “接口”文件。根据约定,这些接口文件都以i 作为后缀。

// sample.i -Swig interface
%moudle sample
%{
#include "sample.h"
}

/* Customizations */
%extend Point{
    /* Constructor for Point objects */
    Point(double x, double y){
        Point *p = (Point *) malloc(sizeof(Point));
        p->x = x;
        p->y = y;
        return p;

    };
};

/* Map int *remainder as an output argument */
%include typemaps.i
%apply int *OUTPUT {int * remainder};

/* Map the argument pattern (double *a, int n) to arrats */
%typemap(in) (double *a, int n)(Py_buffer view){
    view.obj = NULL;
    if (PyObject_GetBuffer($input, &view, PyBUF_ANY_CONTIGUOUS | PyBUF_FORMAT) == -1){
        SWIG_fail;
    }
    if (strcmp(view.format, "d") != 0){
        PyErr_SetString(PyExc_TypeError, "Expected an array of doubles");
        SWIG_fail;
    }
    $1 = (double *) view.buf;
    $2 = view.len / sizeof(double);
}

%typemap(freearg)(double *a, int n){
    if (view$argnum.obj){
        PyBuffer_Release(&view$argum);
    }
}

/* C declarations to be include in the extension module, you can copy the .h to here directly */

extern int gcd(int, int);
...

一旦写好了这个接口文件, Swig就可以作为命令行工具在终端中调用了:

swig -python -py3 sample.i

swig会产生两个文件:sample_wrap.c 和 sample.py。而后编写 setup.py.py 并进行编译即可正常导入。

15.4 用 Cpython 来包装 C代码

从某种程度上来看,用Cpython创建一个扩展模块和手动编写扩展模块有些类似,它们都需要创建一组包装函数。现在假设C代码已经被编译为C库。

首先我们创建一个名为 csample.pxd 文件。

# csample.pxd
#
# Declarations of "external" C functions and structures

# declara the head file we need
cdef extern from "sample.h"
   # next code copy from heah.h
   int gcd(int, int)
   bint in_mandel(double, double, int)
   int divide(int, int, int *)
   double avg(double *, int) nogil
   ...

这个文件在Cpython中的目的和作用就相当于一个C头文件。接下来创建一个名为sample.pyx的文件。这个文件定义包装函数,作为Python解释器到csample.pxd文件中定义的底层C代码之间的桥梁。

# sample.pyx

# Import the low-level C declarations
cimport csample

from cpython.pycapsule cimport *
from libc.stdlib cimport malloc, free

def gcd(unsigned int x, unsigned int y):
    return csample.gcd(x, y)

# other function 
...

要构建出扩展模块,还需要创建一个setup.py文件。如:

from disutils.core import setup
from disutils.extension import Extension
from Cpython.Distuils import build_ext

ex_modules = [
    Extension('sample',
              ['sample.pys'],
              libraries=['sample'],
              librarie_dirs=['x']
        )
]

setup(
    name = 'Sample extension module',
    cmdclass = {'build_ext' : build_ext},
    ext_modules = ext_modules
    )

最终使用 python3 setup.py build_ext --inplace构建出名为 sample.so 的扩展模块。