Skip to content

Cython

简介

https://github.com/cython/cython

官网

官方文档

中文文档

Python 有调用 C 语言代码的能力,这意味着,可以用 C 语言重写某些代码来提升程序性能

Cython 是 Python 的超集。它几乎是 Python 和 C 语言的合并,是一种渐进类型的语言。
任何 Python 代码都是有效 Cython 代码。
Cython 代码可以编译成 C 语言代码,使用 Cython 可以编写一个模块或者一个方法,并逐渐扩展到越来越多的 C 语言类型,还可以将 C 语言类型和 Python 类型混合在一起,使用 Cython 可以获得混合后的完美组合,只在瓶颈处使用 C 语言优化,而在其他地方仍然可以享有 Python 的便捷与优雅

当 Python 的性能问题产生阻碍时,不需要把整个代码库用另一种语言来编写,只需要用 Cython 重写几个函数几乎就能得到所需要的性能,或者使用其他方法优化 Python

1
pip install Cython

cython 的 vscode 插件: Language-Cython

第一个例子

1
2
3
python setup.py --help-commands

python setup.py build_ext --help

由于 Cython 能接受几乎所有的合法 Python 源文件,开始使用 Cython 的最难的事情之一是怎么编译你的拓展(extension)。

从一个简单的例子开始:

新建一个hello的文件夹
在文件夹下面新建一个hello.pyx,内容如下

1
2
def say_hello():
    print("Hello!!!")

再新建一个文件setup.py:

1
2
3
4
5
6
from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("hello.pyx")
)

执行如下命令构建:

1
python setup.py build_ext --inplace

运行完之后会生成hello.cpython-37m-x86_64-linux-gnu.sohello.c

在当前文件夹下面打开 ipython:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
In [1]: import hello                                                   

In [2]: hello.say_hello()                                              
Hello!!!

In [3]: hello                                                          
Out[3]: <module 'hello' from '/home/zhaoyangzhen/hello/hello.cpython-37m-x86_64-linux-gnu.so'>

In [4]: ls                                                       
build/   hello.cpython-37m-x86_64-linux-gnu.so*  setup.py  hello.c  hello.pyx

如果在其他目录import hello是无法调用hello.say_hello()

如果删除掉build目录和hello.c,在当前目录也是可以正常使用这个模块的

执行python setup.py install这个命令,就可以在其他地方使用hello这个模块了

1
2
3
4
5
6
7
In [2]: import hello

In [3]: hello
Out[3]: <module 'hello' from '/home/zhaoyangzhen/anaconda3/lib/python3.7/site-packages/hello.cpython-37m-x86_64-linux-gnu.so'>

In [4]: hello.say_hello()
Hello!!!

调用 C/C++ 函数

https://github.com/cython/cython/tree/master/Cython/Includes

从 Cpython 导入

导入 C 函数

先以一个 C 标准库中的函数为例。 你不需要向你的代码中引入额外的依赖,Cython 都已经帮你定义好了这些函数。所以你可以将这些函数直接 cimport 进来并使用。
举个例子,比如说当你想用最简单的方法将char*类型的值转化为一个整型值时, 你可以使用atoi()函数,这个函数是在stdlib.h头文件中定义的。我们可以这样来写:

hello.pyx:

1
2
3
4
5
6
7
8
from libc.stdlib cimport atoi

cdef parse_charptr_to_py_int(char* s):
    assert s is not NULL, "byte string value is NULL"
    return atoi(s)   # note: atoi() has no error detection!

def test(s):
    return parse_charptr_to_py_int(s)
1
2
python setup.py build
python setup.py install
1
2
3
In [1]: import hello                                             
In [2]: hello.test('23333.33'.encode())                          
Out[2]: 23333
导入 C++ 函数

setup.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

extensions = [
    Extension("demo", sources=["demo.pyx"], extra_compile_args=["-O2"], language="c++")
]
setup(
    ext_modules = cythonize(extensions)
)

demo.pyx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from libcpp.vector cimport vector

def primes(unsigned int nb_primes):
    cdef int n, i
    cdef vector[int] p
    p.reserve(nb_primes)  # allocate memory for 'nb_primes' elements.

    n = 2
    while p.size() < nb_primes:  # size() for vectors is similar to len()
        for i in p:
            if n % i == 0:
                break
        else:
            p.push_back(n)  # push_back is similar to append()
        n += 1

    # Vectors are automatically converted to Python
    # lists when converted to Python objects.
    return p
1
2
python setup.py build
python setup.py install
1
2
3
4
5
6
7
In [1]: import demo                                              

In [2]: demo.primes(5)                                           
Out[2]: [2, 3, 5, 7, 11]

