C 和 Python 在数据类型上都有各自的成熟特性,比如支持数据在不同类型之间进行转换。
# Py_ssize_t 是 ssize_t 的别名 cdef Py_ssize_t num = 123 # 声明一个指针类型的变量 cdef Py_ssize_t *p = &num # 转成浮点型指针 cdef double *p2 = <double *> p
指针类型可以任意转换,这里我们将整型指针转成了浮点型指针,显然这是合法的。在 C 里面类型转换使用的是小括号,这里使用的是尖括号。
由于指针类型的转换不受限制,所以我们可以手动实现内置函数 id 的功能。
def my_id(obj): """ 实现内置函数 id 的功能 我们知道 Python 的变量本质上就是个指针 id(obj) 会获取 obj 指向对象的地址 说白了,不就是 obj 本身吗? """ # obj 是 PyObject * 类型, 转成 void * cdef void *p = <void *> obj # 而指针存储的值本质上是一串 16 进制整数 # void * 再转成 Py_ssize_t 即可拿到地址 return <Py_ssize_t> p
我们来测试一下:
import pyximport pyximport.install(language_level=3) import cython_test num = 123 print(cython_test.my_id(num)) print(id(num)) """ 140706631063024 140706631063024 """ s = "古明地觉" print(cython_test.my_id(s)) print(id(s)) """ 2125601466576 2125601466576 """ print(cython_test.my_id(object)) print(id(object)) """ 140706630839120 140706630839120 """
我们实现的函数 my_id 和内置函数 id 打印的结果是一样的,所以 Python 虽然一切皆对象,但是我们操作的都是指向对象的指针,通过指针来操作对象。所以 id(obj) 表示的不是获取 obj 的地址,而是 obj 指向对象的地址,如果站在 C 的角度,那么就是 obj 存储的值本身。
所以 id 函数所做的事情就是把变量存储的地址转成 10 进制整数返回,我们如果想实现 id 函数的功能,只需要将变量(PyObject *)转成 void *,因为不同类型的指针可以相互转换,虽然转换之后指针的含义变了,但存储的地址不变。然后再将 void * 转成 Py_ssize_t,即可拿到存储的地址。
可能有人好奇,为什么先要转成 void * 之后,才能转成整型呢?直接转成整型不行吗?我们来测试一下,这两者的区别。
def my_id1(obj): # 先转成 void *,再转成 Py_ssize_t return <Py_ssize_t> <void *> obj def my_id2(obj): # 直接转成 Py_ssize_t return <Py_ssize_t> obj num = 666 print(my_id1(num)) print(my_id2(num)) """ 1617542740432 666 """
区别很明显了,因为 Python 的变量是一个指向值的指针。如果转换之后的类型是指针类型,那么转换的是变量;如果转换之后的类型不是指针类型,那么转换的是对象。
所以 my_id2 返回的是 666,由于转换之后的类型不是指针类型,因此参与转换的是对象,相当于将 Python 整数转成了静态的 C 整数。并且 num 必须指向一个整数,否则它无法和 C 的 Py_ssize_t 类型相对应。
而对于 my_id1 函数,转换之后的类型是指针类型,所以参与转换的是变量、即 PyObject *。任何指针都可以和 void * 转换,因此先将变量转成 void *,然后再由 void * 转成整型。
另外再补充一点,我们前面说指针的转换不受限制,针对的是纯 C 代码。但现在不是纯 C,而是 Cython,所以指针转换还是有限制的,这个限制主要针对 PyObject *,它只能转成 void *。
<int *> obj
上面的代码是有问题的,Python 类型的变量在指针转换的时候,只能转成 void *。当然啦,char * 是个例外。
name = b"satori" print(<char *> name) # b'satori'我们看到
char * 表示 C 的字符串类型,上述代码做的事情就是将 Python 字节串转成 C 字符串,当然打印的时候还是以 Python 字节串的形式打印的。所以 char * 算是一个例外吧,在 Cython 里面是把 char * 整体作为一个基础类型来看的,并且在转换的时候 name 必须指向一个 bytes 对象,否则会转换失败,因为 char * 和 Python 里面的 bytes 是相对应的。
但是我们发现,无论是指针类型、还是常规类型,这里都是 C 的类型。那么可不可以转成 Python 类型呢?答案是可以的,来看个例子。
def func(a): cdef list lst1 = list(a) print(lst1) print(type(lst1)) cdef list lst2 = <list> a print(lst2) print(type(lst2)) func([1, 2, 3]) """ [1, 2, 3] <class 'list'> [1, 2, 3] <class 'list'> """ func((1, 2, 3)) """ [1, 2, 3] <class 'list'> (1, 2, 3) <class 'tuple'> """
打印的结果很明显,如果是 list(a),那么会根据 a 指向的对象创建一个新的列表,所以 lst1 一定指向一个列表。但 <list> a 则是将变量 a、也就是 PyObject * 拷贝一份,然后转成 PyListObject *,相当于将动态变量转成静态变量。
第一次调用 func,参数 a 指向了一个列表,但它是泛型指针。于是通过 <list> a 将它转成静态的,在操作的时候可以避免一些额外开销,当然不管是动态还是静态,指向的都是列表。另外这个例子有点刻意了,其实直接 cdef list lst2 = a 就可以了,因为做好了类型标注,那么会自动转换。
然后第二次调用 func,参数 a 指向了一个元组,显然对于 list(a) 是没有影响的,因为它会创建新列表。但 <list> a 就有问题了,因为 a 实际指向的是元组,应该是 <tuple> a,所以转换失败。在早期的 Cython 中会引发一个SystemError,但目前不会了,如果转换失败还保留原来的类型。
可如果我们希望在无法转换的时候报错,这个时候要怎么做呢?
def func(a): # 将 <list> 换成 <list?> 即可 cdef list lst2 = <list?> a print(lst2) print(type(lst2))
此时传递其它对象就会报错了,比如我们传递了一个元组,会报出 TypeError: Expected list, got tuple。
尖括号里面的类型可以任意,包括 C 类型以及 Python 内置类型。但说实话,使用尖括号做类型转换的场景不是很多,我们通过 cdef 指定类型时,会自动转换。当然后续,我们也会给出使用尖括号做类型转换的一些最佳实践。