Вызов python из golang

Интеграция функций Python в Golang

Недавно я работал над сервисом на Python, которому требовалась интеграция с приложением на Go. Звучит довольно просто. Момент, который привлек мое внимание, был следующим:

Python может быть динамически подключен как разделяемая библиотека (.dll / .lib.so).

Что же представляют собой эти разделяемые библиотеки? Они содержат скомпилированный код и данные, которые могут быть динамически связаны с другими программами во время выполнения.

https://t.me/Golang_google – Golang в телеграм! Наш обучающий на практике канал .

И почему это представляет для нас интерес на данном этапе? Говоря простым языком, это может помочь собрать сервис python в модуль, который можно импортировать в Go и использовать (так же просто, как import xyz).

Давайте рассмотрим шаг за шагом, как мы можем это сделать. Предварительные условия, которые нам потребуются, это установка Python3 и python3-devel. Также нам потребуется инструмент pkg-config.

Мы можем начать с создания простого скрипта Python, который выводит версию установленного Python. Это будет наше приложение на Python, которое мы будем модулировать и использовать в Go.

 1 import sys 2 3 def print_v(): 4 print("Python version <>".format(sys.version))

Мы можем сохранить приведенный выше скрипт как print_version.py в приведенной ниже структуре каталогов:

На стороне Go мы можем создать простое приложение и сохранить его в следующей структуре:

└── py-app │ └── print_version.py │ └── go-app ├── go.mod └── main.go

Следует помнить, что main.go интегрирует Python в Go, поэтому давайте разделим его на части.

 1 package main 2 3 // #cgo pkg-config: python3-embed 4 // #include 5 import "C" 6 import ( 7 "fmt" 8 "os" 9 ) 

Строки 3 и 4 указывают на то, что мы будем использовать встроенную версию python, исполняемую с помощью C (строка 5).

Любой код Python должен быть выполнен с помощью интерпретатора Python, поэтому он инициализируется в строке 12.

Далее начинается сложная часть. Поскольку мы создаем наше Python-приложение независимо от Go, мы собираемся использовать py-app как python-модуль в нашем main.go . Но py-app – это приложение, определяемое пользователем. Как же python распознает его как модуль? Это можно сделать одним из следующих двух способов в зависимости от конкретного случая использования.

  1. Добавьте py-app в папку lib установки Python, где находятся все модули.
  2. Установите каталог проекта в путь поиска модулей Python.
13 // Get the current Python module search path 14 sysPath := C.PySys_GetObject(C.CString("path")) 15 if sysPath == nil < 16 fmt.Println("Error getting sys.path") 17 return 18 >19 20 // Convert sys.path to a Go slice of strings 21 var pathSlice []string 22 numPaths := C.PyList_Size(sysPath) 23 for i := C.Py_ssize_t(0); i

Строка 14 эквивалентна вызову sys.path в Python. Это возвращает список путей, которые преобразуются в фрагменты строк Go, с которыми можно работать.

28 // Get current working directory 29 cwd, err := os.Getwd() 30 if err != nil < 31 fmt.Println("Error getting current working directory:", err) 32 return 33 >34 35 pathSlice = append(pathSlice, cwd)

Мы берем наш текущий рабочий каталог и добавляем его к фрагменту путей. Все, что нужно, это установить этот отредактированный фрагмент путей обратно в системный путь python.

36 // Convert the modified path slice back to a Python list 37 modifiedPath := C.PyList_New(C.Py_ssize_t(len(pathSlice))) 38 for i, path := range pathSlice < 39 pathStr := C.CString(path) 40 pathItem := C.PyUnicode_FromString(pathStr) 41 C.PyList_SetItem(modifiedPath, C.Py_ssize_t(i), pathItem) 42 >43 44 // Set the modified sys.path back to Python 45 C.PySys_SetObject(C.CString("path"), modifiedPath)

Поэтому мы преобразуем фрагмент обратно в список Python и зададим его в качестве пути поиска модулей по умолчанию в Python.

Вот он, крутой прием, которого мы так долго ждали. Давайте используем наше приложение Python, которое называется print_version.py, как модуль в Go.

46 // Import and use the Python module 47 moduleName := C.CString("print_version") 48 module := C.PyImport_ImportModule(moduleName) 49 50 // Check if the module was imported successfully 51 if module == nil

И как любой модуль, давайте обратимся к нему – это функция print_v().

