如何在go中执行python3代码

go-python3使用指南与踩坑记录

简介

​ 在开发工作中有时候会有需要和其他语言进行交互的需求,笔者前段时间就接到了需要在go中调用python3的需求,这种需求笔者想到了两种解法,一是直接在代码中调用python3,二是使用shell命令执行python3脚本。在本文中主要介绍了在go中使用go-python3这个库调用python3的一些实践历程与踩坑经历。关于使用脚本在另一篇文章:https://blog.csdn.net/LuciferMS/article/details/121888491

环境搭建

​ github上有现成的go调用python3开源框架,https://github.com/DataDog/go-python3,值得注意的是目前这个库只支持python3.7,笔者开始的时候电脑上装的是python3.8,所以在这个上面花费了好多时间,不过这也给了我一个教训,就是使用一个开源库的时候一定要好好看他的readme.md,留意一下有什么值得注意的地方。

在这里插入图片描述

​ 使用go get github.com/DataDog/go-python3就可以获取这个库到你的项目中,但是也有可能会报错,如果是pkg_config的问题可以在电脑上安装一下这个依赖,mac上安装:brew install pkg_config ,再试一次,问题就迎刃而解了。

程序demo

​ 笔者的程序目录结构是这样的:
在这里插入图片描述

​ 下面我们来看这个库的具体使用demo:

1.go代码 test_python3.go

package main

import (
   "fmt"
   "github.com/DataDog/go-python3"
   "log"
   "os"
)

func ImportModule(dir, name string) *python3.PyObject {
   sysModule := python3.PyImport_ImportModule("sys")
   path := sysModule.GetAttrString("path")
   pathStr, _ := pythonRepr(path)
   log.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)
   log.Println("after add path is " + pathStr)
   return python3.PyImport_ImportModule(name)
}

func main(){
   python3.Py_Initialize()
   if !python3.Py_IsInitialized() {
      fmt.Println("Error initializing the python interpreter")
      os.Exit(1)
   }
   path, _ := os.Getwd()
   helloPy := ImportModule(path + "/py3", "test_python3")
   if helloPy == nil {
      log.Fatalf("helloPy is nil")
      return
   }
   helloFunc := helloPy.GetAttrString("test_print_name")
   if helloFunc == nil {
      log.Fatalf("helloFunc is nil")
   }
   var args = python3.PyTuple_New(1)
   python3.PyTuple_SetItem(args, 0, python3.PyUnicode_FromString("python3"))
   helloPy3Str := helloFunc.Call(args, python3.Py_None)
   if helloPy3Str == nil {
      log.Fatalf("helloPy3Str is nil")
   }
   funcResultStr, _ := pythonRepr(helloPy3Str)
   log.Println("func result: " + funcResultStr)
}

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
}

2.python代码test_python3.py

import os

from test_import_py import f
from test_import_py import x

# import cython
def test_print_name(name):
    print(os.getcwd())
    y = f(1.1)
    print(x)
    return y

3.python&cython代码

import cython

x = 3

@cython.cfunc
@cython.exceptval(-2, check=True)
def f(x: cython.double) -> cython.double:
    return x ** 2 - x

程序运行结果

在这里插入图片描述

程序测试点

  1. 如何引入执行一个python方法?
  2. 在python代码中引入别的模块会不会有问题
  3. 在python程序中写Cython会不会有问题

结果解析

  1. python3.Py_Initialize() 调用这个方法可以初始化python3的执行环境,只有执行了这个方法才可以运行相关的python3操作,值得注意的是这个方法既没有返回值,也不能重复初始化,重复初始化会报错。这个方法没有返回值意味着我们没法从这个方法中感知它的执行过程是否有问题,但是他也提供了python3.Py_IsInitialized() 这个方法来让我们知道python3的环境是否初始化完成。这两个方法要一起使用。

  2. 添加搜索路径 ,我们要往sys.path中添加我们写的python代码所在的目录作为搜索路径,添加进去之后,文件之中的代码才可以作为一个模块被import进来。值得注意的是,当我们import一个文件的时候,会把这个py脚本的代码执行一边,如py代码中的print("hello")就被执行了

  3. 执行代码,其中有一些api和值得关注的点如下:

    • PyImport_ImportModule:引入一个模块,通俗的理解就是传入一个文件名(前提是这个文件名所在的目录已经被引入到搜索路径下面,也就是sys.path下面),值得注意的是,如果这个被引入的文件路径不对或者不存在,这个方法返回的是nil,并不会报错,如果报错了,那应该是我们引入的文件中的python代码存在着问题,笔者遇到过一个坑,就是python代码中import的一些依赖并没有在代码中有使用,然后就报错了
    • GetAttrString:根据名字获取模块中的一个变量,返回的是一个pythonObject,比如我们再a.py中定义了一个demo = 1 那么这个时候我们a.GetAttrString(“demo”)就获取到了这个变量了,当然这里也可以获取一个方法。
    • Call:调用一个方法,值得注意的是这里的传参是一个tuple,如果不是tuple,Call方法会返回nil。还有一点值得关注的地方是,这里存在着一个并发的问题,如果一个函数在Call执行的过程中,再次被调用,此时python环境就会crash,感觉这个应该是python的原因,python的GIL机制,不能支持真正的并发运行代码。这个demo会在后面贴出。
    • pythonRepr,封装的代码,将一个python对象转化为字符串。

