使用cython扩展python库

什么是Cython

Cython 是一种静态编译的编程语言,它结合了 Python 的易用性C语言的高性能,并主要用于加速 Python 程序与 C/C++ 集成。它以一种接近 Python 的语法编写代码,并在编译过程中将其转换为高效的 C 代码,从而提高运行性能。

Cython 的主要用途

  1. 性能优化

    • 用于加速计算密集型任务,如科学计算、数值处理和图像处理。
    • 比如将 Python 循环转换为 C 代码,减少解释器的开销。
  2. 封装 C/C++ 代码

    • 将现有的 C/C++ 库封装成 Python 模块,从而让 Python 程序使用这些高性能的库。
  3. 扩展模块开发

    • 开发自定义的 Python 扩展模块,用于特定的高性能任务。
  4. 高效使用 NumPy

    • 提供对 NumPy 的优化支持,用户可以通过静态类型声明来加速数组运算。

Cython 的特点

  • 通过将 Python 代码转译为 C 代码并编译成共享库(.so 文件, windows为.pyd),可以显著加快代码运行速度,特别是在计算密集型任务中。
  • 可以直接调用 C/C++ 函数和库,也可以将现有 Python 程序与 C/C++ 代码集成。
  • Cython 支持大部分 Python 语法,同时通过类型声明增强性能。
  • 支持现有的 Python 包和库,例如 NumPy,用户可以在其中通过添加类型注解来进一步优化性能。
  • 对于 Python 开发者,学习 Cython 是相对简单的,因为它的语法非常类似于 Python。

Cython 的工作原理

  1. 使用.pxd文件声明接口, 包括python 接口或c/c++接口, 其具体的实现定义在.pyx.c.cpp文件中。
  2. 编写接口实现为.pyx.c.cpp中,Cython(.pyx)代码语法类似于 Python,可以选择性地添加类型声明。
  3. 使用 cythonize 工具将 .pyx 文件转换为 C 代码,并编译为共享库。
  4. 生成的共享库可以像普通 Python 模块一样通过 import 使用。

Cython代码示例

基本步骤:

  1. 编写c/c++代码, 包括.h或.hpp、.c或.cpp文件
  2. 编写pxd文件声明接口, .h文件中的接口, 及.c/cpp中的实现, 或纯声明cython接口。
  3. 编写pyx文件, 导入pxd中的接口进行封装, 此文件中的接口,凡是非cdef声明的函数都可最终被导出,不论是def/cdef/cpdef的类都会被导出。
  4. 利用cythonize工具编译生成so或pyd

pxd文件相当于c/c++中的头文件, 主要用于声明接口及数据类型, 多个pyx文件可导入相同的pxd文件。 另一个作用就是封装c/c++库。

导出C/C++接口

编写.h及.c文件:

// test.h
void helloworld();


// test.c
#include <stdio.h>

void helloworld()
{
    printf("hello world");
}

再编写对应hpp/cpp文件:

// test1.hpp
#include <string>

namespace test{
    std::string hello();
}


// test1.cpp
#include "test1.hpp"


std::string test::hello(){
    return "hello world";
}

test.pxd文件声明, 函数后面添加except +表示捕获异常为python 异常。

from libcpp.string cimport string

# 声明c
cdef extern from "test.c":
    pass

cdef extern from "test.h":
    void helloworld() except +

# 声明c++
cdef extern from "test1.cpp":
    pass

cdef extern from "test1.hpp" namespace "test":
    string hello() except +


# 声明cython, 多个pyx可调用
cdef void quicksort(double[:] arr, int left, int right)

也可以将C++的声明放置到另一个pxd中。

编写对应的pyx文件, 此文件主要起到实现或包装作用:

# distutils: language = c
# cython: language_level=3
import test

# 此接口才会暴露给python
def helloc():
    test.helloworld()

# 此接口才会暴露给python
def hellocpp():
    test.hello()


# quicksort的实现
cdef void quicksort(double[:] arr, int left, int right):
    """
    内部实现快速排序,仅供 Cython 使用
    """
    if left >= right:
        return

    cdef double pivot = arr[left]
    cdef int i = left
    cdef int j = right

    while i < j:
        while i < j and arr[j] >= pivot:
            j -= 1
        arr[i] = arr[j]
        while i < j and arr[i] <= pivot:
            i += 1
        arr[j] = arr[i]

    arr[i] = pivot
    quicksort(arr, left, i - 1)
    quicksort(arr, i + 1, right)


# 此接口才会暴露给python
cpdef sort_array(double[:] arr):
    """
    对数组进行排序,供 Python 调用
    """
    cdef int n = arr.shape[0]
    quicksort(arr, 0, n - 1)

编写对应的setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import sys, os

# c/c++依赖的库目录
library_path = ""
include_path = os.path.join(library_path, "include")
libpath = os.path.join(library_path, "lib")

extensions = [
    Extension("helloworld", ["test.pyx"],
              include_dirs=[include_path],  
              libraries=libraries, 
              library_dirs=[libpath])
]

setup(
    ext_modules=cythonize(extensions)
)

执行命令编译:

python setup.py build_ext --inplace

类型的问题

c/c++中的枚举类型与python中的Enum是不对应的, 与python中的int及bint对应。

其他数据类型对应参考链接Using C++ in Cython

多个pyx编译到一个module中

cython的限制是每定义一个Extension就会生成一个so或者pyd, 所以默认情况下Extension的pyx文件就类似c++中的对外接口hpp文件, 所有的接口都声明在这个文件中。 不支持多个pyx文件。

多个pyx文件只能声明在不同的Extension中。

如果想一个Extension中引用多个pyx文件, 需要将所有pyx文件include "xxx.pyx"到一个pyx中, 然后Extension中仅仅引用包含了所有pyx文件的pyx, 如下所示的all.pyx中包含了所有pyx:

# 1.pyx
def test1():
    pass

# 2.pyx
def test2():
    pass

# all.pyx
include "1.pyx"
include "2.pyx"

编译到指定目录

编译到当前目录:

python setup.py build_ext --inplace

编译到指定目录目录:

python setup.py build_ext -b  /dir/to/pyd

如果想要在打包whl文件时,将cython编译到指定目录, 就需要自定义编译类:

# setup.py
from setuptools.command.build_ext import build_ext

extensions = [
    Extension(...)
]


class CustomBuildExt(build_ext):
    """自定义扩展模块构建类"""
    
    def get_ext_filename(self, ext_name):
        """
        设置pyd安装目录
        """
        return os.path.join("dir", super().get_ext_filename(ext_name))

setup(
    ext_modules=cythonize(extensions),
    cmdclass={
            'build_ext': CustomBuildExt,
        }
)
posted @   汗牛充栋  阅读(55)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示