python-cookbook
  • Introduction
  • 第 1 章 数据结构和算法
    • 1.1 解压序列赋值给多个变量
    • 1.2 解压可迭代对象赋值给多个变量
    • 1.3 保留最后N个元素
    • 1.4 查找最大或最小的N个元素
    • 1.5 实现一个优先级队列
    • 1.6 字典中的键映射多个值
    • 1.7 字典排序
    • 1.8 字典的运算
    • 1.9 查找两字典的相同点
    • 1.10 删除序列相同元素并保持顺序
    • 1.11 命名切片
    • 1.12 序列中出现次数最多的元素
    • 1.13 通过某个关键字排序一个字典列表
    • 1.14 排序不支持原生比较的对象
    • 1.15 通过某个字段将记录分组
    • 1.16 过滤序列元素
    • 1.17 从字典中提取子集
    • 1.18 映射名称到序列元素
    • 1.19 转换并同时计算数据
    • 1.20 合并多个字典或映射
  • 第 2 章 字符串和文本
    • 2.1 使用多个界定符分割字符串
    • 2.2 字符串开头或结尾匹配
    • 2.3 用Shell通配符匹配字符串
    • 2.4 字符串匹配和搜索
    • 2.5 字符串搜索和替换
    • 2.6 字符串忽略大小写的搜索替换
    • 2.7 最短匹配模式
    • 2.8 多行匹配模式
    • 2.9 将Unicode文本标准化
    • 2.10 在正则式中使用Unicode
    • 2.11 删除字符串中不需要的字符
    • 2.12 审查清理文本字符串
    • 2.13 字符串对齐
    • 2.14 合并拼接字符串
    • 2.15 字符串中插入变量
    • 2.16 以指定列宽格式化字符串
    • 2.17 在字符串中处理html和xml
    • 2.18 字符串令牌解析
    • 2.19 实现一个简单的递归下降分析器
    • 2.20 字节字符串上的字符串操作
  • 第 3 章 数字日期和时间
    • 3.1 数字的四舍五入
    • 3.2 执行精确的浮点数运算
    • 3.3 数字的格式化输出
    • 3.4 二八十六进制整数
    • 3.5 字节到大整数的打包与解包
    • 3.6 复数的数学运算
    • 3.7 无穷大与NaN
    • 3.8 分数运算
    • 3.9 大型数组运算
    • 3.10 矩阵与线性代数运算
    • 3.11 随机选择
    • 3.12 基本的日期与时间转换
    • 3.13 计算最后一个周五的日期
    • 3.14 计算当前月份的日期范围
    • 3.15 字符串转换为日期
    • 3.16 结合时区的日期操作
  • 第 4 章 迭代器与生成器
    • 4.1 手动遍历迭代器
    • 4.2 代理迭代
    • 4.3 使用生成器创建新的迭代模式
    • 4.4 实现迭代器协议
    • 4.5 反向迭代
    • 4.6 带有外部状态的生成器函数
    • 4.7 迭代器切片
    • 4.8 跳过可迭代对象的开始部分
    • 4.9 排列组合的迭代
    • 4.10 序列上索引值迭代
    • 4.11 同时迭代多个序列
    • 4.12 不同集合上元素的迭代
    • 4.13 创建数据处理管道
    • 4.14 展开嵌套的序列
    • 4.15 顺序迭代合并后的排序迭代对象
    • 4.16 迭代器代替while无限循环
  • 第 5 章 文件与 IO
    • 5.1 读写文本数据
    • 5.2 打印输出至文件中
    • 5.3 使用其他分隔符或行终止符打印
    • 5.4 读写字节数据
    • 5.5 文件不存在才能写入
    • 5.6 字符串的I-O操作
    • 5.7 读写压缩文件
    • 5.8 固定大小记录的文件迭代
    • 5.9 读取二进制数据到可变缓冲区中
    • 5.10 内存映射的二进制文件
    • 5.11 文件路径名的操作
    • 5.12 测试文件是否存在
    • 5.13 获取文件夹中的文件列表
    • 5.14 忽略文件名编码
    • 5.15 打印不合法的文件名
    • 5.16 增加或改变已打开文件的编码
    • 5.17 将字节写入文本文件
    • 5.18 将文件描述符包装成文件对象
    • 5.19 创建临时文件和文件夹
    • 5.20 与串行端口的数据通信
    • 5.21 序列化Python对象
  • 第 6 章 数据编码和处理
    • 6.1 读写CSV数据
    • 6.2 读写JSON数据
    • 6.3 解析简单的XML数据
    • 6.4 增量式解析大型XML文件
    • 6.5 将字典转换为XML
    • 6.6 解析和修改XML
    • 6.7 利用命名空间解析XML文档
    • 6.8 与关系型数据库的交互
    • 6.9 编码和解码十六进制数
    • 6.10 编码解码Base64数据
    • 6.11 读写二进制数组数据
    • 6.12 读取嵌套和可变长二进制数据
    • 6.13 数据的累加与统计操作
  • 第 7 章 函数
    • 7.1 可接受任意数量参数的函数
    • 7.2 只接受关键字参数的函数
    • 7.3 给函数参数增加元信息
    • 7.4 返回多个值的函数
    • 7.5 定义有默认参数的函数
    • 7.6 定义匿名或内联函数
    • 7.7 匿名函数捕获变量值
    • 7.8 减少可调用对象的参数个数
    • 7.9 将单方法的类转换为函数
    • 7.10 带额外状态信息的回调函数
    • 7.11 内联回调函数
    • 7.12 访问闭包中定义的变量
  • 第 8 章 类与对象
    • 8.1 改变对象的字符串显示
    • 8.2 自定义字符串的格式化
    • 8.3 让对象支持上下文管理协议
    • 8.4 创建大量对象时节省内存方法
    • 8.5 在类中封装属性名
    • 8.6 创建可管理的属性
    • 8.7 调用父类方法
    • 8.8 子类中扩展property
    • 8.9 创建新的类或实例属性
    • 8.10 使用延迟计算属性
    • 8.11 简化数据结构的初始化
    • 8.12 定义接口或者抽象基类
    • 8.13 实现数据模型的类型约束
    • 8.14 实现自定义容器
    • 8.15 属性的代理访问
    • 8.16 在类中定义多个构造器
    • 8.17 创建不调用init方法的实例
    • 8.18 利用Mixins扩展类功能
    • 8.19 实现状态对象或者状态机
    • 8.20 通过字符串调用对象方法
    • 8.21 实现访问者模式
    • 8.22 不用递归实现访问者模式
    • 8.23 循环引用数据结构的内存管理
    • 8.24 让类支持比较操作
    • 8.25 创建缓存实例
  • 第 9 章 元编程
    • 9.1 在函数上添加包装器
    • 9.2 创建装饰器时保留函数元信息
    • 9.3 解除一个装饰器
    • 9.4 定义一个带参数的装饰器
    • 9.5 可自定义属性的装饰器
    • 9.6 带可选参数的装饰器
    • 9.7 利用装饰器强制函数上的类型检查
    • 9.8 将装饰器定义为类的一部分
    • 9.9 将装饰器定义为类
    • 9.10 为类和静态方法提供装饰器
    • 9.11 装饰器为被包装函数增加参数
    • 9.12 使用装饰器扩充类的功能
    • 9.13 使用元类控制实例的创建
    • 9.14 捕获类的属性定义顺序
    • 9.15 定义有可选参数的元类
    • 9.16 args和*kwargs的强制参数签名
    • 9.17 在类上强制使用编程规约
    • 9.18 以编程方式定义类
    • 9.19 在定义的时候初始化类的成员
    • 9.20 利用函数注解实现方法重载
    • 9.21 避免重复的属性方法
    • 9.22 定义上下文管理器的简单方法
    • 9.23 在局部变量域中执行代码
    • 9.24 解析与分析Python源码
    • 9.25 拆解Python字节码
  • 第 10 章 模块与包
    • 10.1 构建一个模块的层级包
    • 10.2 控制模块被全部导入的内容
    • 10.3 使用相对路径名导入包中子模块
    • 10.4 将模块分割成多个文件
    • 10.5 利用命名空间导入目录分散的代码
    • 10.6 重新加载模块
    • 10.7 运行目录或压缩文件
    • 10.8 读取位于包中的数据文件
    • 10.9 将文件夹加入到sys.path
    • 10.10 通过字符串名导入模块
    • 10.11 通过钩子远程加载模块
    • 10.12 导入模块的同时修改模块
    • 10.13 安装私有的包
    • 10.14 创建新的Python环境
    • 10.15 分发包
  • 第 11 章 网络与 Web 编程
    • 11.1 作为客户端与HTTP服务交互
    • 11.2 创建TCP服务器
    • 11.3 创建UDP服务器
    • 11.4 通过CIDR地址生成对应的IP地址集
    • 11.5 创建一个简单的REST接口
    • 11.6 通过XML-RPC实现简单的远程调用
    • 11.7 在不同的Python解释器之间交互
    • 11.8 实现远程方法调用
    • 11.9 简单的客户端认证
    • 11.10 在网络服务中加入SSL
    • 11.11 进程间传递Socket文件描述符
    • 11.12 理解事件驱动的IO
    • 11.13 发送与接收大型数组
  • 第 12 章 并发编程
    • 12.1 启动与停止线程
    • 12.2 判断线程是否已经启动
    • 12.3 线程间通信
    • 12.4 给关键部分加锁
    • 12.5 防止死锁的加锁机制
    • 12.6 保存线程的状态信息
    • 12.7 创建一个线程池
    • 12.8 简单的并行编程
    • 12.9 Python的全局锁问题
    • 12.10 定义一个Actor任务
    • 12.11 实现消息发布-订阅模型
    • 12.12 使用生成器代替线程
    • 12.13 多个线程队列轮询
    • 12.14 在Unix系统上面启动守护进程
  • 第 13 章 脚本编程与系统管理
    • 13.1 通过重定向-管道-文件接受输入
    • 13.2 终止程序并给出错误信息
    • 13.3 解析命令行选项
    • 13.4 运行时弹出密码输入提示
    • 13.5 获取终端的大小
    • 13.6 执行外部命令并获取它的输出
    • 13.7 复制或者移动文件和目录
    • 13.8 创建和解压归档文件
    • 13.9 通过文件名查找文件
    • 13.10 读取配置文件
    • 13.11 给简单脚本增加日志功能
    • 13.12 给函数库增加日志功能
    • 13.13 实现一个计时器
    • 13.14 限制内存和CPU的使用量
    • 13.15 启动一个WEB浏览器
  • 第 14 章 测试、调试和异常
    • 14.1 测试stdout输出
    • 14.2 在单元测试中给对象打补丁
    • 14.3 在单元测试中测试异常情况
    • 14.4 将测试输出用日志记录到文件中
    • 14.5 忽略或期望测试失败
    • 14.6 处理多个异常
    • 14.7 捕获所有异常
    • 14.8 创建自定义异常
    • 14.9 捕获异常后抛出另外的异常
    • 14.10 重新抛出被捕获的异常
    • 14.11 输出警告信息
    • 14.12 调试基本的程序崩溃错误
    • 14.13 给你的程序做性能测试
    • 14.14 加速程序运行
  • 第 15 章 C 语言扩展
    • 15.1 使用ctypes访问C代码
    • 15.2 简单的C扩展模块
    • 15.3 编写扩展函数操作数组
    • 15.4 在C扩展模块中操作隐形指针
    • 15.5 从扩张模块中定义和导出C的API
    • 15.6 从C语言中调用Python代码
    • 15.7 从C扩展中释放全局锁
    • 15.8 C和Python中的线程混用
    • 15.9 用WSIG包装C代码
    • 15.10 用Cython包装C代码
    • 15.11 用Cython写高性能的数组操作
    • 15.12 将函数指针转换为可调用对象
    • 15.13 传递NULL结尾的字符串给C函数库
    • 15.14 传递Unicode字符串给C函数库
    • 15.15 C字符串转换为Python字符串
    • 15.16 不确定编码格式的C字符串
    • 15.17 传递文件名给C扩展
    • 15.18 传递已打开的文件给C扩展
    • 15.19 从C语言中读取类文件对象
    • 15.20 处理C语言中的可迭代对象
    • 15.21 诊断分段错误
