安装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
等。
还是通过例子学习。首先我们需要有C/C++的源代码,包括.h
和.cpp
文件;然后需要通过Cython
包装C/C++源代码,即定义好.pxd
和.pyx
文件(这里
.pxd与
.pyx的关系就像
.h与
cpp的关系一样,相互依赖-->头文件声明,源文件定义)
;如下,
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-->str
,var.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标准库distutils
和Cython
库进行,
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。以后用到再仔细研究学习了。