Go-Bibliothek mit Python verwenden

Aktualisiert 6. Januar 2022

Vor ein paar Wochen habe ich einen URL-Tokenizer in Python geschrieben und der Code war dem eines Go-Projekts sehr ähnlich. Ich habe einige Stunden investiert, um eine Lösung zu finden, den Go-Code in Python zu verwenden, und hier sind meine Ergebnisse.

Wie man Go-Code in Python wiederverwendet

  • Go- und Python-Programme können über gRPC miteinander kommunizieren
  • Go-Code mit GoToPy in Python-Code übersetzen
  • Go als C-Bibliothek exportieren und einen Python-Wrapper schreiben

Ich habe mich entschieden, den C-Bibliothek-Export auszuprobieren, und hier sind meine Ergebnisse.

Wie man die C-Bibliothek exportiert

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

Das resultierende Binärformat hängt vom Betriebssystem ab. Für andere Plattformen kann man den Cross-Compiler wie folgt verwenden:

GOOS=linux GOARCH=arm64 go build -o awesome.so -buildmode=c-shared awesome.go

Alle verfügbaren GOOS/GOARCH-Kombinationen in Go 1.7 kann man mit folgendem Befehl auflisten:

go tool dist list

Wie man exportierbaren Go-Code schreibt

Wir müssen import "C" verwenden, um cgo zu aktivieren.

Die Präambel kann beliebigen C-Code enthalten, einschließlich Funktions- und Variablendeklarationen und -definitionen, und #include <stdlib.h> ist ein Muss.

Jede Funktion, die wir exportieren möchten, müssen wir mit export function_name kennzeichnen.

Python- und Go-Typen sind nicht direkt kompatibel, daher müssen wir ctypes verwenden. Komplexe Strukturen werden nicht direkt unterstützt, aber wir können einen einfachen JSON-String für Unmarshal verwenden. Im folgenden Beispiel verwenden wir eine Liste von URLs.

package main

/*
#include <stdlib.h>
*/
import "C"

import (
	"encoding/binary"
	"encoding/json"
	"unsafe"

	tok "github.com/emetriq/gourltokenizer/tokenizer"
)

//export Tokenize
func Tokenize(urlsByte *C.char, size C.int) unsafe.Pointer {
	d := C.GoBytes(unsafe.Pointer(urlsByte), size)
	urls := make([]string, 0, size)
	_ = json.Unmarshal([]byte(d), &urls)
	result := make([][]string, 0, len(urls))
	for _, url := range urls {
		result = append(result, tok.TokenizeV2(url, tok.IsEnglishStopWord))
	}
	resultByte, _ := json.Marshal(result)
	length := make([]byte, 8)
	binary.LittleEndian.PutUint64(length, uint64(len(resultByte)))
	return C.CBytes(append(length, resultByte...))
}

//export Free
func Free(addr *C.char) {
	C.free(unsafe.Pointer(addr))
}

func main() {}

Im Python-Code müssen wir die Bibliothek laden, und hier zeigt sich der erste Nachteil. Zum Beispiel ist die Verteilung vorgefertigter Wheel-Pakete eine große Herausforderung, wenn man an all die möglichen GOOS/GOARCH-Kombinationen denkt, und persönlich mag ich die hässlichen C-Typen nicht.

import ctypes as ct
from typing import List
import json

_lib = ct.cdll.LoadLibrary("./tokenizer.so")

_lib.TokenizeEng.argtypes = [ct.c_char_p, ct.c_int]
_lib.TokenizeEng.restype = ct.POINTER(ct.c_ubyte*8)
_lib.Free.argtypes = ct.c_void_p,
_lib.Free.restype = None

tokenize = _lib.Tokenize
free = _lib.Free

def tokenize(urls: List[str]):
    try:
        data = json.dumps(urls).encode('utf-8')
        ptr = tokenize(data, len(data))
        length = int.from_bytes(ptr.contents, byteorder='little')
        data = bytes(ct.cast(ptr,
                ct.POINTER(ct.c_ubyte*(8 + length))
                ).contents[8:])
        return json.loads(data.decode('utf-8'))
    finally:
        free(ptr)

print(tokenize(["https://www.google.com/hallo/essen",
"https://www.facebook.com/autos/geld/news"]))

Fazit

Python-Wrapper sind cool, aber vorgefertigte Pakete für alle Plattformen erfordern viel Arbeit in der CI/CD-Pipeline.

Ich denke, wenn man maximale Performance erreichen möchte, muss man immer nativen Code in Kombination mit einer einheitlichen Unit-Test-Spezifikation für alle Programmiersprachen verwenden.

Wenn die Performance nicht so wichtig ist, kann man jsii ausprobieren. Der Basiscode ist TypeScript und jsii kann den Code nach Python, Java, C# und Go konvertieren. Aber unter der Haube gibt es immer eine jsii-Laufzeitumgebung, sodass wir hier nicht von 100% nativem Code sprechen können.