Powered by GitBook
On this page
  • 问题
  • 解决方案
  • 讨论

Was this helpful?

  1. 第 15 章 C 语言扩展

15.10 用Cython包装C代码

问题

你想使用 Cython 来创建一个 Python 扩展模块,用来包装某个已存在的 C 函数库。

解决方案

使用 Cython 构建一个扩展模块看上去很手写扩展有些类似, 因为你需要创建很多包装函数。不过,跟前面不同的是,你不需要在 C 语言中做这些——代码看上去更像是 Python。

作为准备,假设本章介绍部分的示例代码已经被编译到某个叫 libsample 的 C 函数库中了。 首先创建一个名叫 csample.pxd 的文件,如下所示:

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

cdef extern from "sample.h":
    int gcd(int, int)
    bint in_mandel(double, double, int)
    int divide(int, int, int *)
    double avg(double *, int) nogil

    ctypedef struct Point:
         double x
         double y

    double distance(Point *, Point *)

这个文件在 Cython 中的作用就跟 C 的头文件一样。 初始声明 cdef extern from "sample.h" 指定了所需的 C 头文件。 接下来的声明都是来自于那个头文件。文件名是 csample.pxd ,而不是 sample.pxd ——这点很重要。

下一步,创建一个名为 sample.pyx 的问题。 该文件会定义包装器,用来桥接 Python 解释器到 csample.pxd 中声明的 C 代码。