并发调用方法的坑

test_concurrency.go

package main

import (
   "fmt"
   "github.com/DataDog/go-python3"
   "log"
   "os"
)

func main()  {
   python3.Py_Initialize()
   if !python3.Py_IsInitialized() {
      fmt.Println("Error initializing the python interpreter")
      os.Exit(1)
   }
   sysModule := python3.PyImport_ImportModule("sys")
   path := sysModule.GetAttrString("path")
   //pathStr, _ := pythonRepr2(path)
   //log.Println("before add path is " + pathStr)
   python3.PyList_Insert(path, 0, python3.PyUnicode_FromString(""))
   python3.PyList_Insert(path, 0, python3.PyUnicode_FromString("./py3"))
   concurrencyFile := python3.PyImport_ImportModule("test_concurrency")
   if concurrencyFile == nil {
      log.Fatalf("concurrency is nil")
      return
   }
   testFunc := concurrencyFile.GetAttrString("test_func")
   if testFunc == nil {
      log.Fatalf("testFunc is nil")
      return
   }
   go func() {
      testFunc.Call(python3.Py_None, python3.Py_None)
      for  {

      }
   }()
  	//time.Sleep(10 * time.Second)
   go func() {
      testFunc.Call(python3.Py_None, python3.Py_None)
      for  {

      }
   }()
   select {

   }
}

test_concurrency.py

import time

def test_func():
    print("hello")
    time.sleep(10)
  • 这个程序的大概意思就是一个python方法耗时是10s(执行的时候人为的sleep 10s),然后在go中有两个协程并发的去调用这个方法,这个程序的执行结果就是python crash,

  • 解决方法1: 但是如果将注释中的代码也加入其中,也就是说在两个协程之间加入11s的沉睡,这样两个协程对这个func的调用就是串行的了,这样就不会有问题。

  • 解决方法2:

    将go代码改成下面这个样子,改动点在代码中已标出

    package main
    
    import (
       "fmt"
       "github.com/DataDog/go-python3"
       "log"
       "os"
    )
    
    func main()  {
       python3.Py_Initialize()
       if !python3.Py_IsInitialized() {
          fmt.Println("Error initializing the python interpreter")
          os.Exit(1)
       }
       sysModule := python3.PyImport_ImportModule("sys")
       path := sysModule.GetAttrString("path")
       python3.PyList_Insert(path, 0, python3.PyUnicode_FromString(""))
       python3.PyList_Insert(path, 0, python3.PyUnicode_FromString("./py3"))
       concurrencyFile := python3.PyImport_ImportModule("test_concurrency")
       if concurrencyFile == nil {
          log.Fatalf("concurrency is nil")
          return
       }
       testFunc := concurrencyFile.GetAttrString("test_func")
       if testFunc == nil {
          log.Fatalf("testFunc is nil")
          return
       }
       go func() {
          ensure := python3.PyGILState_Ensure() //获取python GIL(全局解释器)状态 0:释放 1:占用
          fmt.Println(ensure)
          testFunc.Call(python3.Py_None, python3.Py_None)
          for  {
    
          }
       }()
      go func() {
      	for {
      		if  python3.PyGILState_Ensure() == python3.PyGILState(0) {
      			testFunc.Call(python3.Py_None, python3.Py_None)
      			break
      		} else {
    
      		}
      	}
      	for  {
    
      	}
      }()
       select {
    
       }
    }
    

