go编写python模块(go 语言开发 Python 扩展)

1. 什么是python拓展模块

学习go语言编写Python扩展,可以参考官方文档:

  • https://golang.org/cmd/cgo/
  • https://docs.python.org/3/c-api/index.html

Python 为主导的项目中引入 Go 有以下几种方式:

  • 将 Go 源文件编译成动态库,然后直接通过 Python 的 ctypes 模块调用
  • 将 Go 源文件编译成动态库或者静态库,再结合 Cython 生成对应的 Python 扩展模块,然后直接 import 即可
  • 将 Go 源文件直接编译成 Python 扩展模块,当然这要求在使用 CGO 的时候需要遵循 Python 提供的 C API

Python拓展模块是用其它语言(通常是C/C++)编写,并可以在Python中导入和使用的模块。

2. go 语言开发 Python 扩展思路

拓展模块的开发步骤是:

  1. 用C/C++编写模块内容,并导出Python可调用的函数。
  2. 编写模块初始化函数,如:
PyMODINIT_FUNC PyInit_example(void) 
{
    // ...
}
  1. 使用Python C API定义模块的方法表、类型对象等。
    // example.c
PyMODINIT_FUNC PyInit_example(void) 
{
    static PyMethodDef methods[] = {
        {"add",  add, METH_VARARGS, "Add two numbers"},
        {NULL, NULL, 0, NULL}
    };

    static struct PyModuleDef moduledef = {
        PyModuleDef_HEAD_INIT,
        "example",      /* module name */ 
        NULL,           /* docstring */
        -1,             /* size of per-interpreter state */ 
        methods 
    };

    return PyModule_Create(&moduledef);
}

static PyObject* add(PyObject* self, PyObject* args) 
{
    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
        return NULL;
    }
    return PyLong_FromLong(a + b);
}
  1. 编译成动态库(.so文件或.pyd文件)。
  2. 在Python中导入和使用这个模块。
import example

example.add(1, 2)  # Returns 3

调用add方法,它实际上会执行C代码中的add函数。

使用go原理类似:

  1. 编写go源代码,使用cgo编译生成共享库。cgo可以调用C语言的函数,所以go代码可以调用Python C API与Python进行交互。
  2. 导入C包,用于cgo调用Python C API

3. go编写python2拓展模块 示例

使用python.go和foo.go实现的简单Python扩展示例:

python.go:

package pymodule

/*
#include <Python.h>
*/
import "C"
import "unsafe"

//export InitModule
func InitModule() {
	C.Py_InitModule("pymodule", Methods)
}

var Methods = []*C.PyMethodDef{
	{"say_hello", (C.PyCFunction)(C.PyCFunctionWithKeywords)(C.PyCFunctionCast)(unsafe.Pointer(C.SayHello)), METH_O, ""},
}

普通的 Go 只是多了一句 import “C”,除此之外没有任何和 CGO 相关的代码,也没有调用 CGO 的相关函数。但是由于这个 import,会使得 go build 命令在编译和链接阶段启动 gcc 编译器。

C 代码要通过注释的形式写在 import “C” 这行语句上方(中间不可以有空格,这是规定)。而一旦导入,就可以通过 C 这个名字空间进行调用,比如这里的 C.puts、C.CString 等等。

注意: import “C”,它不是导入一个名为 C 的包,我们可以将其理解为一个名字空间,C 语言的所有类型、函数等等都可以通过这个名字空间去调用。

编译之前,我们需要定义一些环境变量,让编译器知道在哪找Python.h
当使用 cgo 编译 go 代码生成共享库时,需要找到 Python 的头文件、库文件和 pkg-config 信息。通过设置这三个环境变量,go 的 cgo 可以找到 Python 2 的相关文件,完成 Python 扩展的编译。

如果不设置这些环境变量,cgo 在编译时很可能找不到 Python 的依赖,导致编译失败。

  1. PKG_CONFIG_PATH: 指定 pkg-config 工具搜索 *.pc 文件的路径。这里指定了 Python 的 pkg-config 文件路径。
  2. LD_LIBRARY_PATH: 指定动态链接库搜索路径。这里指定了 Python 的库文件路径。
  3. C_INCLUDE_PATH: 指定 C/C++ 头文件搜索路径。这里添加了 Python的 include 目录。

下面配置很重要:

export PKG_CONFIG_PATH=/home/test/env/python2/lib/pkgconfig/
export LD_LIBRARY_PATH=/home/test/env/python2/lib/

