预定义的 .pxd 文件

之前我们使用了这样一条导入语句:from libc.stdlib cimport malloc,显然这是 Cython 提供的预定义 .pxd 文件,位于 Cython 主目录的 Includes 目录中。

图片

C 的头文件在 Cython 里面是以 .pxd 文件的形式存在的,所以这个目录相当于包含了常用的 C 头文件。当然这些都只是声明,至于具体实现则隐藏在编译器当中,我们看不到罢了。

from libc cimport stdio

stdio.printf(<char *>"name = %s, age = %dn",
             <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 之后就更加智能了。

2022-07-10 09:00 发表于北京

阅读原文

简介:醉心于 Python 的东方厨,致力于提供最好的 Python 文章,点个关注吧(#^.^#)公众号:古明地觉的编程教室
(3)
打赏 喜欢就点个赞支持下吧 喜欢就点个赞支持下吧

声明:本文来自“古明地觉的编程教室”,分享链接:https://www.zyxiao.com/p/309754    侵权投诉

网站客服
网站客服
内容投稿 侵权处理
分享本页
返回顶部