使用Cython优化Python函数以提高速度
几周前,我在网上问了一个关于如何提高用Python写的函数速度的问题。那时候,有人提到可以用Cython来实现这个目标。他还很友好地给了我一个示例,教我如何把那段代码用Cython处理。我想对下面的代码做同样的事情,看看通过声明变量类型能让它快多少。我有几个相关的问题。我看过cython.org上的教程,但还是有些疑问。这些问题是紧密相关的:
- 我对C语言一无所知。我需要学习哪些部分,才能用Cython来声明变量类型?
- 在C语言中,什么类型对应Python的列表和元组?比如,我可以在Cython中用
double
来表示Python中的float
。那我该怎么处理列表呢?一般来说,在哪里可以找到某个Python类型对应的C类型?
如果能给我一个如何将下面的代码用Cython处理的示例,那就太好了。我在代码中插入了注释,说明了变量的类型。
class Some_class(object):
** Other attributes and functions **
def update_awareness_status(self, this_var, timePd):
'''Inputs: this_var (type: float)
timePd (type: int)
Output: None'''
max_number = len(self.possibilities)
# self.possibilities is a list of tuples.
# Each tuple is a pair of person objects.
k = int(math.ceil(0.3 * max_number))
actual_number = random.choice(range(k))
chosen_possibilities = random.sample(self.possibilities,
actual_number)
if len(chosen_possibilities) > 0:
# chosen_possibilities is a list of tuples, each tuple is a pair
# of person objects. I have included the code for the Person class
# below.
for p1,p2 in chosen_possibilities:
# awareness_status is a tuple (float, int)
if p1.awareness_status[1] < p2.awareness_status[1]:
if p1.value > p2.awareness_status[0]:
p1.awareness_status = (this_var, timePd)
else:
p1.awareness_status = p2.awareness_status
elif p1.awareness_status[1] > p2.awareness_status[1]:
if p2.value > p1.awareness_status[0]:
p2.awareness_status = (price, timePd)
else:
p2.awareness_status = p1.awareness_status
else:
pass
class Person(object):
def __init__(self,id, value):
self.value = value
self.id = id
self.max_val = 50000
## Initial awareness status.
self.awarenessStatus = (self.max_val, -1)
2 个回答
C语言并不直接支持列表这个概念。
它的基本数据类型包括 int
(还有 char
、short
、long
)、float
和 double
(这些类型和Python中的对应类型比较简单明了),还有指针。
如果你对指针这个概念不太了解,可以看看这个链接:维基百科:指针
在某些情况下,指针可以用来替代元组或数组。字符指针是所有字符串的基础。
比如说你有一个整数数组,你会把它存储在一块连续的内存区域里,并且有一个起始地址。你需要定义类型(int
)并且说明这是一个指针(*
):
cdef int * array;
现在你可以这样访问数组中的每个元素:
array[0] = 1
不过,内存必须先分配(比如使用 malloc
),而且高级索引是无法使用的(例如 array[-1]
会得到内存中的随机数据,超出分配空间的索引也是如此)。
更复杂的类型在C语言中并没有直接对应,但通常有一种C语言的方式可以做到这些,而不需要使用Python的类型(例如,for循环并不需要一个范围数组或迭代器)。
正如你自己注意到的,写出好的Cython代码需要对C语言有更详细的了解,所以接下来去找个教程学习是个不错的选择。
总的来说,你可以通过运行 cython
命令并加上 -a
这个“注释”选项,看到 Cython 为每一行源代码生成的 C 代码。具体的例子可以参考 Cython 的 文档。这在你想找出函数内部的瓶颈时,特别有帮助。
另外,还有一个叫做 “提前绑定以提高速度” 的概念,适用于将你的代码转换为 Cython。Python 对象(比如下面的 Person
类的实例)在访问属性时使用的是普通的 Python 代码,这在内层循环中会比较慢。我猜如果你把 Person
类改成 cdef class
,你会看到一些速度上的提升。此外,你还需要在内层循环中给 p1
和 p2
对象指定类型。
由于你的代码中有很多 Python 调用(比如 random.sample
),除非你找到方法把这些行转换成 C 代码,否则你可能不会获得很大的速度提升,这需要花费不少精力。
你可以把东西写成 tuple
或 list
,但这通常不会带来太大的速度提升。尽量在可能的情况下使用 C 数组;这部分你需要查一下。
通过下面这些简单的修改,我得到了 1.6 倍的速度提升。注意,我在这里和那里做了一些更改才能让它编译通过。
ctypedef int ITYPE_t
cdef class CyPerson:
# These attributes are placed in the extension type's C-struct, so C-level
# access is _much_ faster.
cdef ITYPE_t value, id, max_val
cdef tuple awareness_status
def __init__(self, ITYPE_t id, ITYPE_t value):
# The __init__ function is much the same as before.
self.value = value
self.id = id
self.max_val = 50000
## Initial awareness status.
self.awareness_status = (self.max_val, -1)
NPERSONS = 10000
import math
import random
class Some_class(object):
def __init__(self):
ri = lambda: random.randint(0, 10)
self.possibilities = [(CyPerson(ri(), ri()), CyPerson(ri(), ri())) for i in range(NPERSONS)]
def update_awareness_status(self, this_var, timePd):
'''Inputs: this_var (type: float)
timePd (type: int)
Output: None'''
cdef CyPerson p1, p2
price = 10
max_number = len(self.possibilities)
# self.possibilities is a list of tuples.
# Each tuple is a pair of person objects.
k = int(math.ceil(0.3 * max_number))
actual_number = random.choice(range(k))
chosen_possibilities = random.sample(self.possibilities,
actual_number)
if len(chosen_possibilities) > 0:
# chosen_possibilities is a list of tuples, each tuple is a pair
# of person objects. I have included the code for the Person class
# below.
for persons in chosen_possibilities:
p1, p2 = persons
# awareness_status is a tuple (float, int)
if p1.awareness_status[1] < p2.awareness_status[1]:
if p1.value > p2.awareness_status[0]:
p1.awareness_status = (this_var, timePd)
else:
p1.awareness_status = p2.awareness_status
elif p1.awareness_status[1] > p2.awareness_status[1]:
if p2.value > p1.awareness_status[0]:
p2.awareness_status = (price, timePd)
else:
p2.awareness_status = p1.awareness_status