古明地觉的编程教室

让 Python 的属性查找具有 C 一级的性能

前面我们介绍了静态类,静态类实例在属性查找方面比动态类要高效很多,因为静态类实例的属性是通过数组存储的,一个萝卜一个坑,访问的时候基于索引访问。但无论是静态类还是动态类,其实例在查找属性时,底层都会调用 PyObject_GetAttr 这个 C API。

那么问题来了,能不能再快一点呢?因为一旦涉及到 Python/C API,效率都是不高的,而 Python 的对象在底层都是一个 C 结构体,那么在查找属性的时候能不能直接访问 C 结构体的字段呢?不要再走 Python 的 C API 了。

我们举例说明:

cdef class Score:
    cdef 
public:
        
int chinese, math, english

    def __init__(self, 

int chinese,
                 
int math, int english):
        self.chinese = chinese
        self.math = math
        self.english = english

s = Score(90, 98, 92)
print(
    s.chinese, s.math, s.english
)  # 90 98 92

以上是一段 Cython 代码,如果换成功能相同的 C 代码的话:

#include <stdio.h>

typedef struct {
    int chinese;
    int math;
    int english;
} Score;

int main() {
    Score s = {90, 98, 92};
    printf("%d %d %d\n",
           s.chinese, s.math, s.english); // 90 98 92
}

那么问题来了,我们能不能将 Cython 中的属性访问,转成 C 一级的属性访问呢?答案是可以的,下面来看一下具体的做法。

# 文件名:score.pyx
cdef class Score
:
    cdef 
public:
        
int chinese, math, english

    def __init__(self, 

int chinese,
                 
int math, int english):
        self.chinese = chinese
        self.math = math
        self.english = english

我们需要先将 score.pyx 编译成扩展模块,编译方式很简单,这里不再赘述了。然后还要编写一个头文件 score.h:

#include <Python.h>

// 定义一个 C 结构体,模拟扩展类 Score
typedef struct {
    PyObject_HEAD
    int chinese;
    int math;
    int english;
} C_Score;

接下来再编写一个 Cython 源文件导入它:

# 文件名:cython_test.pyx
cdef 
extern from "score.h":
    ctypedefclass score.
Score [objectC_Score]:
        cdef:
            
int chinese
            
int math
            
int english

# 这里我们使用了 ctypedef class,可以简单认为导入了一个类
# 而 ctypedef class 紧跟的是 score.Score,编译器在看到之后
# 就知道要从 score 模块里导入类 Score
# 所以我们不需要 from score import Score,会自动导入

# 但是导入了还不算完,后面还跟了一个 C_Score
# C_Score 和 score.Score 里面的成员都是一样的
# 然后 Cython 编译器会生成 C 结构体字段的直接访问
# 而不会再走 Python 的 C API

def summer(

Score s):
    # 进行类型声明的话,只需要使用 Score 即可
    # 并且这里的 Score 只能在 Cython 内部使用
    return s.chinese + s.math + s.english

然后进行编译,导入测试一下:

from score import Score
import cython_test

s = Score(99, 98, 97)
print(cython_test.summer(s))  # 294

结果没有任何问题,并且此时 Cython 是直接访问的结构体字段,而不是使用 __getattr__。

然后要说明的是,类的名字和 C 结构体的名字不要求相同,但是内部字段的名字应该是相同的。假设 C_Score 的字段如下:

#include <Python.h>

// 定义一个 C 结构体,模拟扩展类 Score
typedef struct {
    PyObject_HEAD
    int a;
    int b;
    int c;
} C_Score;

那么声明的时候就应该这么做:

# 文件名:cython_test.pyx
cdef 
extern from "score.h":
    ctypedefclass score.
Score [objectC_Score]:
        cdef:
            
int chinese "a"
            
int math "b"
            
int english "c"

否则的话,在 C 结构体中就找不到对应的字段。当然啦,找不到的话会退化使用 __getattr__,不会报错。

此外该方法也适用于内置类型,不过用的不多,而且我们也很少使用 ctypedef class,因此本文的内容了解一下即可。

Image

E N D

Image

ImageImageImage