楔子
C 语言里面存在结构体、共同体、枚举,而这些在 Cython 里面也是支持的,只不过声明的方式不太一样,下面来看一看。
结构体
在 C 里面定义一个结构体,一般有两种方式。
#include <stdio.h> // 直接定义,此时 struct Girl 整体是一个类型 struct Girl { char *name; int age; }; // 使用 typedef 起一个别名 // 此时 Boy 是一个类型 typedef struct { char *name; int age; } Boy; int main() { // 声明一个 struct Girl 类型的实例 struct Girl g; g.name = "mercy"; g.age = 37; // 声明一个 Body 类型的实例 Boy b; b.name = "hanzo"; b.age = 38; printf("name = %s, age = %d\n", g.name, g.age); printf("name = %s, age = %d\n", b.name, b.age); /* name = mercy, age = 37 name = hanzo, age = 38 */ }
以上是 C 的结构体,在 Cython 里面要如何定义呢?
# 相当于 C 的 struct Girl{...}; cdef struct Girl: char *name int age # 相当于 C 的 typedef struct {...} Boy; ctypedef struct Boy: char *name int age # 创建结构体实例,无论结构体使用哪一种方式定义 # 在创建实例的时候,格式都是一样的 cdef Girl g1 cdef Boy b1 # 创建的时候也可以直接赋值,支持位置参数和关键字参数 # 但是使用关键字参数要注意顺序 # 结构体字段出现的顺序,就是参数的顺序 cdef Girl g2 = Girl("mercy", 37) cdef Boy b2 = Boy("hanzo", age=38) # 打印的时候会以字典的形式打印 print(g2) print(b2) """ {'name': b'mercy', 'age': 37} {'name': b'hanzo', 'age': 38} """ # 当然啦,也可以先声明,然后单独赋值 # 我们上面创建了 g1 和 b1,下面赋值 g1.name, g1.age = "mercy", 37 b1.name, b1.age = "hanzo", 38 print(g1) print(b1) """ {'name': b'mercy', 'age': 37} {'name': b'hanzo', 'age': 38} """ # 通过 Python 的字典赋值 # 显然它涉及数据转换,会有额外开销 # 不建议使用此方式 cdef Girl g3 = {"name": "mercy", "age": 37} cdef Boy b3 = {"name": "hanzo", "age": 38} print(g3) print(b3) """ {'name': b'mercy', 'age': 37} {'name': b'hanzo', 'age': 38} """
在 C 里面还存在结构体的嵌套:
struct Girl { struct { char *name; int age; } Person; int length; };
Cython 也是允许结构体嵌套的,但是定义必须要单独拿出来,什么意思呢?看个例子就明白了。
cdef struct Person: char *name int age # Person 的定义必须要单独拿出来定义 # 不可以嵌套定义 cdef struct Girl: Person person int length cdef Girl g = Girl(person=Person("mercy", 37), length=167) print(g) """ {'person': {'name': b'mercy', 'age': 37}, 'length': 167} """
另外,当定义结构体的时候,字段的类型必须都是 C 的类型。
共同体
共同体的声明在 C 和 Cython 里面都和结构体类似,我们来看一下。
cdef union U1: short n1 int n2 ctypedef union U2: short n1 int n2 cdef U1 u1 u1.n2 = 0X1234_4321 print(u1.n1 == 0X4321) # True cdef U2 u2 u2.n2 = 0X4321_1234 print(u2.n1 == 0X1234) # True
使用方法和结构体类似,关于共同体的具体知识这里就不多说了,可以查询 C 语言共同体相关的内容。总之它的目的是为了节省内存,一个结构体实例所占的内存,等于内部所有字段所占内存之和;而一个共同体实例所占的内存,等于内部占用内存最大的字段。
cdef struct S: short field1 # 2 字节 int field2 # 4 字节 ssize_t field3 # 8 字节 cdef union U: short field1 int field2 ssize_t field3 cdef S s cdef U u # 等于所有字段所占内存之和 # 2 + 4 + 8 = 14,但由于存在内存对齐 # 所以是 16 print(sizeof(s)) # 16 # 最长字段所占的内存 # 因此是 8 print(sizeof(u)) # 8
因此共同体明显要省内存,但是修改某一个字段,会影响其它字段,因为字段之间共用一组内存。而结构体则不会,字段之间互不影响,因为它们使用的是不同的内存。
枚举
定义枚举也很简单,我们可以在多行中定义,也可以在单行中定义然后用逗号隔开。
# 相当于 C 的 enum COLOR1 {}; cdef enum COLOR1: RED = 1 YELLOW = 3 GREEN = 5 # 相当于 C 的 typedef enum {} COLOR2; ctypedef enum COLOR2: PURPLE, BROWN # 在 C 里面声明一个枚举变量 # 可以是 enum COLOR1,整体是一个类型 # 也可以直接使用 COLOR2 # 但在 Cython 里面声明方式是一样的 # cdef COLOR1、cdef COLOR2 cdef COLOR1 c1 = YELLOW cdef COLOR2 c2 = BROWN print(YELLOW) print(BROWN) """ 3 1 """
枚举比较简单。
以上就是结构体、共同体和枚举在 Cython 中的使用方式,但说实话,如果只是写 Cython,那么我们很少会用到这些 C 级结构。像结构体、共同体、枚举这些结构,一般都是在和外部的 C 代码交互的时候才会用到,比如我们要调用一个现有的 C 库函数,但这个函数接收一个结构体,那么这个时候我们会构造一个结构体然后传过去。
使用 ctypedef 给类型起别名
在 C 语言中有一个 typedef 关键字,可以用来给类型起别名,就像我们上面演示的那样。如果是使用 struct S {}; 这种方式定义结构体,那么 struct S 整体是一个类型。但我们可以通过 typedef 起一个别名,比如 typedef struct S{} S2; 那么声明的时候既可以使用 struct S,也可以直接使用 S2,因为 typedef 给 struct S 起了一个别名叫 S2。
注意:typedef 并不是定义了一个新的类型,它只是给一个已经存在的类型起了一个别名而已,像 CPython 里面有一个 Py_ssize_t,它就是 ssize_t 的别名。所以这个关键字的名字起的不好,会让人以为使用 typedef 是定义一个新类型一样。
#include <stdio.h> typedef int MY_INT; int main() { MY_INT age = 17; printf("%d\n", age); // 17 }
我们上面给 int 起了一个别名叫 MY_INT,而在 Cython 里面也是支持的,只不过关键字不叫 typedef,而是叫 ctypedef。
ctypedef list MY_LIST def foo(MY_LIST lst): pass
文件名叫 cython_test.pyx,我们调用一下看看。
import pyximport pyximport.install(language_level=3) import cython_test try: cython_test.foo(123) except TypeError as e: print(e) """ Argument 'lst' has incorrect type (expected list, got int) """
参数接收的是 list 类型,因为 MY_LIST 是 list 的别名,但是我们传了一个整数进去,所以报错了。当然不管什么 Python 类型,None 都是满足的。
ctypedef 可以作用于 C 的类型也可以作用于 Python 类型,起别名之后,这个别名可以像上面那样作用于函数参数,也可以用于声明一个变量,比如 cdef MY_LIST lst。
但是不可以像这样:MY_LIST("123"),或者 isinstance(xxx, MY_LIST)。起的别名只能用在 C 语义当中,比如类型声明、或者 <MY_LIST?> 将变量静态化。但是不能用在 Python 语义当中,比如调用之类的,否则会报错:'MY_LIST' is not a constant, variable or function identifier。
ctypedef 一般也是用在和外部 C 代码交互上面,如果是纯 Cython,那么很少使用 ctypedef 起别名。但是对于 C++ 来说起别名则很常见,因为使用 typedef 可以显著的缩短长模板类型。
另外 ctypedef 对出现的位置也是有要求的,如果不和外部 C 代码交互的话,它应该出现在全局作用域中,不可以出现在函数等局部作用域里,也不可以出现在 if, for, while 语句块内。