go-python3初体验

最近接触了一个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调用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值