上面代码的改动主要是添加了对python GIL状态的判断,python由于又全局解释器的存在,所以不能并行执行多线程的代码,这个并发的问题应该也是由于GIL的存在才会导致的。笔者翻阅了Cython的API文档,找到了这个方法可以获取GIL的状态,这个问题才得以解决,但是感觉这也不是最优的方案,因为这样代码还是串行执行的,后面再探究一下有没有优化的空间吧。

总结

​ 其实现在回头看来,这段探索历程还是很有意思的,在工作中能有这样的体验当然也是相当棒的。
​ 话说回来,笔者也不是很懂python,所以一些东西都是摸着石头过河,很多结论都带有一丝猜测在里面,有不对支持还请读者多多提点。

参考

  • python GIL: https://www.cnblogs.com/traditional/p/13289905.html
  • Cython的Api文档: https://docs.python.org/zh-cn/3/c-api/index.html
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
下面是一个简单的 Python 贪吃蛇代码: ```python import turtle import time import random delay = 0.1 # 设置屏幕 wn = turtle.Screen() wn.title("贪吃蛇游戏") wn.bgcolor("green") wn.setup(width=600, height=600) wn.tracer(0) # 创建蛇头 head = turtle.Turtle() head.speed(0) head.shape("square") head.color("black") head.penup() head.goto(0, 0) head.direction = "stop" # 创建食物 food = turtle.Turtle() food.speed(0) food.shape("circle") food.color("red") food.penup() food.goto(0, 100) # 创建身体列表 segments = [] # 创建分数 score = 0 high_score = 0 pen = turtle.Turtle() pen.speed(0) pen.shape("square") pen.color("white") pen.penup() pen.hideturtle() pen.goto(0, 260) pen.write("分数: 0 最高分: 0", align="center", font=("Courier", 24, "normal")) # 移动函数 def go_up(): if head.direction != "down": head.direction = "up" def go_down(): if head.direction != "up": head.direction = "down" def go_left(): if head.direction != "right": head.direction = "left" def go_right(): if head.direction != "left": head.direction = "right" def move(): if head.direction == "up": y = head.ycor() head.sety(y + 20) if head.direction == "down": y = head.ycor() head.sety(y - 20) if head.direction == "left": x = head.xcor() head.setx(x - 20) if head.direction == "right": x = head.xcor() head.setx(x + 20) # 设置键盘绑定 wn.listen() wn.onkeypress(go_up, "Up") wn.onkeypress(go_down, "Down") wn.onkeypress(go_left, "Left") wn.onkeypress(go_right, "Right") # 循环游戏 while True: wn.update() # 检查是否碰到边界 if head.xcor()>290 or head.xcor()<-290 or head.ycor()>290 or head.ycor()<-290: time.sleep(1) head.goto(0,0) head.direction = "stop" # 隐藏身体 for segment in segments: segment.goto(1000, 1000) # 清空身体列表 segments.clear() # 重置分数 score = 0 # 重置延迟 delay = 0.1 pen.clear() pen.write("分数: {} 最高分: {}".format(score, high_score), align="center", font=("Courier", 24, "normal")) # 检查是否吃到食物 if head.distance(food) < 20: # 移动食物到随机位置 x = random.randint(-290, 290) y = random.randint(-290, 290) food.goto(x, y) # 添加新的身体 new_segment = turtle.Turtle() new_segment.speed(0) new_segment.shape("square") new_segment.color("grey") new_segment.penup() segments.append(new_segment) # 缩短延迟 delay -= 0.001 # 增加分数 score += 10 if score > high_score: high_score = score pen.clear() pen.write("分数: {} 最高分: {}".format(score, high_score), align="center", font=("Courier", 24, "normal")) # 移动身体 for index in range(len(segments)-1, 0, -1): x = segments[index-1].xcor() y = segments[index-1].ycor() segments[index].goto(x, y) if len(segments) > 0: x = head.xcor() y = head.ycor() segments[0].goto(x, y) move() # 检查是否碰到身体 for segment in segments: if segment.distance(head) < 20: time.sleep(1) head.goto(0,0) head.direction = "stop" # 隐藏身体 for segment in segments: segment.goto(1000, 1000) # 清空身体列表 segments.clear() # 重置分数 score = 0 # 重置延迟 delay = 0.1 pen.clear() pen.write("分数: {} 最高分: {}".format(score, high_score), align="center", font=("Courier", 24, "normal")) time.sleep(delay) wn.mainloop() ``` 你可以将代码保存为 `snake.py` 文件并在命令行运行。注意需要先安装 turtle 库。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值