最近接触了一个web项目,后端是用golang写的,由于需要将网页的数据传入给深度学习模型,也就是说要在go和python之间进行交互。在一番search之后决定用go-python3这个包来实现。
先放上go-python3的地址: https://github.com/DataDog/go-python3
注意:go-pyhton3只支持python3.7,所以要事先确保python的环境要符合要求。
之后就需要安装go-python3这个包,使用
go get github.com/DataDog/go-python3
准备工作就绪之后,就开始上代码了,代码主要分为三个function。
1.PyMain()
func PyMain(db, user, pass, host, port, table, q string) (string, error){
path, _ := os.Getwd()
startPy := ImportModule(path + "/nl2sql/code", "start")
if startPy == nil {
return "", fmt.Errorf("PyModule is nil")
}
mainFunc := startPy.GetAttrString("main")
if mainFunc == nil {
fmt.Println("mainFunc is nil")
}
var args = python3.PyTuple_New(7)
python3.PyTuple_SetItem(args, 0, python3.PyUnicode_FromString(db))
python3.PyTuple_SetItem(args, 1, python3.PyUnicode_FromString(user))
python3.PyTuple_SetItem(args, 2, python3.PyUnicode_FromString(pass))
python3.PyTuple_SetItem(args, 3, python3.PyUnicode_FromString(host))
python3.PyTuple_SetItem(args, 4, python3.PyUnicode_FromString(port))
python3.PyTuple_SetItem(args, 5, python3.PyUnicode_FromString(table))
python3.PyTuple_SetItem(args, 6, python3.PyUnicode_FromString(q))
state := python3.PyEval_SaveThread() // 释放全局解释器锁并保存线程状态,使得当前线程可以释放GIL(全局解释器锁)并继续运行。
defer python3.PyEval_RestoreThread(state) //恢复线程状态,即重新获取全局解释器锁
wg := sync.WaitGroup{} //用于等待一组go协程执行完毕,它会阻塞等待,直到计数器值归0
wg.Add(1)
ch := make(chan string)
go func(){
defer wg.Done() // 当协程执行完毕,将计数器减一
_gstate := python3.PyGILState_Ensure() // 获取GIL,如果当前线程已经拥有GIL,则直接返回;如果没有,则会阻塞等待获取GIL,并返回GIL状态信息。
defer python3.PyGILState_Release(_gstate) // 释放之前调用Ensure获得的全局解释器所。
mainStr := mainFunc.Call(args, python3.Py_None)
if mainStr == nil {
err := python3.PyErr_Occurred()
if err != nil {
errStr, _ := pythonRepr(err)
fmt.Println("Error:", errStr)
ch <- ""
return
}
fmt.Println("mainStr is nil")
ch <- ""
return
}
funcResultStr, _ := pythonRepr(mainStr)
fmt.Println("funcResultStr:",funcResultStr)
ch <- funcResultStr
}()
resultStr := <-ch
wg.Wait() // 阻塞等待,直到计数器归零
fmt.Println("resultStr:",resultStr)
if resultStr == "" {
return "", fmt.Errorf("Error occurred while processing the query")
}
return resultStr, nil
}
2.ImportModule
func ImportModule(dir, name string) *python3.PyObject {
sysModule := python3.PyImport_ImportModule("sys")
path := sysModule.GetAttrString("path")
pathStr, _ := pythonRepr(path)
fmt.Println("before add path is " + pathStr)
python3.PyList_Insert(path, 0, python3.PyUnicode_FromString(""))
python3.PyList_Insert(path, 0, python3.PyUnicode_FromString(dir))
pathStr, _ = pythonRepr(path)
fmt.Println("after add path is " + pathStr)
return python3.PyImport_ImportModule(name)
}
3.pythonRepr
func pythonRepr(o *python3.PyObject) (string, error) {
if o == nil {
return "", fmt.Errorf("object is nil")
}
s := o.Repr()
if s == nil {
python3.PyErr_Clear()
return "", fmt.Errorf("failed to call Repr object method")
}
defer s.DecRef()
return python3.PyUnicode_AsUTF8(s), nil
}
先忽略掉我在PyMain中传入的参数以及它们的意义,我们聚焦于go-python3的整体流程。整体的思路就是先获取到.py文件,再获取到.py文件中想要调用的方法,最后调用方法返回结果。在代码中我们定义了ImportModule用来导入模块,
startPy := ImportModule(path + “/nl2sql/code”, “start”)
其中,start为py文件名,也就是说我想要交互的文件为start.py。值得注意的是前面传入的路径一定要正确。
在ImportModule中有一个方法python3.PyImport_ImportModule,用于导入模块,它返回一个新的引用对象,该对象引用已加载到的模块,如果模块已经被导入,则返回该模块对象的引用。
定义的pythonRepr方法,该方法将指向PyObject的指针作为参数。它使用Repr()方法返回对象的字符串表示。如果对象为nil,则返回一个错误。python3.PyUnicode_AsUTF8用于将Repr()返回的Unicode对象转换为UTF-8编码的字符串。值得注意的是DecRef(),用于递减对象的引用计数。
介绍完定义的两个方法后,继续回到我们的go-python3流程中,当获取到start.py模块后,再获取py模块中的方法函数
mainFunc := startPy.GetAttrString(“main”)
我需要调用start.py中的main函数(是自己定义的 def main()),GetAttrString()函数获取start模块中的main属性。
接下来就是执行这个方法函数,但是有可能我们需要传入参数
var args = python3.PyTuple_New(7)
python3.PyTuple_SetItem(args, 0, python3.PyUnicode_FromString(db))
python3.PyTuple_SetItem(args, 1, python3.PyUnicode_FromString(user))
python3.PyTuple_SetItem(args, 2, python3.PyUnicode_FromString(pass))
python3.PyTuple_SetItem(args, 3, python3.PyUnicode_FromString(host))
python3.PyTuple_SetItem(args, 4, python3.PyUnicode_FromString(port))
python3.PyTuple_SetItem(args, 5, python3.PyUnicode_FromString(table))
python3.PyTuple_SetItem(args, 6, python3.PyUnicode_FromString(q))
python3.PyTuple_New(n) 用来创建一个python元组对象,包含n个元素。
python3.PyTuple_SetItem() 将n个字符串对象添加到元组中,这些字符串对象是使用python3.PyUnicode_FromString() 函数创建的,它将C字符串转换为Python Unicode对象。如果我们调用的方法不需要传入参数,那么可以不用args,直接mainStr := mainFunc.Call(python3.Py_None)。Call() 用来执行方法并得到返回值。再将返回值传入pythonRepr得到字符串。由于我在web项目的main.go中进行的python解释器的初始化和关闭,所以上面代码中没有展示出,具体如下:
package main
import (
"github.com/DataDog/go-python3"
"os"
"fmt"
)
func init() {
python3.Py_Initialize()
if !python3.Py_IsInitialized() {
fmt.Println("Error initializing the python interpreter")
os.Exit(1)
}
}
func main() {
defer python3.Py_Finalize()
}
至此,go-python3调用python代码中的方法已经实现,但是可以看到在PyMain中有一些还没提及到的代码,那么它们具体的作用又是什么呢?
因为在实际使用中,我们很大几率会多次调用python代码,于是这其中就需要面对解释器全局锁(GIL)和线程。
state := python3.PyEval_SaveThread() // 释放全局解释器锁并保存线程状态,使得当前线程可以释放GIL(全局解释器锁)并继续运行。
defer python3.PyEval_RestoreThread(state) //恢复线程状态,即重新获取全局解释器锁
这两行代码是成对出现的,GIL是CPython使用的一种机制,用于确保每次只有一个线程执行Python字节码,这是非常必要的,因为CPython的内存管理不是线程安全的。
另外在代码中可以看到我们用go func创建了一个协程,关于为何要用协程,我看到的一个解释是在调用Call() 方法时,存在着一个并发的问题,如果一个函数在Call执行的过程中,再次被调用,此时python环境就会crash。于是我们特意将Call操作放在协程内,协程如果需要传输数据就要通过通道,于是我们创建了ch := make(chan string)
_gstate := python3.PyGILState_Ensure() // 获取GIL,如果当前线程已经拥有GIL,则直接返回;如果没有,则会阻塞等待获取GIL,并返回GIL状态信息。
defer python3.PyGILState_Release(_gstate) // 释放之前调用Ensure获得的全局解释器所。
这两行代码在协程中也是成对出现的。
最后来做一个总结,go-python3目前能找到的使用文章不多,在使用过程中也花了不少心思,虽然有CPython API文档,但是由于版本的更迭,许多方法不能使用,例如我打算创建一个解释器的连接池,和一些创建解释器的方法都被取消掉了,如果读者们想要更进一步使用go-python3,需要在文档方面多下些功夫了。对了,还有一个坑需要注意,那就是在部署好环境之后,如果调用go-python3返回的各项数据为nil,那么就需要考虑是python方面的问题,我们需要检查python代码中不要有没有使用到的import的包,另外读者们可以先在终端直接运行python的代码,看能否正常运行,在通过go-python3调用。