使用 Golang 和 Docker 运行 Python 代码

本篇文章聊聊如何使用 Golang 来运行 Python 代码,用 Python 现成软件包来偷个懒儿,来少写一些代码。

写在前面

最近折腾了一些“陈年项目”,不少都是使用 Python 实现的。而我在折腾的项目的代码主要是使用 Golang 实现的。改写这些项目中的基础逻辑并不麻烦,借助 ChatGPT ,都是分分钟的事情。

但是有一些项目依赖的 Python 软件包,却让我为了难:

  1. Go 官方没有提供功能相等的,测试完备的替代包。
  2. 开源社区没有实现功能相近的软件包,或者实现的程序缺乏测试保障。
  3. 重新从零到一实现,意味着大量的时间消耗,尤其是具备大量测试用例的 Python 项目,比如:https://github.com/derek73/python-nameparser

作为一个有追求的工程师,我们首先需要排除掉使用 os/exec 这类方式,丑陋(不可靠、不稳定)的使用 Shell 来执行 Python 代码。

完整代码开源在 soulteary/docker-python-in-go,你可以自取。

在折腾之前,我们先聊聊原理和场景限制。

实现原理和场景限制

2018 年 11 月,DataDog 团队借鉴社区成名已久的 sbinet/go-python 项目,创建了 DataDog/go-python3 项目,提供了 Go 语言和 CPython-3 API 的绑定。

但可惜的是,在 2021 年 12 月 1 日,DataDog 团队宣布存档项目。值得庆幸的是,官方宣布项目交由 go-python/cpy3 继续维护。

不过,随着 Python 的版本迭代和变动,项目陷入了困境:

  • Python 3.8 中,需要调整 Python 源码实现,移除 PyEval_ReInitThreads 函数,才能够正常工作。
  • Python 3.9 之后,Python C API 中更是移除了 PyDict_ClearFreeList 的接口支持,导致项目不能继续兼容运行。

所以,如果我们愿意调整 Python 源码,那么我们可以使用 3.8 版本的 Python,否则方案就只能在 3.7 版本的 Python 运行。

除了不同版本 Python 本身的“接口”限制之外,还有一个硬件相关的限制。我们身边越来越多同学购置了 M1 / M2 芯片的 Mac ,但是 Python 3.8 之前,支持 ARM 芯片,会出现比较多的编译问题。除了 Python 之外,Golang 1.17.7 及之前的版本也对 M1 / M2 芯片存在兼容性问题。

所以,如果我们需要支持 M1 / M2 的设备,那么我们需要使用社区维护者调整过源码的 Python 3.8,以及比较新版本的 Golang。

使用 Docker 解决上面的环境依赖问题

在 2023 年,许多系统、软件都产生了非常多的变化。如果我们按照网上的方式来,可能会遇到这样或者那样的问题。好在,我们还有一条简单可靠的路:Docker。

虽然,社区维护者 Christian Korneck 提供了一些例子,但其实在容器里无论是安装 Python 还是 Golang,都会引入不必要的额外变量。

我们有更好的方案,直接基于 Python 和 Golang 的官方提供的镜像,来制作构建环境和运行环境,让 Docker 容器既小巧又可靠。

编程实战

好了,前置的相关知识,到这里就了解的差不多了。下面,我们来聊聊如何折腾它。

准备 Python 程序

我们以前文中提到的 Python 软件包 derek73/python-nameparser 为例,编写一个简单的 Python 程序片段,能够“简单快速的解析人名”。

from nameparser import HumanName

print(HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)").as_dict())

将上面的代码保存为 app.py,然后使用 python app.py 执行这个程序,验证程序能够正常运行。

# python app.py

{
    
    'title': 'Dr.', 'first': 'Juan', 'middle': 'Q. Xavier', 'last': 'de la Vega', 'suffix': 'III', 'nickname': 'Doc Vega'}

程序准备完毕之后,我们来完成 Golang 部分的实现。

实现 Golang 程序

Golang 的程序实现也不复杂,我们可以将上面的代码直接 HardCode 到 Go 里,或者使用 osio 包里的函数,来读取我们的 Python 程序,大概 20 行内就能解决战斗。

package main

import (
	python3 "github.com/go-python/cpy3"
)

func main() {
    
    
	defer python3.Py_Finalize()
	python3.Py_Initialize()

	code := `
from nameparser import HumanName

print(HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)").as_dict())
`
	python3.PyRun_SimpleString(code)
}

这部分代码的备份,你在 soulteary/docker-python-in-go/app/main.go 可以找到。

使用 Docker 完成程序构建

这里,我们先来实现一个最简的 Docker 配置:

# Base Images
FROM golang:1.20.4-alpine3.18 AS go-builder
FROM python:3.7-alpine3.18 AS builder
# Base Builder Env
COPY --from=go-builder /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
RUN python -m pip install --upgrade pip
RUN apk add build-base pkgconfig
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
ENV CGO_ENABLED=1
# Copy source code
COPY app /app
WORKDIR /app
# Install deps
RUN pip install nameparser && \
    go mod init human-name && \
    go mod tidy
# Build the binary
RUN go build -o HumanName

# Run Image
FROM python:3.7-alpine3.18
# Copy Python Deps
COPY --from=builder /usr/local/lib/python3.7/site-packages/nameparser /usr/local/lib/python3.7/site-packages/nameparser
# Copy binary
COPY --from=builder /app/HumanName /HumanName
CMD ["/HumanName"]

