Go-Bibliothek mit Python verwenden
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.