Cython封装C++代码给python使用

672 阅读5分钟

在Python开发中,有时候需要使用Cython等工具将C++代码封装起来并在Python中使用。这样做有以下几个好处:

  1. 性能提升: C++通常比Python更高效。通过将C++代码封装为Python扩展模块,可以在Python中调用底层C++代码,从而获得更高的执行速度。这对于需要处理大量数据或者对性能要求较高的任务尤为重要。

  2. 现有库和功能的重用: C++拥有丰富的第三方库和功能,有时候我们希望在Python项目中重用这些库。通过将C++代码封装为Python模块,我们可以直接在Python中使用这些功能,而不需要重新实现它们。

  3. 访问底层系统资源: C++可以更容易地访问底层系统资源,如操作系统API、硬件驱动程序等。通过将C++代码封装为Python模块,我们可以在Python中调用这些底层资源,从而实现更高级别的功能。

  4. 增加代码保护性: 通过将C++代码封装为Python模块,我们可以将核心算法和实现细节隐藏起来,只暴露必要的接口给Python开发人员使用。这样可以增加代码的保护性,防止源代码被意外泄露或者滥用。

Python的线程功能在某些情况下可能效率较低。Python中的线程是通过全局解释器锁(Global Interpreter Lock,GIL)实现的,它会在任意时刻只允许一个线程执行Python字节码。这意味着在多线程环境下,Python的线程不能充分利用多核处理器的优势,因为同一时刻只有一个线程在执行。

相比之下,C++对多线程的支持更加直接和灵活。C++标准库提供了线程、互斥量、条件变量等多线程相关的类和函数,可以更细粒度地控制线程的创建、同步和通信。C++没有全局解释器锁,因此多个C++线程可以在多核处理器上并行执行,从而实现更高的并发性能。

下面简易举个例子,通过C++代码封装一个CsvSave类,此类功能可以开启一个线程专门保存数据到CSV文件中,从而提高python程序效率

开发环境 windows-x64 VS2022 Python 3.10.11 需要安装python库 pip install cython

项目目录 include C++的头文件,src CPP代码,python python相关文件 image.png

#ifndef CSVSAVER_H
#define CSVSAVER_H

#include <fstream>
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <queue>
#include <atomic>
#include <string>

class CsvSaver
{
public:
    CsvSaver();
    CsvSaver(const std::string &filename);
    ~CsvSaver();

    void add_data(const std::vector<std::string> &data);
    bool Init(const std::string &filename);
private:

    void stop();
    void run();
    void write_row(std::ofstream &file, const std::vector<std::string> &row);

    std::string filename_;                            // CSV文件名
    std::thread thread_;                              // CSV文件保存线程
    std::ofstream file_;                              // CSV文件
    std::vector<std::vector<std::string>> buffer_;    // 数据缓冲区
    std::queue<std::vector<std::string>> data_queue_; // 数据队列
    std::mutex mu_;                                   // 互斥量
    std::condition_variable cv_;                      // 条件变量
    std::atomic<bool> stopped_;                       // 线程停止标志
};

#endif // CSVSAVER_H

CPP代码

#include "CsvSaver.h"

#ifdef _WIN32
const std::string kCsvLineEnding = "\r\n"; // 定义CSV行结束符(Windows)
#else
const std::string kCsvLineEnding = "\n"; // 定义CSV行结束符(非Windows)
#endif

static int kBufferSize = 1024;    // 缓冲区大小
static int kFlushInterval = 1000; // 写入时间间隔

CsvSaver::CsvSaver() : stopped_(true)
{
}

CsvSaver::CsvSaver(const std::string &filename) : filename_(filename), stopped_(false)
{
    file_.open(filename, std::ios::out | std::ios::app);
    if (!file_.is_open())
    {
        std::cerr << "Failed to open file: " << filename << std::endl;
        return;
    }
    thread_ = std::thread(&CsvSaver::run, this);
}

CsvSaver::~CsvSaver()
{
    stop();
    if (file_.is_open())
    {
        file_.flush();
        file_.close();
    }
}
bool CsvSaver::Init(const std::string &filename)
{
    if(stopped_){
        stopped_ = true;
    }else{
        std::cerr << "CsvSaver no need Init" << std::endl;
        return false;
    }
    file_.open(filename, std::ios::out | std::ios::app);
    if (!file_.is_open())
    {
        std::cerr << "Failed to open file: " << filename << std::endl;
        return false;
    }
    thread_ = std::thread(&CsvSaver::run, this);
    return true;
}
void CsvSaver::add_data(const std::vector<std::string> &data)
{
    std::unique_lock<std::mutex> lock(mu_);
    data_queue_.push(data);
    if (buffer_.size() >= kBufferSize)
    {
        cv_.notify_one();
    }
}

void CsvSaver::stop()
{
    {
        std::unique_lock<std::mutex> lock(mu_);
        stopped_ = true;
        cv_.notify_one();
    }
    thread_.join();
    while (!data_queue_.empty())
    {
        auto frontData = std::move(data_queue_.front());
        data_queue_.pop();
        write_row(file_, frontData);
    }
}