将上面的内容保存为 Dockerfile,然后使用下面的命令构建镜像:

docker build -t soulteary/python-in-golang .

镜像构建过程中,我们将看到类似下面的日志:

# docker build -t soulteary/python-in-golang .

[+] Building 2.5s (18/18) FINISHED                                                                                                 
 => [internal] load build definition from Dockerfile                                                                          0.0s
 => => transferring dockerfile: 37B                                                                                           0.0s
 => [internal] load .dockerignore                                                                                             0.0s
 => => transferring context: 2B                                                                                               0.0s
 => [internal] load metadata for docker.io/library/python:3.7-alpine3.18                                                      2.4s
 => [internal] load metadata for docker.io/library/golang:1.20.4-alpine3.18                                                   1.5s
 => [auth] library/python:pull token for registry-1.docker.io                                                                 0.0s
 => [builder 1/8] FROM docker.io/library/python:3.7-alpine3.18@sha256:f48c5f6a8a22a73558ea93eb26d2c7928d23f2acb2bb9270be9a08  0.0s
 => [go-builder 1/1] FROM docker.io/library/golang:1.20.4-alpine3.18@sha256:ee2f23f1a612da71b8a4cd78fec827f1e67b0a8546a98d25  0.0s
 => [internal] load build context                                                                                             0.0s
 => => transferring context: 56B                                                                                              0.0s
 => CACHED [builder 2/8] COPY --from=go-builder /usr/local/go/ /usr/local/go/                                                 0.0s
 => CACHED [builder 3/8] RUN python -m pip install --upgrade pip                                                              0.0s
 => CACHED [builder 4/8] RUN apk add build-base pkgconfig                                                                     0.0s
 => CACHED [builder 5/8] COPY app /app                                                                                        0.0s
 => CACHED [builder 6/8] WORKDIR /app                                                                                         0.0s
 => CACHED [builder 7/8] RUN pip install nameparser &&     go mod init human-name &&     go mod tidy                          0.0s
 => CACHED [builder 8/8] RUN go build -o HumanName                                                                            0.0s
 => CACHED [stage-2 2/3] COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages    0.0s
 => CACHED [stage-2 3/3] COPY --from=builder /app/HumanName /HumanName                                                        0.0s
 => exporting to image                                                                                                        0.0s
 => => exporting layers                                                                                                       0.0s
 => => writing image sha256:8c4e43d26afff57ef65cc8431d3c0a01e57ad43e2acbe3f90ac5a5b6c5edab12                                  0.0s
 => => naming to docker.io/soulteary/python-in-golang 

镜像构建完毕,执行下面的命令,就能够验证程序是否正常了:

docker run --rm -it soulteary/python-in-golang

不出意外,程序将输出下面的内容:

# docker run --rm -it soulteary/python-in-golang

{
    
    'title': 'Dr.', 'first': 'Juan', 'middle': 'Q. Xavier', 'last': 'de la Vega', 'suffix': 'III', 'nickname': 'Doc Vega'}

好了,在 Golang 中运行 Python 程序,到这里就基本搞定啦。

当我们观察构建好的镜像,能够看到,我们构建的镜像仅仅比官方原始镜像增加了 1.9 MB,是不是非常“环保”。

# go-name docker images | grep python

soulteary/python-in-golang    latest           8218d4d4b16b   About a minute ago   48.9MB
python                        3.7-alpine3.18   e4fbc12a05a9   11 days ago          47MB

使用镜像加速构建过程

为了能够让镜像构建速度加快,我们可以为 Python 和 Golang ,以及我们所使用的系统 Alpine 添加软件源镜像。

# Base Images
FROM golang:1.20.4-alpine3.18 AS go-builder
FROM python:3.7-alpine3.18 AS builder
# Base Builder Env
COPY --from=go-builder /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
# Set Alpine Mirror
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN python -m pip install --upgrade pip
# Set PyPi Mirror
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
RUN apk add build-base pkgconfig
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
ENV CGO_ENABLED=1
# Set Golang Mirror
ENV GOPROXY="https://goproxy.cn"
# Copy source code
COPY app /app
WORKDIR /app
# Install deps
RUN pip install nameparser && \
    go mod init human-name && \
    go mod tidy
# Build the binary
RUN go build -o HumanName

# Run Image
FROM python:3.7-alpine3.18
# Copy Python Deps
COPY --from=builder /usr/local/lib/python3.7/site-packages/nameparser /usr/local/lib/python3.7/site-packages/nameparser
# Copy binary
COPY --from=builder /app/HumanName /HumanName
CMD ["/HumanName"]

最后

其实在 Golang 中运行 Python 除了本文的方法,以及在文章前面我们避免的方法之外,还有两种方案,有机会我们再展开聊聊。

好了,这篇文章,就先写到这里啦。

–EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术资料。

喜欢折腾的小伙伴,欢迎阅读下面的内容,扫码添加好友。

关于“交友”的一些建议和看法

添加好友时,请备注实名和公司或学校、注明来源和目的,否则不会通过审核。

关于折腾群入群的那些事


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2023年05月21日
统计字数: 6730字
阅读时间: 14分钟阅读
本文链接: https://soulteary.com/2023/05/21/run-python-code-with-golang-and-docker.html

猜你喜欢

转载自blog.csdn.net/soulteary/article/details/130798768