libc
之前我们使用了这样一条导入语句:from libc.stdlib cimport malloc,显然这是 Cython 提供的预定义 .pxd 文件,位于 Cython 主目录的 Includes 目录中。
C 的头文件在 Cython 里面是以 .pxd 文件的形式存在的,所以这个目录相当于包含了常用的 C 头文件。当然这些都只是声明,至于具体实现则隐藏在编译器当中,我们看不到罢了。
from libc cimport stdio stdio.printf(<char *>"name = %s, age = %d\n", <char *>"satori", <int> 16) """ name = satori, age = 16 """
在 C 里面 #include <stdio.h> 之后,可以直接使用 printf,但是在 Cython 里面则需要通过 stdio.printf 的形式。这是由 Python 的命名空间决定的,因为 printf 位于 stdio 里面,在属性查找时必须先找到 stdio,然后才能找到 printf。
或者使用 from libc.stdio cimport printf 直接将某个具体的函数导入进来也行,这样就能直接使用 printf 了。当然啦,我们也可以 cimport *,这样就和 C 的 #include 一致了。
另外在导入的时候,记得名字不要冲突:
from libc.math cimport sin from math import sin """ from libc.math cimport sin from math import sin ^ ------------------------------------------------------------ cython_test.pyx:2:17: Assignment to non-lvalue 'sin' """
显然导入的两个函数重名了,因此 Cython 引发了一个编译错误。而为了修复这一点,我们只需要这么做。
from libc.math cimport sin as c_sin from math import sin as py_sin print(c_sin(3.1415926 / 2)) print(py_sin(3.1415926 / 2)) """ 0.9999999999999997 0.9999999999999997 """
此时就没有任何问题了。
另外导入函数的时候不可以重名,但如果我们导入的是模块的话,那么是可以重名的。
from libc cimport math import math print(math.sin(math.pi / 2)) """ 1.0 """
尽管 import math 是在下面,但是调用的时候会从 C 标准库中进行调用。不过这种做法总归是不好的,我们应该修改一下:
from libc cimport math as c_math import math as py_math
所以这些预定义的 .pxd 文件就类似于 C 的头文件:
- 它们都声明了 C 一级的数据结构供外界调用;
- 它们都允许开发者对功能进行拆分, 分别通过不同的模块实现;
- 它们都实现了公共的 C 级接口;
至于每个文件里面都可以使用哪些函数,可以点进源码中查看:
关于这部分语法下一篇文章会说,总之可以看到和 C 的标准库是一致的,只不过声明的方式不同,一个是 C 的语法,一个是 Cython 的语法。但我们知道 Cython 代码也是要翻译成 C 代码的,所以 from libc cimport stdlib 最终也会被翻译成 #include <stdlib.h>。
并且 Includes 目录下除了 libc 之外,还有其它的包:
其中 libcpp 里面包含了 C++ 标准模板库(STL)容器的声明,如:string, vector, list, map, pair, set 等等。而 cpython 则可以让我们访问 Python/C API,当然还有一个重要的包就是 numpy,Cython 也是支持的。
访问 Python/C API
我们来看看如何通过 Cython 来访问 Python/C API。
from cpython.list cimport ( PyList_SetItem, PyList_GetItem ) cdef list names = ["古明地觉"] # names[0] = "古明地恋" 等价于如下 PyList_SetItem(names, 0, "古明地恋") print(names) """ ['古明地恋'] """ # print(names[0]) 等价于如下 # 由于 PyList_GetItem 返回的是 PyObject * # 我们需要转成 object # PyObject * 是 C 层面的, object 是 Python 层面的 print(<object>PyList_GetItem(names, 0)) """ 古明地恋 """ from cpython.object cimport ( PyObject_RichCompareBool, Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE ) # 2 < 1 等价于如下 print( PyObject_RichCompareBool(2, 1, Py_LT) ) # False # 2 > 1 等价于如下 print( PyObject_RichCompareBool(2, 1, Py_GT) ) # True from cpython.object cimport ( PyObject_IsInstance, PyObject_IsSubclass ) # isinstance(123, int) 等价于如下 print( PyObject_IsInstance(123, int) ) # True # issubclass(int, object) 等价于如下 print( PyObject_IsSubclass(int, object) ) # True
Python 的操作都可以通过 Python/C API 来实现,并且这种方式的速度要稍微快那么一点点。但是很明显会比较麻烦,使用 names[0] 肯定比 PyList_GetItem 这种方式来的直接,如果你还对其它的 API 感兴趣,也可以进入源码中查看。
想查看哪个对象的 API,就直接去对应的文件里面找即可。然后在导入的时候,可以直接通过 cpython 来导入,因为其它 .pxd 文件的内容都被导入到 __init__.pxd 里面了。
这个 __init__.pxd 和 __init__.py 类似,import 一个包会自动查找内部的 __init__.py,而 cimport 一个包会自动查找内部的 __init__.pxd。
小结
C、C++ 头文件通过 #include 命令进行访问,该命令会对相应的头文件进行包含。而 Cython 的 cimport 更智能,也更不容易出错,我们可以把它看做是一个使用命名空间的编译时导入语句。
而早期的 Cython 没有 cimport 语句,只有一个在源码级对文件进行包含的 include 语句,但是有了 cimport 之后就更加智能了。
E N D