In [3]: demo.primes(10)                                          
Out[3]: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

外部引入

demo.pyx

1
2
3
4
5
cdef extern from "math.h":
    double sin(double x)

def py_sin(x):
    return sin(x)
1
2
3
4
5
6
7
In [1]: import demo                                                        

In [2]: demo.py_sin(0)                                                     
Out[2]: 0.0

In [4]: demo.py_sin(3.14 / 2)                                              
Out[4]: 0.9999996829318346

demo.pyx

1
2
3
4
5
cdef extern from "stdio.h":
    extern int printf(const char *format, ...)

def SayHello():
    printf("hello,world\n")
1
2
3
4
In [1]: import demo                                                        

In [2]: demo.SayHello()                                                    
hello,world

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <cstdio>
#include <map>
#include <algorithm>
using namespace std;

int test() {
    map<int, int> m;
    m[1] = 2333; m[2] = 666;
    int x = max(m[1], m[2]);
    return x;
}

test.h

1
2
3
4
5
6
#ifndef _TEST_H
#define _TEST_H

int test();

#endif

setup.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

extensions = [
    Extension("demo", sources=["demo.pyx", "test.cpp"], extra_compile_args=["-O2"], language="c++"),
]
setup(
    ext_modules = cythonize(extensions)
)

demo.pyx

1
2
3
4
5
cdef extern from "test.h":
    int test()

def py_test():
    print(test())
1
2
3
4
In [1]: import demo                                                        

In [2]: demo.py_test()                                                     
2333

变量的命名

C 和 Cython 都支持没有参数名的函数声明

1
2
cdef extern from "string.h":
    char* strstr(const char*, const char*)

然而,这样的话 Cython 代码将不能通过关键字参数来调用这个函数,所以,我们最好这样去声明一个函数:

1
2
cdef extern from "string.h":
    char* strstr(const char* src_str, const char* sub_str)

这会让清楚地知道你所调用了哪两个参数,从而能够避免二义性并增强你的代码的可读性:

demo.pyx

1
2
3
4
5
6
7
cdef extern from "string.h":
    char* strstr(const char* src_str, const char* sub_str)

def py_strstr():
    cdef char* data = "hfvcakdfagbcffvschvxcdfgccbcfhvgcsnfxjh"
    pos = strstr(sub_str='akd', src_str=data)
    print(pos != NULL)
1
2
3
4
In [1]: import demo                                                        

In [2]: demo.py_strstr()                                                   
True

将 Python list 传递给 C/C++ 函数

setup.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import numpy
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

extensions = [
    Extension("demo", sources=["demo.pyx", "test.cpp"], extra_compile_args=["-O2"], language="c++"),
]
setup(
    ext_modules = cythonize(extensions)
)

test.h

1
2
3
4
5
6
7
8
#ifndef _TEST_H
#define _TEST_H
#include <vector>
#include <list>

int test(std::list<int> &);

#endif

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include "test.h"
using namespace std;

int test(list<int> &l) {
    for(list<int>::const_iterator i = l.begin(); i != l.end(); i++) {
        printf("%d\n", *i);
    }
    map<int, int> m;
    m[1] = 2333; m[2] = 666;
    int x = max(m[1], m[2]);
    return 0;
}

demo.pyx

1
2
3
4
5
6
7
from libcpp.list cimport list

cdef extern from "test.h":
    int test(list[int] &)

def open_price(list[int] l):
    test(l)
1
2
3
4
5
6
7
In [1]: import demo                                                        

In [3]: demo.open_price([1, 2, 3, 5])                                      
1
2
3
5

list改成vector也是一样的效果

使用 C int 指针

test.h

1
2
3
4
5
6
7
8
#ifndef _TEST_H
#define _TEST_H
#include <vector>
#include <list>

int test(int* a);

#endif

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include "test.h"
using namespace std;

int test(int* a) {
    int ret = a[0];
    map<int, int> m;
    m[1] = 2333; m[2] = 666;
    int x = max(m[1], m[2]);
    return ret;
}

demo.pyx

1
2
3
4
5
6
7
8
from libcpp.list cimport list

cdef extern from "test.h":
    int test(int *a)

def open_price():
    cdef int *a = [2223333, 6, 5]
    print(test(a))
1
2
3
4
In [1]: import demo                                                        

In [2]: demo.open_price()                                                  
2223333

向 C/C++ 传递二维 list

test.h

1
2
3
4
5
6
7
8
#ifndef _TEST_H
#define _TEST_H
#include <vector>
#include <list>

int test(int* a, int n, int m);

#endif

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include "test.h"
using namespace std;