55 // Access Function Definition 56 functionName := C.CString("print_v") 57 function := C.PyObject_GetAttrString(module, functionName) 58 59 if function == nil

Это извлекает определение нашей функции, и все, что остается, это вызвать ее.

62 args := C.PyTuple_New(0) 63 64 C.PyObject_CallObject(function, args)

Теперь, когда мы закончили использовать интерпретатор Python, мы можем очистить его ресурсы.

Давайте подытожим, на чем мы остановились. У нас есть исходный код py-app и есть код Go для использования Python-приложения в качестве модуля.

Чтобы связать все вместе, мы собираемся создать Python-приложение в виде модуля с помощью Cython.

Cython – это язык программирования, который является надмножеством Python. Он предназначен для написания кода на Python, который может быть скомпилирован в C или C++ для повышения производительности.

Мы собираемся использовать эту библиотеку Cython для преобразования нашего приложения Python в набор кодов C, а затем собрать его в файлы разделяемых объектов (.so). Чтобы использовать Cython, выполните команду pip3 install Cython .

Мы можем создать файл setup, который соберет наш исходный код Python в папке py-app.

└── py-app │ ├── print_version.py │ └── setup.py │ └── go-app ├── go.mod └── main.go
1 from setuptools import setup, Extension 2 from Cython.Build import cythonize 3 4 extensions = [ 5 Extension("print_version", ["print_version.py"]), 6 ] 7 8 setup( 9 name='py-app', 10 ext_modules=cythonize(extensions), 11 )

Мы используем Cython для преобразования нашего кода python в файлы C, которые затем собираются в библиотеки общих объектов с помощью setuptools.

Сценарий выполняется с помощью следующей команды

python3 setup.py build_ext --inplace 

Эта команда создаст модуль расширения Cython и поместит скомпилированные файлы (.so) в текущий каталог.

Интеграция функций Python в Golang

Это создает исходный текст на языке C для нашего файла Python и файл разделяемых объектов (.so). Мы можем скопировать этот файл в наше приложение Go, как показано ниже:

Интеграция функций Python в Golang

Теперь давайте выполним сценарий Go с помощью go run main.go, и вуаля – модуль запущен.

Источник

Создание модуля Python на Go

В последнее время я увлёкся изучение Go — современного языка програмирования от Google. Первые впечатления от его использования очень приятные и постепенно некоторые свои сервисы я начал переводить с Python на него.

Язык довольно простой и в плане перехода с Python особого дискомфорта не возникает. Cпособ обработок ошибок в Go мне понравился даже больше, чем исключения, но это вкусовщина. Кроме того Go является компилируемым и строго типизированным, что позволяет отловить многие ошибки на этапе компиляции.

В последнее время я работал над продуктом TachoBI для анализа карт тахографа и котнроля режима труда и отдыха водителей, который базируется на ERP системе с открытым исходным кодом Odoo. Так как этот продукт ориентирован на конечных пользователей, то полностью распространять всю систему в исходниках идея не очень и я начал думать, как это можно исправить. Первая идея была написать модули на С и распространять в виде библиотеки к Python приложению, но так как сроки поджимали, а функциональноть была объемная, то эту затею я оставил и стал искать другое решение. Как-то мне пришла идея, а нельзя ли скрестить Python и Go, так как скорость разработки и подерживаемость кода находятся приблизительно на одном уровне. Такое решение было найдено, но без С все-таки не обошлось.

Задача

В качестве примера для иллюстрации я выбрал простую задачку. Пусть на вход в функцию подается строка и ненулевое число, а на выходе получается их конкатенация. Например: “строка” + 1 = “строка1”.

Так как оба языка работают с С, то их взаимодействие мы построим через питоновкую С-шную библиотеку, а к ней с помощью CGO присоединим функцию на Go. Для работы используются Go 1.8 и Python 2.7.

Подготовка функции на Go

Итак файл lib.go, в котором будет функция, выполняющая нашу задачу будет выглядеть вот так:

package main import ( "errors" "strconv" ) func sum(s string, i int) (string, error) < result := "" var err error = nil if i != 0 < result = s + strconv.Itoa(i) >else < err = errors.New("Индекс не может быть 0") >return result, err > 

Подготовка C библиотеки для Python

В стандартной документации по Python описано как можно создавать для него C-ные модули. Наша библиотека будет называться test_lib, поэтому скелет нашего модуля будет храниться в файле test_lib.go со следующим содержимым:

package main /* #ifdef __linux__ #include #elif __APPLE__ #include #endif PyObject *test_lib_error; PyObject * Concat(PyObject *, PyObject *); int Parse_Args(PyObject * args, char * str, int * index) < return PyArg_ParseTuple(args, "ls", index, str); >static PyMethodDef test_lib_methods[] = < , >; PyMODINIT_FUNC inittest_lib(void) < PyObject *m; m = Py_InitModule("test_lib", test_lib_methods); if (m == NULL) return; test_lib_error = PyErr_NewException("test_lib.error", NULL, NULL); Py_INCREF(test_lib_error); PyModule_AddObject(m, "error", test_lib_error); >*/ import "C" func setException(text string) < C.PyErr_SetString(C.test_lib_error, C.CString(text)) return >func main() <> 

Здесь весь С-ный код находится в блоке комментария /**/ , перед импортом “C”. Такой блок называется преамбулой и используется при компиляции С частей пакета. Переменные и функции из преаумбулы можно вызывать в gо коде. Между импортом и комментарием не должно быть пробелов. Подробнее можно посмотреть в документации CGO.

Давайте разберемся что делает преамбула.

Сначала мы присоединяем заголовочный файл Python.h, чтобы получить возможность работать с питоновскими объектами. Затем мы создаем объекты для обработки ошибок test_lib_error и нашей будующей функции Concat , которая и будет реализовывать нашу задачу. Через них будет происходить общение с go-ным кодом.

Функция Parse_Args будет описана ниже. Далее объявляем массив test_lib_methods , который содержит таблицу методов реализуемых расширением, в формате .

Затем инициализируем модуль функцией inittest_lib .

Для того чтобы пробросить текс ошибки из Go в питоновское исключение, создается функция setException . Которая в C-ную переменную test_lib_error передаст текст, который получит на входе.

Объедиение созданной библиотеки с Go функцией

Мы выполнили все необходимые приготовления, и теперь нам осталось связать нашу функцию sum , с функцией Concat в нашей библиотеке. Для этого в файл lib.go необходимо добавить следующий код:

/* #cgo pkg-config: python-2.7 --cflags --libs #ifdef __linux__ #include #elif __APPLE__ #include #endif int Parse_Args(PyObject * args, char * str, int * index); */ import "C" //export Concat func Concat(self *C.PyObject, args *C.PyObject) *C.PyObject < var string = new(C.char) var c_idx C.int if C.Parse_Args(args, string, &c_idx) == 0 < setException("Ошибка парсинга аргументов") return nil >result := "" var err error = nil if result, err = sum(C.GoString(string), int(c_idx)); err != nil < setException(err.Error()) >return C.PyString_FromString(C.CString(result)) > 

Как видно тут в преамбуле мы указываем функцию Parse_Args , которая была объявлена в файле test_lib.go, с ее помощью мы поместим входные параметры в соответствующие переменные для дальнешей работы.

Преамбуда //export указывает на то, что мы можем использовать эту функцию в коде на С (в файле test_lib.go), в котором она инициализируется добавляется в таблицу функций.

Посмторим что происходит в самой функции. Сначала инициализируются переменные с С-ными типами (различны с типами в Go), затем эти переменные инициализируются входными параметрами, и если опрерация не удалось будет выбрасываться питоновский Exception. Если все прошло нормально и входные параметры мы получили, вызываем нашу функцию sum для проведения операции. Надо обратить внимание, что перед тем как передать входные параметры в функцию они из С-ных типов приводятся к типам в Go.

Если операция выполнена удачно, то для возврата результата, мы преобразовываем строку Go в сишную строку, а ее в строку питона и уже этот результат вернется нам при вызове эой библиотеки.

Сборка и проверка

Теперь нам осталось собрать библиотеку и проверить ее работу. Для сборки надо выполнить

go build -buildmode=c-shared -o test_lib.so 

А проверить можно выполнив команду

python -c 'import test_lib; print(test_lib.Concat('abc', 1))' 

P.S. на последней версии MacOS Siera данная операция не выполняется, и пока мне не удалось разобраться почему.

Заключение

Касательно моей основной задачи перевести весь код с Python на Go не заняло у меня много времени. А получившее решение было более чем работоспособным. Кроме того необходимо отметить что такми образом можно использовать функциональность горутин в Python об этом указано в статье[1], но сам я данную штутку не пробывал. Весь исходный код примера лежит можно увидеть на github.

Дополнительные материалы

Источник

Читайте также:  Php isset but null
Оцените статью