export C_INCLUDE_PATH=/home/test/env/python2/include/python2.7

foo.go:

package pymodule

//export SayHello
func SayHello(name *C.char) *C.char {
   return C.CString("Hello, " + C.GoString(name) + "!") 
}

编译

go build -buildmode=c-shared -ldflags '-s -w' -o pymodule.so python.go  foo.go

在Python中使用:

import pymodule

pymodule.say_hello('John')  # Prints "Hello, John!"
  1. 在python.go中定义了InitModule函数来初始化模块,并定义了方法列表Methods
  2. 在foo.go中定义并导出SayHello函数
  3. 编译两个文件生成共享库pymodule.so
  4. 在Python中导入pymodule模块,调用say_hello方法
  5. say_hello方法会调用foo.go中的SayHello函数
  6. SayHello函数接收来自Python的name,并返回拼接的问候语
    所以,这个示例通过cgo实现了一个简单的Python扩展,导出一个say_hello方法,用于向指定的人问好。

4. go编写python3拓展模块 示例

Python 2和Python 3的C API在许多方面都发生了变化,导致同样的C代码在Python 2和3环境下编译结果不同。

Python 2和Python 3的一些主要C API变化包括:

  1. Py_InitModule被PyModule_Create替代
  2. PyArg_ParseTuple的格式字符串发生变化。
  3. Py_BuildValue的格式字符串发生变化。
  4. 其他许多C API也发生较大变化。
package main

/*
#cgo pkg-config: python3 
#define Py_LIMITED_API
#include <Python.h>

static PyObject *sayHello(PyObject *self, PyObject *args) {
    return PyUnicode_FromString("Hello from C!");
}

static PyMethodDef FooMethods[] = {
    {"sayHello",  sayHello, METH_NOARGS, "Say hello"},
    {NULL, NULL, 0, NULL}  
};

static struct PyModuleDef hellomodule = {
   PyModuleDef_HEAD_INIT,
   "hello",  
   NULL,  
   -1, 
   NULL
};

PyMODINIT_FUNC PyInit_hello()
{
    PyModuleDef_Init(&hellomodule);
    hellomodule.m_methods = FooMethods;
    PyObject *m = PyModule_Create(&hellomodule);
    return m;
}
*/
import "C"

func main() {}

注释这部分代码实际上是C语言,只不过它调用了Go导出的sayHello函数。
编译时cgo会解析C注释,并使用其中的实现生成Python扩展。

  1. 定义sayHello方法,返回"Hello from C!"。
  2. 定义FooMethods方法数组,指定sayHello方法。
  3. 定义hellomodule模块。
  4. 实现PyInit_hello初始化函数,设置m_methods并创建模块。

这段代码使用了Python C API中的几个重要函数:

  1. PyUnicode_FromString:构造一个Python字符串对象。我们使用它来构造"Hello from C!"返回值。
  2. PyArg_ParseTuple:解析Python函数的参数。这里我们没有使用它,因为sayHello是无参数方法。
  3. Py_BuildValue:构造Python返回值。我们也没有直接使用它,而是使用PyUnicode_FromString构造字符串对象。
  4. PyMethodDef:定义方法信息,我们使用它来定义sayHello方法信息。
  5. PyModuleDef:定义模块信息,我们使用它来定义hello模块信息
  6. PyModuleDef_Init:初始化PyModuleDef实例。
  7. PyModule_Create:创建一个模块,传入PyModuleDef实例。
  8. PyMODINIT_FUNC:定义模块初始化函数返回类型。我们使用它来定义PyInit_hello的返回类型。

在Python中:

import hello

hello.sayHello()  # Prints "Hello from C!"

代码优化

将C代码写在注释中有一定的局限性:

  1. 只能写很简短的代码,不利于开发较复杂的扩展。
  2. 没有自动完成功能,编写不方便。
  3. 调试困难,不能使用Go的调试工具。

5. python拓展模块什么时候加载

Python中,扩展模块是在导入时加载的。具体 load 的过程是:

  1. 当我们在 Python 中导入一个扩展模块时,Python 会搜索名为 module.so(Linux) 或 module.pyd(Windows)的文件。
  2. 如果找到该文件,Python 会加载它,并执行里面的初始化函数。对于我们的例子,这个函数是 PyInit_hello()。
  3. 这个初始化函数会返回一个模块对象。Python 使用这个对象来初始化一个模块,并将它插入 sys.modules,这样在后续的导入中可以重用该模块。
  4. Python 中可以使用该模块,并调用它暴露的方法。