int test(int* a, int n, int m) {
    printf("n = %d, m = %d\n", n, m);
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < m; ++j) {
            printf("%d ", a[i * m + j]);
            sum += a[i * m + j];
        }
    }
    printf("\n");
    int ret = a[0];
    return sum;
}

demo.pyx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from libcpp.list cimport list
from libc.stdlib cimport malloc, free

cdef extern from "test.h":
    int test(int *a, int n, int m)

def open_price(msgs):
    cdef int *a = <int *>malloc(len(msgs[0]) * len(msgs) * sizeof(int))
    n = len(msgs)
    m = len(msgs[0])
    print(n, m)
    for i in range(n):
        for j in range(m):
            a[i * m + j] = msgs[i][j]
    for i in range(n * m):
        print(a[i])
    print(test(a, n, m))
    free(a)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
In [1]: import demo                                                        

In [2]: demo.open_price([[1, 2], [3, 4], [5, 6]])                          
(3, 2)
1
2
3
4
5
6
n = 3, m = 2
1 2 3 4 5 6 
21

也可以直接使用二维vector

test.h

1
2
3
4
5
6
7
8
9
#ifndef _TEST_H
#define _TEST_H
#include <vector>
#include <list>
using namespace std;

int test(vector<vector<int> > &);

#endif

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include "test.h"
using namespace std;

int test(vector<vector<int> > &l) {
    for (int i = 0; i < l.size(); ++i) {
        for (int j = 0; j < l[i].size(); ++j) {
            printf("%d\n", l[i][j]);
        }
    }
    map<int, int> m;
    m[1] = 2333; m[2] = 666;
    int x = max(m[1], m[2]);
    return 0;
}

demo.pyx

1
2
3
4
5
6
7
from libcpp.vector cimport vector

cdef extern from "test.h":
    int test(vector[vector[int]] &)

def open_price(l):
    test(l)
1
python setup.py install
1
2
3
4
5
6
7
8
9
In [1]: import demo                                                        

In [2]: demo.open_price([[1, 2], [3, 4], [5, 7]])                          
1
2
3
4
5
7

使用结构体

test.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#ifndef _TEST_H
#define _TEST_H
#include <vector>
#include <list>

struct Node {
    int a, b;
};

Node test();

#endif

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include "test.h"
using namespace std;

Node test() {
    Node node;
    node.a = 1116;
    node.b = 1917;
    return node;
}

demo.pyx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from libcpp.list cimport list
from libc.stdlib cimport malloc, free

cdef extern from "test.h":
    cdef struct Node:
        int a, b
    Node test()

def open_price(msgs):
    print(test())
1
2
3
4
In [1]: import demo                                                        

In [2]: demo.open_price(0)                                                 
{'a': 1116, 'b': 1917}

使用 C++ STL 中的 map

test.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#ifndef _TEST_H
#define _TEST_H
#include <vector>
#include <list>
#include <map>
using namespace std;

map<int, int> test();

#endif

test.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <cstdio>
#include <map>
#include <vector>
#include <algorithm>
#include "test.h"
using namespace std;

map<int, int> test() {
    map<int, int> m;
    m[1] = 2;
    m[000001] = 930;
    m[300306] = 23333;
    return m;
}

demo.pyx

1
2
3
4
5
6
7
8
from libcpp.map cimport map

cdef extern from "test.h":
    map[int, int] test()

def open_price():
    ans = test()
    print(ans)
1
2
3
4
In [1]: import demo                                                        

In [2]: demo.open_price()                                                  
{1: 930, 300306: 23333}

注意

执行 python setup.py install 时会根据 .pyx 文件的文件名去生成对应的 .c.cpp 文件,所以之前不要有相同文件名的 .c.cpp 文件

调试

当出现错误时,扩展可能会无法运行,这非常糟糕。
静态类型提供了许多优于 Python 的优点,可以让你在编译步骤中捕获很多问题。
而在 Python 中,如果没有严格的测试例程和全面的测试覆盖的情况下,就很难发现这些问题

另一方面,所有的内存管理必须手动操作。并且,错误的内存管理是 C 语言中大多数编程错误的主要原因。
在最好的情况下,这样的错误只会导致一些内存泄漏,这将逐渐消耗掉你所有的环境资源。
最好的情况并不意味着容易处理。
如果没有使用适当的外部工具,如 Valgrind,内存泄漏是非常棘手的。

总之,在大多数情况下,扩展代码中的内存管理问题将在 Python 中导致无法恢复的段错误,并且会导致解释器崩溃,并且不会抛出任何异常。

这意味着你最终需要使用大多数 Python 程序员不需要使用的额外的工具。这增加了开发环境和工作流程的复杂性