# sample.pyx
# Import the low-level C declarations
cimport csample

# Import some functionality from Python and the C stdlib
from cpython.pycapsule cimport *

from libc.stdlib cimport malloc, free

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

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

def divide(x, y):
    cdef int rem
    quot = csample.divide(x, y, &rem)
    return quot, rem

def avg(double[:] a):
    cdef:
        int sz
        double result

    sz = a.size
    with nogil:
        result = csample.avg(<double *> &a[0], sz)
    return result

# Destructor for cleaning up Point objects
cdef del_Point(object obj):
    pt = <csample.Point *> PyCapsule_GetPointer(obj,"Point")
    free(<void *> pt)

# Create a Point object and return as a capsule
def Point(double x,double y):
    cdef csample.Point *p
    p = <csample.Point *> malloc(sizeof(csample.Point))
    if p == NULL:
        raise MemoryError("No memory to make a Point")
    p.x = x
    p.y = y
    return PyCapsule_New(<void *>p,"Point",<PyCapsule_Destructor>del_Point)

def distance(p1, p2):
    pt1 = <csample.Point *> PyCapsule_GetPointer(p1,"Point")
    pt2 = <csample.Point *> PyCapsule_GetPointer(p2,"Point")
    return csample.distance(pt1,pt2)

该文件更多的细节部分会在讨论部分详细展开。 最后,为了构建扩展模块,像下面这样创建一个 setup.py 文件:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [
    Extension('sample',

              ['sample.pyx'],
              libraries=['sample'],
              library_dirs=['.'])]