查找拓展模块so文件位置的顺序

当在 Python 中导入一个扩展模块时,Python 会按照以下顺序搜索名为 module.so 的文件:

  1. 当前目录。Python 会先搜索当前路径下的 module.so 文件。
  2. PYTHONPATH 路径。PYTHONPATH 环境变量指定的路径下搜索 module.so 文件。
  3. 构建路径。如果是从源码编译的 Python,会搜索构建路径下的 module.so 文件。
  4. 动态链接库路径。搜索系统的动态链接库路径,如 Linux 下的 /usr/lib 等。
  5. 创建 module.so 软链接。如果上述路径下存在 module.so 的软链接,Python 也会进行搜索。
  6. 报 ImportError。如果在上述所有路径下都没有找到 module.so 文件,Python 会抛出 ImportError。
    所以我们可以通过:
  7. 将 module.so 文件置于当前路径或者 PYTHONPATH 下。
  8. 创建 module.so 到上述路径的软链接。
  9. 安装 module.so 文件到系统的动态链接库路径下。
  10. 在源码编译 Python 时指定 module.so 路径。
    来确保 Python 可以成功导入该扩展模块。

6. go和c数据结构转化对应关系

  1. 基本类型:
    | Go | C |
    | ---------- | -------------- |
    | byte | char |
    | rune | wchar_t |
    | int | int |
    | uint | unsigned int |
    | int32 | int32_t |
    | uint32 | uint32_t |
    | int64 | int64_t |
    | uint64 | uint64_t |
    | float32 | float |
    | float64 | double |
    | complex64 | float complex |
    | complex128 | double complex |

  2. 字符串:
    Go中的字符串被转化为C中的char*。在C中操作字符串时需要注意长度和空终止等。

  3. 切片:
    Go中的切片被转化为C中的结构体:
    c
    struct {
    void *data;
    int len;
    int cap;
    }
    data指向切片的底层数组,len是切片的长度,cap是容量。

  4. 结构体:
    Go中的结构体会被“扁平化”转化为C,丢失结构信息。取而代之的是对应的C类型的变量。如果要在C中重构这个结构,需要定义与之对应的C结构体。

  5. 接口:
    Go中的接口在转化为C后会丢失,需要通过其他机制来表示,如函数指针等。

  6. 通道:
    Go中的通道无法直接在C中表示。可以在Go中以通道来发送/接受数据,在C中以其他方式来模拟通道的效果。

  7. 函数:
    在C/Go之间转化时,需要使用//export和cgo的调用约定来定义可以相互调用的函数。函数的参数与返回值也需要满足两种语言的类型兼容要求。

7. python C 拓展模块常用api

Python C API的官方文档地址:https://docs.python.org/3/c-api/

Python语言本身是用C语言实现的。而Python C API则是在C语言中嵌入和扩展Python的接口。

Python C API包含一组在C语言中使用的宏、函数和类型等,用于创建Python的拓展模块。借助该API,我们可以在C语言中嵌入Python的解释器,调用Python的函数与对象,实现C语言与Python之间的数据交换与操作。

Python C API用于创建拓展模块,它包含许多函数,常用的一些如下:

  1. Py_Initialize(): 初始化Python解释器,必须调用
    当使用cgo调用Python C API时,cgo会自动在背后调用Py_Initialize()进行初始化。所以在我们的C代码中不需要显式调用。
    如果不使用cgo,而是直接使用Python C API,那么需要在代码开始处调用Py_Initialize(),例如:
int main() {
    Py_Initialize();
    // ...
    Py_Finalize();
}

如果不使用cgo,需要在开始和结束处分别调用Py_Initialize()和Py_Finalize()。
Py_Initialize()会初始化Python解释器,设置信号处理函数等。而Py_Finalize()会正确关闭Python解释器,释放资源。

cgo是Go语言调用C语言函数的接口。它允许我们在Go源码中直接嵌入C代码,并在两种语言之间传递数据。

