Python安装、调用扩展模块(python,c/c++)

安装Python模块

distutils, setuptools是python标准库里边的工具包,用于安装扩展模块。后者是前者的增强版,支持便捷安装,但大多C/C++扩展模块的安装还是依赖于distutils来安装。wheel是首先编译出跨平台二进制安装包,发布出去后可以直接安装再系统中,无需在本地重新编译。

下面举例安装my_module.py扩展模块:
my_module.py

import numpy as np
from matplotlib import pyplot as plt

def plot(x, fun):
    plt.figure(figsize=(6, 6))
    plt.plot(x, fun(x))
    plt.title('function show')
    plt.show()

def plot_sin(n=100):
    print(f'Given {n} input points, then plot sin function as following:')
    x = np.array(range(0, n, 1))
    plot(x, np.sin)


if __name__ == '__main__':
    plot_sin(100)

setup.py

#-*-coding: utf-8 -*-

from distutils.core import setup

setup(
    name="my_module", # 目标安装模块名
    version="1.0",
    author="beta",
    author_email="beta@1py.com",
    py_modules=['my_module'], # 原始模块名,无后缀
)

python setup.py __?__参数说明

(1)终端执行python setup.py install

(betak) e:\Program_Library\Python\VS-Python-DLL\sample>python stp.py build install
running build
running build_py
creating build
creating build\lib
copying my_module.py -> build\lib
running install
running install_lib
copying build\lib\my_module.py -> D:\Anaconda3\envs\betak\Lib\site-packages
byte-compiling D:\Anaconda3\envs\betak\Lib\site-packages\my_module.py to my_module.cpython-37.pyc
running install_egg_info
Writing D:\Anaconda3\envs\betak\Lib\site-packages\my_module-1.0-py3.7.egg-info

可以看到my_module.py已经被安装到标准库文件夹里了,之后可以直接from my_module import plot_sin来使用了。
需到标准库"site-package"找到该文件即可删除。注意前面python setup.py install后缀是install

(2)当需要将我们的代码打包发布出去时,使用python setup.py sdist

writing manifest file 'MANIFEST'
creating my_module-1.0
making hard links in my_module-1.0...
hard linking my_module.py -> my_module-1.0
hard linking stp.py -> my_module-1.0
creating dist
Creating tar archive
removing 'my_module-1.0' (and everything under it)

这时候在dist文件夹内就生成了代码项目的tar压缩包。别人下到本地后,进一步安装即可使用。

其他安装参数指令如下,python setup.py --help-commands