void CsvSaver::run()
{
    auto last_flush_time = std::chrono::steady_clock::now();
    while (!stopped_)
    {
        std::unique_lock<std::mutex> lock(mu_);
        cv_.wait_for(lock, std::chrono::milliseconds(100));

        if (data_queue_.empty())
        {
            continue;
        }

        auto now = std::chrono::steady_clock::now();
        auto elapsed_time = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_flush_time);

        if (data_queue_.size() >= kBufferSize || elapsed_time.count() >= kFlushInterval || stopped_)
        {
            while (!data_queue_.empty())
            {
                auto frontData = std::move(data_queue_.front());
                data_queue_.pop();
                write_row(file_, frontData);
            }
            last_flush_time = std::chrono::steady_clock::now();
        }
    }
}

void CsvSaver::write_row(std::ofstream &file, const std::vector<std::string> &row)
{
    for (size_t i = 0; i < row.size(); ++i)
    {
        file << row[i];
        if (i < row.size() - 1) {
            file << ",";
        }
    }
    file << kCsvLineEnding;
}

写了一个MyClass类,最终要提供到python使用的C++API接口

// MyClass.h

// C++类的声明和定义
#include "CsvSaver.h"
class MyClass
{
public:
    MyClass(); // 构造函数
    ~MyClass();
    bool initFunction(std::string filepath);
    void saveData(const std::vector<std::string> &strings);

private:
    CsvSaver *m_CsvSaver;
};
#include "MyClass.h"
#include <iostream>
using namespace std;
MyClass::MyClass()
{
    // 构造函数的实现
}
MyClass::~MyClass()
{
    if (m_CsvSaver)
    {
        delete m_CsvSaver;
        m_CsvSaver = nullptr;
    }
}

bool MyClass::initFunction(std::string filepath)
{
    // 类的功能函数的实现
    cout << "C++ InitFunction\n";

    cout << "filepath:" << filepath << "\n";
    m_CsvSaver = new CsvSaver(filepath);
    return true;
}

void MyClass::saveData(const std::vector<std::string> &strings)
{
    m_CsvSaver->add_data(strings);
}

python目录

  • setup.py
  • myclass_wrapper.pyx
  • test.py

myclass_wrapper.pyx将C++API接口封装python接口.同时C++数据类型,要用对应python数据类型转化

# myclass_wrapper.pyx
# cython: language_level=3
import ctypes
from libcpp.vector cimport vector
from libcpp.string cimport string

cdef extern from "MyClass.h":
    cdef cppclass MyClass:
        MyClass() except +
        int initFunction(string filePath)
        void saveData(const vector[string]& strings)
        
cdef class PyMyClass:
    cdef MyClass *thisptr

    def __cinit__(self):
        self.thisptr = new MyClass()

    def __dealloc__(self):
        del self.thisptr

    def initFunction(self, filePath, ip, port) -> int:
        #decode方法将C++字符串解码为Python字符串
        #encode方法将Python字符串编码为C++字符串
        return self.thisptr.initFunction(filePath.encode(), ip.encode(), port)

    def saveData(self, string_list):
        cdef vector[string] cpp_strings
        for s in string_list:
            cpp_strings.push_back(s.encode())
        self.thisptr.saveData(cpp_strings)

setup.py 用于管理cython构建文件的来源,类似CMakeLists和makefile

  • include_dirs 头文件目录,在python上层的include中
  • sources cpp文件目录 myclass_wrapper.pyx文件
  • extra_compile_args 编译标准参数
  • language 语言
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

extensions = [
    Extension(
        "myclass_wrapper",
        sources=["myclass_wrapper.pyx", "../src/MyClass.cpp","../src/CsvSaver.cpp","../src/UDPClient.cpp"],
        language="c++",
        extra_compile_args=["/std:c++latest"],
        include_dirs=["../include"],
        #如果要链接其他库
        #libraries=["my_library"],  # 添加要链接的库的名称
        #library_dirs=["/path/to/library"], # 添加库文件的路径
    )
]

setup(
    ext_modules=cythonize(extensions)
)

python测试文件

import myclass_wrapper
import ctypes
# 创建 MyClass 实例并调用其方法
my_object = myclass_wrapper.PyMyClass()

#初始化CSV保存
my_object.initFunction("data.csv")

strings = ["Hello", "World"]
my_object.saveData(strings)  # 将整个列表传递给 saveData 方法
data_list = []  # 创建一个空列表

while True:
        data_buffer = '12345'
        data_list.append(data_buffer)  # 将接收到的数据添加到列表中
        if len(data_list) >= 10:  # 判断 data_list 的长度是否达到指定阈值
            my_object.saveData(data_list)  # 将整个列表传递给 saveData 方法
            print(data_list)
            data_list.clear()  # 清空 data_list

运行说明 在python目录下输入

python setup.py build_ext --inplace
python .\test.py

注意事项

  1. python setup.py build_ext --inplace
  2. 此时会编译生成myclass_wrapper.cp310-win_amd64.pyd,myclass_wrapper.cpp
  3. 这里取决你setup.py起的名字和python版本以及C++VS编译器MSVC,我的版本时3.10.11所有名字带有310
  4. 如果代码有错误,这里编译就会提示,建议MyClass类和CsvSaver类的功能先用C++编译测试后再封装python,
  5. 如果有错误,基本就是myclass_wrapper.pyx,封装python api问题了
  6. 如果没问题再运行python test.py测试例子

运行效果

image.png