cgo会在背后自动调用一些初始化函数,如Py_Initialize(),这是因为:
当我们在Go代码中调用Python C API时,Python的解释器需要被初始化,否则无法工作。所以cgo会在我们的C代码被执行前自动调用Py_Initialize()进行初始化。

  1. Py_Finalize(): 退出Python解释器,可选调用。

  2. Py_BuildValue(): 创建Python对象,根据格式字符串决定对象类型。如"i"->int,“s”->str等。

  3. PyArg_ParseTuple(): 从Python传递过来的参数中提取数据,填充C变量。如"si"可以提取int和str。

  4. PyModule_Create(): 创建Python模块。 python3 使用这个!

  5. PyModule_AddObject(): 将对象添加到模块中。

  6. PyObject_CallObject(): 调用一个Python对象(如函数)。

  7. PyCallable_Check(): 检查一个对象是否可调用。

  8. PyErr_Format(): 设置一个异常。

  9. Py_None: None对象,用于返回None值。

  10. PyTuple_Pack(): 创建元组对象。

  11. PyDict_SetItemString(): 将一个值加入字典。

  12. PyString_FromString(): 创建字符串对象。

  13. PyInt_FromLong(): 从long创建int对象。

  14. PyModule_GetDict(): 获取模块的字典。

8. python调用so go函数原理

我们的so模块是通过cgo生成的,它依赖Go语言代码。所以Python调用so模块时,实际上会触发Go语言代码的执行。

具体过程是:

  1. 我们使用Go语言编写cgo代码,如sayHello函数,然后编译生成so模块。
  2. 在Python中导入这个so模块,并调用其函数,如sayHello。
  3. Python调用sayHello时,会触发so模块中cgo生成的包装函数执行。
  4. 这个包装函数又会调用我们编写的sayHello Go函数。所以实际执行是在Go语言层。
  5. 我们的sayHello函数运行在一个goroutine中,这个goroutine就是Go进程。
  6. Go进程会随着我们的Go程序一直运行,直到程序退出。
  7. 我们需要在Go程序退出前调用C.Py_Finalize()关闭解释器。

加入我们的go的main函数是空的, func main() {},理论调用sayHello整个生命周期就结束了,还需要调C.Py_Finalize()关闭解释器吗?

由于我们的main函数是空的,理论上调用sayHello函数后,相应的Go进程应该就结束了。

但是,事实并非如此。这是因为:

  1. 假如我们在sayHello函数中,我们启动了一个goroutine执行时间consuming的操作(如睡眠)。
  2. 由于sayHello函数是cgo导出的,会转换为C语言风格。在C语言中不存在goroutine的概念。
  3. 所以,尽管我们在sayHello中启动了goroutine,但在cgo转换为C函数后,这个语义会丢失。
  4. 结果是,goroutine实际上启动了一个独立的Go进程,但sayHello函数无法等待它结束就返回了。
  5. 这样,调用sayHello后的Python程序会结束,但之前启动的Go进程却在默默运行,导致资源泄漏。

总结:所以这种情况,还是看你代码写的有没有问题。

为什么我们即使在示例代码的main函数中调用C.Py_Finalize()也无法解决资源泄露?

在Go的main函数中调用C.Py_Finalize(),理论上它应该可以结束sayHello中启动的goroutine。

但是,当Python加载我们的so库并调用sayHello函数时,会重新触发Go代码执行,启动一个全新的Go进程。而此时Go的main函数已经退出,无法再调用C.Py_Finalize()。所以新启动的Go进程会一直运行,导致资源泄漏。

main函数只在编译so库时执行一次,用于初始化我们的Go代码。
当Python实际调用sayHello函数时,会重新触发我们的Go代码执行,启动一个全新的Go进程。
此时,这个新进程与我们编译时执行的main函数毫无关系。main函数已经退出,无法再执行任何操作。

所以,当Python调用sayHello函数时:

  1. 我们的Go代码会重新执行,但与编译时执行的main函数无关。
  2. 这个新进程无法调用main函数中定义的任何函数或变量。main函数所在的上下文已经消失。
  3. 只有全局变量的状态会保留在so库中,新进程可以访问。但main函数本身不会再执行。
  4. 所以,在这个新进程中,无法再调用C.Py_Finalize()。它必须在Python程序的入口函数中调用,与Go代码无关。

要彻底理解这个问题,需要明确:
5. 生成so库的过程与执行go代码的关系。编译时会执行一次main函数,用于初始化
6. Python加载so库与调用cgo函数的机制。会重新触发go代码执行,启动一个全新的进程。
7. 进程的特征与上下文的概念。每个进程都有自己的上下文,无法共享main函数的上下文。
8. Cgo函数的执行过程。真正执行cgo函数的是一个全新的Go进程,与编译时无关。

  1. sayHello函数自身不产生资源泄漏,及时回收所有资源。
  2. 在Python代码中调用C.Py_Finalize(),在程序结束前关闭解释器。
06-06 15:46