Standard commands:
  build            build everything needed to install
  build_py         "build" pure Python modules (copy to build directory)
  build_ext        build C/C++ extensions (compile/link to build directory)
  build_clib       build C/C++ libraries used by Python extensions
  build_scripts    "build" scripts (copy and fixup #! line)
  clean            clean up temporary files from 'build' command
  install          install everything from build directory
  install_lib      install all Python modules (extensions and pure Python)
  install_headers  install C/C++ header files
  install_scripts  install scripts (Python or otherwise)
  install_data     install data files
  sdist            create a source distribution (tarball, zip file, etc.)
  register         register the distribution with the Python package index
  bdist            create a built (binary) distribution
  bdist_dumb       create a "dumb" built distribution
  bdist_rpm        create an RPM distribution
  bdist_wininst    create an executable installer for MS Windows
  check            perform some checks on the package
  upload           upload binary package to PyPI

usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

build是将源代码编译成可调用的链接库。
build_py是编译成纯python的模块。
build_ext是编译C/C++的扩展模块。
install是将源代码编译完之后进一步安装到标准库中。
sdist是将打包待发布的源代码。

setuptools支持更多高级的用法,
比如distutils.core.setup所不支持的develop的安装模式,它会编译并且在适当的位置安装包,然后添加一个简单的链接到python site-packages文件夹中,可以使用显式的-u选项删除包,

python setup.py develop
python setup.py develop -u

使用该方式比其他方式安装包更好一些。maskrcnn-benchmark就是使用这种安装方式,方便开发过程中直接修改源代码(而不用跑到标准库里,或者重新编译、安装)。

setuptools.setup基本使用方法与distutils.core.setup类似,不赘述,用到再查看文档
而安装C/C++扩展更多用的是distutils库,后文学习。

Python调用C/C++扩展模块

在Python中调用C/C++的扩展模块,基本原理是通过第三方工具(比如Cython, Cffi,SWIG,Numba,PyBind11和Boost等库工具)将C/C++的类、方法等包装成支持Python类、方法的接口,然后再编译成类似于动态链接库的库文件(对比windows下的.dll,Python一般是.pyd等,linux下一般都是.so)。

一般在C/C++中通过第三方工具如PyBing11,Boost等库,或者直接调用#include <Python.h>来包装C/C++的源代码;然后编译生成动态链接库,gcc/g++或MSVC等编译器(或者使用Cython来包装C/C++,然后通过distutils使用Cython编译器编译);再在python中直接通过import module调用(module.pyd),或者通过第三方工具包调用,如ctypes等。

官方simple demo演示

还是通过例子学习。首先我们需要有C/C++的源代码,包括.h.cpp文件;然后需要通过Cython包装C/C++源代码,即定义好.pxd.pyx文件(这里.pxd.pyx的关系就像.hcpp的关系一样,相互依赖-->头文件声明,源文件定义);如下,
cpluspy.h

实现一个类(C/C++扩展到Cython的类必须有一个默认空构造器!)和两个函数,第一个做标量线性乘法运算,第二个做向量乘法运算。

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#include <vector>
#include <cassert>

using namespace std;

class CplusA {
public:
	CplusA():text("NULL") {}; 
	CplusA(string t):text(t) {};
	~CplusA() {};
	void show_text();
	double multiply(double a, double b);
private:
	string text;
};

double linear_scalar(double w, double x);

vector<double> linear_vector(vector<double> w, vector<double> x);


cpluspy.cpp

#include "cpluspy.h"


void CplusA::show_text(){
    std::cout << "CplusA.text=" << CplusA::text << endl;
    // std::cout << "CplusA.text=" << CplusA::text << endl;
}

double CplusA::multiply(double a, double b){
    return a * b;
}

double linear_scalar(double w, double x){
    return w * x;
}

vector<double> linear_vector(vector<double> w, vector<double> x){
    vector<double> y;
    int num_w = w.size(), num_x = x.size();
    if(num_w != num_x){
        std::cout << "The size of weight vector and input vector must be same!" << std::endl;
        system("pause");
        exit(EXIT_FAILURE);
    }
    for(int i=0; i < num_w; ++i){
        y.emplace_back(w[i] * x[i]);
    }
    return y;
}

接下来定义Cython头文件和源文件,因为在编译C/C++的Cython扩展时会根据.pyx自动生成一个C/C++的源文件(.c.cpp),该文件的名字和.pyx一样(前缀),所以在定义Cython源文件(.pyx)时需要与C/C++的源文件(.cpp)不一样的名字(头文件无所谓)。

cpluspy_cython.pxd

C/C++的标准库已经被Cython封装到libc, libcpp库里了。
这里类的声明是将.h文件里声明的类重新在Cython中声明,名字一定要一样(且好像只能访问公有成员?)。
类的声明需要关键字,cdef cppclass MyClass:,以及构造器等公有成员。
其他的在.h里的成员直接重新声明就好,和C/C++基本一致的语法。
涉及到C/C++代码块的地方,变量在使用之前要声明类型。
Cython支持泛型编程,C++的<>在这里变成[]

from libcpp.vector cimport vector
from libcpp.string cimport string

cdef extern from "cpluspy.h":
    cdef cppclass CplusA:
        
        CplusA() except + # 如果不定义except +,那么若是在C/C++构造器初始化过程出现异常,python是不会捕获到的。
        CplusA(string) except + 

        void show_text()
        double multiply(double a, double b)
        
    double linear_scalar(double w, double x)
    vector[double] linear_vector(vector[double] w, vector[double] x)

cpluspy_cython.pyx

值得注意的是,C/C++,Cython和Python的字符编码方式不一样,C/C++:char, char*, string, byte, Python:str, bytes,Cython作为第三方库,就是桥接两种语言之间的数据类型传输和转换。
C/C++中的char*, string在Cython中为bytes类型,而Python默认的字符变量类型为str,在传输时需要编解码。
将一个类型为str的变量var编码为bytes类型,var.encode("utf-8");反过来,bytes-->strvar.decode("utf-8")
或者在初始化变量时,str--> var = "hello"; bytes--> var = b"hello"
也可以在Cython中强制转换数据类型,cdef bytes new_s = bytes(old_s, encoding = "utf-8")
具体还得参考官方API文档,c++ string 与python string

# distutils: language = c++
from libcpp.string cimport string

from Rectangle cimport Rectangle
from cpluspy_cython cimport CplusA, linear_scalar, linear_vector

cdef class PyCplusA:
    cdef CplusA pycplus_a

    def __cinit__(self, str s):
        # cdef bytes enc_s = bytes(s, encoding = "utf8")
        self.pycplus_a = CplusA(s.encode("utf8"))

    def multiply(self, double a, double b):
        return self.pycplus_a.multiply(a, b)

    def show_text(self):
        self.pycplus_a.show_text()
    
def py_linear_scalar(double w, double x):
    return linear_scalar(w, x)

def py_linear_vector(vector[double] w, vector[double] x):
    return linear_vector(w, x)


def run_test():
    cls_a = PyCplusA(s="Hello?")
    print("multiply(5, 3)=%.0f"%cls_a.multiply(5, 3))
    cls_a.show_text()
    print("py_linear_scalar(5., 3.)=%.0f"%py_linear_scalar(5., 3.))
    print("py_linear_vector\n([1., 2., 3., 4., 5.], \n[5., 4., 3., 2., 1.]):")
    print(py_linear_vector([1., 2., 3., 4., 5.], [5., 4., 3., 2., 1.]))

接下来,该编译安装了。通过Python标准库distutilsCython库进行,
setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

ext_modules = [Extension(name="cpluspy_cython", sources=["cpluspy_cython.pyx", "cpluspy.cpp"], language="c++")]

setup(name="cpluspy_cython", 
    version="0.1",
    description="a demo of dist setup c/c++ extensions.",
    py_modules=["my_module"], # 本地有一个无关的`my_module.py`文件,也一并安装了
    ext_modules=cythonize(ext_modules),
    )

安装指令python setup.py build_ext --inplace
编译完成时,会在自动生成build文件夹和cpluspy_cython.cpp文件;
整个安装过程顺利结束时,会生成一个cpluspy_cython.cp37-win_amd64.pyd文件,Linux下为.so文件,作为编程语言可以直接调用的动态链接库。
在Python中导入cpluspy_cython并使用,

import cpluspy_cython
from cpluspy_cython import *

print(dir(cpluspy_cython)) # 查看该模块包含哪些属性,方法
run_test() 

测试输出:

In [3]: print(dir(cpluspy_cython))
['PyCplusA', '__builtins__', '__doc__', '__file__', '__loader__', '__name__', '__package__', 
'__spec__', '__test__', 'py_linear_scalar', 'py_linear_vector', 'run_test']

In [4]: run_test()
multiply(5, 3)=15
CplusA.text=Hello?
py_linear_scalar(5., 3.)=15
py_linear_vector
([1., 2., 3., 4., 5.],
[5., 4., 3., 2., 1.]):
[5.0, 8.0, 9.0, 8.0, 5.0]

调用第三方库来重新包装并在两种语言之间进行数据类型的传输和转换,大概就是扩展语言的难点了,必须对两种语言和第三方库足够掌握!

每个开发工具库都有自己的数据类型和方法,比如要将ITK源代码封装传给Python调用,又要怎么做呢?这就不像前面的简单例子,只在C/C++和Python的标准库里进行数据传输转换,ITK里有自己的数据类型和方法,是不是先转换为C/C++标准库的数据类型,然后再通过第三方库处理?还是只提供操作接口,不涉及传输数据,比如只是通过Python调用方法就行,方法不返回特定库的数据?或者说,通过Python调用C/C++编译生成的可执行文件.exe(只需要Python传入参数并运行),会否更简单?

那么什么时候Python需要用到Cython或者别的扩展呢?

Cython扩展的应用场景主要是加速数组运算(Numpy就是Cython的一个得意库),也就是在遇到计算密集型操作的时候,考虑用Cython扩展C/C++库来加速Python的计算速度;除了计算密集型操作外,对应的还有IO密集型操作,比如数据的传输,这时候考虑多线程多进程之类的方法更合适。

至此,大概了解了Cython调用扩展的基本情况,很神奇很实用的一个东东,Cython。以后用到再仔细研究学习了。

参考资料

  1. Cython官方学习文档
  2. Cython官方学习文档-中文
  3. Cython的简单使用
  4. Python Documentation
  5. python打包工具distutils、setuptools分析
  6. c++ string 与python string
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值