setup(
  name = 'Sample extension module',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

要构建我们测试的目标模块,像下面这样做:

bash % python3 setup.py build_ext --inplace
running build_ext
cythoning sample.pyx to sample.c
building 'sample' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes
 -I/usr/local/include/python3.3m -c sample.c
 -o build/temp.macosx-10.6-x86_64-3.3/sample.o
gcc -bundle -undefined dynamic_lookup build/temp.macosx-10.6-x86_64-3.3/sample.o
  -L. -lsample -o sample.so

如果一切顺利的话,你应该有了一个扩展模块 sample.so ,可在下面例子中使用:

>>> import sample
>>> sample.gcd(42,10)
2
>>> sample.in_mandel(1,1,400)
False
>>> sample.in_mandel(0,0,400)
True
>>> sample.divide(42,10)
(4, 2)
>>> import array
>>> a = array.array('d',[1,2,3])
>>> sample.avg(a)
2.0
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> p1
<capsule object "Point" at 0x1005d1e70>
>>> p2
<capsule object "Point" at 0x1005d1ea0>
>>> sample.distance(p1,p2)
2.8284271247461903

讨论

本节包含了很多前面所讲的高级特性,包括数组操作、包装隐形指针和释放 GIL。 每一部分都会逐个被讲述到,但是我们最好能复习一下前面几小节。 在顶层,使用 Cython 是基于 C 之上。.pxd 文件仅仅只包含 C 定义(类似 .h 文件), .pyx 文件包含了实现(类似 .c 文件)。cimport 语句被 Cython 用来导入 .pxd 文件中的定义。 它跟使用普通的加载 Python 模块的导入语句是不同的。

尽管 .pxd 文件包含了定义,但它们并不是用来自动创建扩展代码的。 因此,你还是要写包装函数。例如,就算 csample.pxd 文件声明了 int gcd(int, int) 函数, 你仍然需要在 sample.pyx 中为它写一个包装函数。例如:

cimport csample

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

对于简单的函数,你并不需要去做太多的事。 Cython 会生成包装代码来正确的转换参数和返回值。 绑定到属性上的 C 数据类型是可选的。不过,如果你包含了它们,你可以另外做一些错误检查。 例如,如果有人使用负数来调用这个函数,会抛出一个异常:

>>> sample.gcd(-10,2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "sample.pyx", line 7, in sample.gcd (sample.c:1284)
    def gcd(unsigned int x,unsigned int y):
OverflowError: can't convert negative value to unsigned int

如果你想对包装函数做另外的检查,只需要使用另外的包装代码。例如:

def gcd(unsigned int x, unsigned int y):
    if x <= 0:
        raise ValueError("x must be > 0")
    if y <= 0:
        raise ValueError("y must be > 0")
    return csample.gcd(x,y)

在 csample.pxd 文件中的 in_mandel() 声明有个很有趣但是比较难理解的定义。 在这个文件中,函数被声明为然后一个 bint 而不是一个 int。 它会让函数创建一个正确的 Boolean 值而不是简单的整数。 因此,返回值 0 表示 False 而 1 表示 True。

在 Cython 包装器中,你可以选择声明 C 数据类型,也可以使用所有的常见 Python 对象。 对于 divide() 的包装器展示了这样一个例子,同时还有如何去处理一个指针参数。

def divide(x,y):
    cdef int rem
    quot = csample.divide(x,y,&rem)
    return quot, rem

在这里,rem 变量被显示的声明为一个 C 整型变量。 当它被传入 divide() 函数的时候,&rem 创建一个跟 C 一样的指向它的指针。 avg() 函数的代码演示了 Cython 更高级的特性。 首先 def avg(double[:] a) 声明了 avg() 接受一个一维的双精度内存视图。 最惊奇的部分是返回的结果函数可以接受任何兼容的数组对象,包括被 numpy 创建的。例如:

>>> import array
>>> a = array.array('d',[1,2,3])
>>> import numpy
>>> b = numpy.array([1., 2., 3.])
>>> import sample
>>> sample.avg(a)
2.0
>>> sample.avg(b)
2.0

在此包装器中,a.size0 和 &a[0] 分别引用数组元素个数和底层指针。 语法 &a[0] 教你怎样将指针转换为不同的类型。 前提是 C 中的 avg() 接受一个正确类型的指针。 参考下一节关于 Cython 内存视图的更高级讲述。

除了处理通常的数组外,avg() 的这个例子还展示了如何处理全局解释器锁。语句 with nogil: 声明了一个不需要 GIL 就能执行的代码块。 在这个块中,不能有任何的普通 Python 对象——只能使用被声明为 cdef 的对象和函数。 另外,外部函数必须现实的声明它们能不依赖 GIL 就能执行。 因此,在 csample.pxd 文件中,avg() 被声明为 double avg(double *, int) nogil .

对 Point 结构体的处理是一个挑战。本节使用胶囊对象将 Point 对象当做隐形指针来处理,这个在 15.4 小节介绍过。 要这样做的话,底层 Cython 代码稍微有点复杂。 首先,下面的导入被用来引入 C 函数库和 Python C API 中定义的函数:

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

函数 del_Point() 和 Point() 使用这个功能来创建一个胶囊对象, 它会包装一个 Point * 指针。cdef del_Point() 将 del_Point() 声明为一个函数, 只能通过 Cython 访问,而不能从 Python 中访问。 因此,这个函数对外部是不可见的——它被用来当做一个回调函数来清理胶囊分配的内存。 函数调用比如 PyCapsule_New()、PyCapsule_GetPointer() 直接来自 Python C API 并且以同样的方式被使用。

distance 函数从 Point() 创建的胶囊对象中提取指针。 这里要注意的是你不需要担心异常处理。 如果一个错误的对象被传进来,PyCapsule_GetPointer() 会抛出一个异常, 但是 Cython 已经知道怎么查找到它,并将它从 distance() 传递出去。

处理 Point 结构体一个缺点是它的实现是不可见的。 你不能访问任何属性来查看它的内部。 这里有另外一种方法去包装它,就是定义一个扩展类型,如下所示:

# sample.pyx

cimport csample
from libc.stdlib cimport malloc, free
#...

cdef class Point:
    cdef csample.Point *_c_point
    def __cinit__(self, double x, double y):
        self._c_point = <csample.Point *> malloc(sizeof(csample.Point))
        self._c_point.x = x
        self._c_point.y = y

    def __dealloc__(self):
        free(self._c_point)

    property x:
        def __get__(self):
            return self._c_point.x
        def __set__(self, value):
            self._c_point.x = value

    property y:
        def __get__(self):
            return self._c_point.y
        def __set__(self, value):
            self._c_point.y = value

def distance(Point p1, Point p2):
    return csample.distance(p1._c_point, p2._c_point)

在这里,cdif 类 Point 将 Point 声明为一个扩展类型。 类属性 cdef csample.Point *_c_point 声明了一个实例变量, 拥有一个指向底层 Point 结构体的指针。 __cinit__() 和 __dealloc__() 方法通过 malloc() 和 free() 创建并销毁底层 C 结构体。 x 和 y 属性的声明让你获取和设置底层结构体的属性值。 distance() 的包装器还可以被修改,使得它能接受 Point 扩展类型实例作为参数, 而传递底层指针给 C 函数。

做了这个改变后,你会发现操作 Point 对象就显得更加自然了:

>>> import sample
>>> p1 = sample.Point(2,3)
>>> p2 = sample.Point(4,5)
>>> p1
<sample.Point object at 0x100447288>
>>> p2
<sample.Point object at 0x1004472a0>
>>> p1.x
2.0
>>> p1.y
3.0
>>> sample.distance(p1,p2)
2.8284271247461903

本节已经演示了很多 Cython 的核心特性,你可以以此为基准来构建更多更高级的包装。 不过,你最好先去阅读下官方文档来了解更多信息。

Previous15.9 用WSIG包装C代码Next15.11 用Cython写高性能的数组操作

Last updated 5 years ago

Was this helpful?