Python开发效率极高,生态系统异常强大,各种功能包下载即用,在AI领域大放异彩。
然而,Python作为解释型编程语言,运行效率低下是其最大短板。为了兼顾应用的开发效率和运行性能,通常使用其它高级语言(C、C++、Rust等)编写性能敏感的功能,然后再Python中进行调用。
这就是所谓的Python扩展,目前很多三方包都采用扩展的方式进行开发。
Rust(Pyo3)
前提
全局安装maturin工具:uv tool install maturin
创建Rust库项目
1 cargo new rust_end --lib
1 2 3 4 5 6 7 8 9 10 11 12 13 [package] edition = "2024" name = "rust_end" version = "0.1.0" [lib] crate-type = ["cdylib" ][dependencies] pyo3 = {version = "0.29" , features = ["extension-module" ]}[package.metadata.maturin] bindings = "pyo3"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [build-system] build-backend = "maturin" requires = ["maturin>=1.7,<2.0" ][project] description = "pyo3 test demo for testpypi" keywords = ["rust" , "pyo3" , "maturin" ]license = "MIT" name = "rust_end" readme = "README.md" requires-python = ">=3.8" version = "0.1.0" [tool.maturin] bindings = "pyo3" manylinux = "2_28" universal2 = true zig = true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 use pyo3::{ Bound, PyResult, Python, exceptions::PyValueError, prelude::*, pyclass, pyfunction, pymodule, types::PyModule, }; #[pyfunction] fn add (a: i64 , b: i64 ) -> i64 { a + b } #[pyfunction] fn div (a: f64 , b: f64 ) -> PyResult<f64 > { if b == 0.0 { return Err (PyValueError::new_err ("divide by zero" )); } Ok (a / b) } #[pyclass] struct Counter { count: u64 , } #[pymethods] impl Counter { #[new] #[pyo3(signature = (init = None))] fn new (init: Option <u64 >) -> Self { Counter { count: init.unwrap_or (0 ), } } fn inc (&mut self , step: u64 ) { self .count += step; } fn get (&self ) -> u64 { self .count } fn reset (&mut self ) { self .count = 0 ; } } #[pymodule] fn rust_end (_py: Python<'_ >, m: &Bound<'_ , PyModule>) -> PyResult<()> { m.add_function (pyo3::wrap_pyfunction!(add, m)?)?; m.add_function (pyo3::wrap_pyfunction!(div, m)?)?; m.add_class::<Counter>()?; Ok (()) }
创建Python测试项目
1 uv init python_end --no-package
1 2 uv sync & .venv\Scripts\activate
运行以下命令后,Rust项目会自动编译,并在Python项目中安装这个扩展库
1 maturin develop --release
回到python_end项目编写测试代码main.py
1 2 3 4 5 6 7 8 9 import rust_end def main(): print(f"12+23={rust_end.add(12, 23)}") print(f"12/23={rust_end.div(12, 23)}") print(f"12/0={rust_end.div(12, 0)}") if __name__ == "__main__": main()
发布测试
打包wheel文件:maturin build --release --strip
在Rust项目中安装:uv pip install ..\rust_end\target\wheels\rust_end-0.1.0-cp312-cp312-win_amd64.whl
发布到Test-PyPI:maturin publish --repository testpypi
在Python项目中安装:uv pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple rust_end
nanobind / pybind11
接下来演示如何使用nanobind(pybind11的升级版)将既有C++ SDK包装成Python扩展,这在很多场景下非常有用。
前提
安装nanobind:pip install nanobind
C++动态链接库项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #pragma once #include <string> #include <vector> #if defined(_MSC_VER) #ifdef MATHKIT_EXPORTS #define MATHKIT_API __declspec(dllexport) #else #define MATHKIT_API __declspec(dllimport) #endif #else #define MATHKIT_API #endif namespace mathkit {enum class Op { Add, Sub };class MATHKIT_API Calculator {public : double last_result = 0.0 ; double apply (Op op, double a, double b) ; double reset (double to = 0.0 ) ; }; MATHKIT_API double dot_product (const std::vector<double >& a, const std::vector<double >& b) ;MATHKIT_API std::string greet (const std::string& name) ;}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include "mathkit.h" #include <stdexcept> namespace mathkit {double Calculator::apply (Op op, double a, double b) { switch (op) { case Op::Add: last_result = a + b; break ; case Op::Sub: last_result = a - b; break ; } return last_result; } double Calculator::reset (double to) { last_result = to; return last_result; } double dot_product (const std::vector<double >& a, const std::vector<double >& b) { if (a.size () != b.size ()) { throw std::length_error ("dot_product: vector size mismatch" ); } double s = 0.0 ; for (size_t i = 0 ; i < a.size (); ++i) s += a[i] * b[i]; return s; } std::string greet (const std::string& name) { if (name.empty ()) { throw std::invalid_argument ("greet: name must not be empty" ); } return "Hello, " + name + "! (from C++)" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 cmake_minimum_required (VERSION 3.18 ...4.0 )project (mathkit LANGUAGES CXX)set (CMAKE_CXX_STANDARD 17 )set (CMAKE_CXX_STANDARD_REQUIRED ON )set (CMAKE_CXX_EXTENSIONS OFF )if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set (CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif ()add_library (mathkit SHARED src/mathkit.cpp ) target_include_directories (mathkit PUBLIC src)target_compile_definitions (mathkit PRIVATE MATHKIT_EXPORTS)if (MSVC) target_compile_options (mathkit PRIVATE /utf-8 /W3) endif ()
1 2 3 4 mkdir build cd buildcmake .. -G "Visual Studio 17 2022" -A x64 cmake --build . --config Release
包装项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <nanobind/nanobind.h> #include <nanobind/stl/string.h> #include <nanobind/stl/vector.h> #include "mathkit.h" namespace nb = nanobind;using namespace mathkit;NB_MODULE (mathkit_wrapper, m) { nb::enum_<Op>(m, "Op" ) .value ("Add" , Op::Add) .value ("Sub" , Op::Sub); nb::class_<Calculator>(m, "Calculator" ) .def (nb::init<>()) .def_rw ("last_result" , &Calculator::last_result) .def ("apply" , &Calculator::apply, nb::arg ("op" ), nb::arg ("a" ), nb::arg ("b" )) .def ("reset" , &Calculator::reset, nb::arg ("to" ) = 0.0 ); m.def ("dot_product" , &dot_product, nb::arg ("a" ), nb::arg ("b" )); m.def ("greet" , &greet, nb::arg ("name" )); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 cmake_minimum_required (VERSION 3.18 ...4.0 )project (mathkit_wrapper LANGUAGES CXX)set (CMAKE_CXX_STANDARD 17 )set (CMAKE_CXX_STANDARD_REQUIRED ON )set (CMAKE_CXX_EXTENSIONS OFF )find_package (Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED)execute_process ( COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir OUTPUT_VARIABLE nanobind_ROOT OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY) find_package (nanobind CONFIG REQUIRED)nanobind_add_module(mathkit_wrapper src/mathkit_wrapper.cpp ) target_include_directories (mathkit_wrapper PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/sdk/include" ) target_link_libraries (mathkit_wrapper PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/sdk/lib/win64/mathkit.lib" ) if (MSVC) target_compile_options (mathkit_wrapper PRIVATE /utf-8 /W3) endif ()add_custom_command (TARGET mathkit_wrapper POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/sdk/lib/win64/mathkit.dll" "$<TARGET_FILE_DIR:mathkit_wrapper>" COMMENT "Copying mathkit.dll next to the extension module" )
创建Python测试项目(包装项目的编译依赖特定的Python环境,并且需要在该环境中安装nanobind包)
创建项目:uv init python_test --no-pakage
安装nanobind:uv pip install nanobind
安装虚拟环境:uv sync
激活虚拟环境:& .\.venv\Scripts\activate
编译
1 2 3 4 mkdir build cd buildcmake .. -G "Visual Studio 17 2022" -A x64 cmake --build . --config Release
Python测试项目
将mathkit.dll和生成的pyd文件拷贝到测试项目根目录下
main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import osimport sys_HERE = os.path.dirname(os.path.abspath(__file__)) _WRAPPER_DIR = os.path.normpath( os.path.join(_HERE, "." , "Lib" ) ) sys.path.insert(0 , _WRAPPER_DIR) _dll_handle = os.add_dll_directory(_WRAPPER_DIR) import mathkit_wrapper as mkprint (mk.greet("独立项目" ))print ("dot_product([1,2],[3,4]) =" , mk.dot_product([1.0 , 2.0 ], [3.0 , 4.0 ]))c = mk.Calculator() print ("add(1.5, 2.5) =" , c.apply(mk.Op.Add, 1.5 , 2.5 ))print ("last_result =" , c.last_result)