Ein Mechanismus zum Aufrufen von Ruby-Methoden aus Python, der in 200 Zeilen ausgeführt werden kann

Ich habe eine Bibliothek erstellt, mit der Sie Ruby-Methoden aus Python aufrufen können. Ich werde Methodenketten und Iteratoren einführen, da sie bis zu einem gewissen Grad natürlich verwendet werden können.

https://github.com/yohm/rb_call

Wie es gemacht wurde

Wir entwickeln eine Rails-App, die Jobs für wissenschaftliche und technologische Berechnungen verwaltet. Das Verhalten kann über die Ruby-API gesteuert werden. Da jedoch viele Leute auf dem Gebiet der Wissenschafts- und Technologieberechnung Python-Benutzer sind, gab es viele Anfragen nach einer Python-API anstelle von Ruby.

Was kannst du tun?

Angenommen, Sie haben den folgenden Ruby-Code:

minimal_sample.rb


class MyClass
  def m1
    "m1"
  end

  def m2(a,b)
    "m2 #{a} #{b}"
  end

  def m3(a, b:)
    "m3 #{a} #{b}"
  end

  def m4(a)
    Proc.new { "m4 #{a}" }
  end

  def m5
    enum = Enumerator.new{|y|
      (1..10).each{|i|
        y << "#{i}" if i % 5 == 0
      }
    }
  end
end

if $0 == __FILE__
  obj = MyClass.new
  puts obj.m1, obj.m2(1,2), obj.m3(3,b:4)  #=> "m1", "m2 1 2", "m3 3 4"
  proc = obj.m4('arg of proc')
  puts proc.call                           #=> "m4 arg of proc"
  e = MyClass.m5
  e.each do |i|
    puts i                                 #=> "5", "10"
  end
end

Dasselbe kann in Python wie folgt geschrieben werden.

minimal_sample.py


from rb_call import RubySession

rb = RubySession()                          # Execute a Ruby process
rb.require('./minimal_sample')              # load a Ruby library 'sample_class.rb'

MyClass = rb.const('MyClass')               # get a Class defined in 'sample_class.rb'
obj = MyClass()                             # create an instance of MyClass
print( obj.m1(), obj.m2(1,2), obj.m3(3,b=4) )
                                            #=> "m1", "m2 1 2", "m3 3 4"
proc = obj.m4('arg of proc')
print( proc() )                             #=> "m4 arg of proc"

e = obj.m5()                                # Not only a simple Array but an Enumerator is supported
for i in e:                                 # You can iterate using `for` syntax over an Enumerable
    print(i)                                #=> "5", "10"

Sie können Ruby-Bibliotheken fast so wie sie sind von Python aus aufrufen. Sie können Methodenketten erstellen und mit for iterieren. Obwohl nicht in der Stichprobe enthalten, kann die Listeneinschlussnotation wie erwartet verwendet werden. Sie können auch auf Ruby-Ausnahmen zugreifen.

Wenn Sie es beispielsweise mit Rails-Code kombinieren, können Sie es so schreiben.

rails_sample.py


author = Author.find('...id...')
Book.where( {'author':author} ).gt( {'price':100} ).asc( 'year' )

Wenn Sie Metaprogrammierung und externe Bibliotheken gut verwenden, können Sie dies mit kompaktem Code erreichen, der in eine Datei passt, ungefähr 130 Zeilen für Python und ungefähr 80 Zeilen für Ruby. Natürlich gibt es Einschränkungen, wie später beschrieben, aber es funktioniert gut für die meisten Anwendungen.

Wie haben Sie es umgesetzt?

Probleme mit normalem RPC

Ich verwende RPC, um Ruby-Methoden aus Python aufzurufen. Dieses Mal habe ich eine Bibliothek (Spezifikation?) Namens MessagePack-RPC verwendet. Informationen zur grundlegenden Verwendung von MessagePack RPC finden Sie hier. http://qiita.com/yohm13/items/70b626ca3ac6fbcdf939 Ruby wird als Unterprozess des Python-Prozesses gestartet, und Ruby und Python kommunizieren über Socket miteinander. Geben Sie grob gesagt den Methodennamen und die Argumente an, die Sie von Python an Ruby aufrufen möchten, und geben Sie den Rückgabewert von Ruby an Python zurück. Zu diesem Zeitpunkt ist die Spezifikation zum Serialisieren der gesendeten / empfangenen Daten in MessagePack-RPC definiert.

Wenn es sich um einen Prozess handelt, bei dem "einfach ein Argument angegeben und ein Wert zurückgegeben wird", gibt es bei dieser Methode kein Problem. Ruby macht jedoch häufig Lust, eine Methodenkette zu erstellen, und es gibt viele Bibliotheken, die dies annehmen. In Rails schreiben Sie beispielsweise häufig den folgenden Code.

Book.where( author: author ).gt( price: 100 ).asc( :year )

Eine solche Methodenkette kann mit gewöhnlichen RPC nicht realisiert werden.

Dieses Problem ergibt sich im Wesentlichen aus der Unfähigkeit, den Zustand in der Mitte der Methodenkette zu speichern. RPC zwischen Ruby und Python kann nur Objekte austauschen, die mit MessagePack serialisiert werden können. Daher wird das Objekt nach "Book.where" serialisiert, wenn es vom Ruby-Prozess an den Python-Prozess zurückgegeben wird. Es handelt sich also um ein anderes Selbst wenn Sie die Methode von aufrufen möchten, können Sie sie nicht aufrufen.

Mit anderen Worten, es ist erforderlich, ein Ruby-Objekt im Ruby-Prozess zu halten, und es ist ein Mechanismus erforderlich, um später bei Bedarf darauf zu verweisen.

Lösungen

Daher werden dieses Mal Ruby-Objekte, die nicht durch den Rückgabewert von Ruby serialisiert werden können, im Ruby-Prozess beibehalten, und nur die ID und die Klasse des Objekts werden an die Python-Seite zurückgegeben. Definieren Sie eine Klasse mit dem Namen "RubyObject" auf der Python-Seite, behalten Sie ein Paar (ID, Klasse) von der Ruby-Seite als Mitglied bei und delegieren Sie den Methodenaufruf an dieses RubyObject an das Objekt im Ruby-Prozess. Machen.

Die Verarbeitung bei der Rückgabe eines Werts auf der Ruby-Seite ist ungefähr wie folgt.

@@variables[ obj.object_id ] = obj          #Bewahren Sie das Objekt so auf, dass es später anhand der ID referenziert werden kann
MessagePack.pack( [self.class.to_s, self.object_id] )  #Geben Sie die Klassen- und Objekt-ID an die Python-Seite zurück

Alles, was mit MessagePack serialisiert werden kann, wie z. B. String und Fixnum, wird unverändert an Python gesendet.

Die Verarbeitung, wenn sie auf der Python-Seite empfangen wird

class RubyObject():
    def __init__(self, rb_class, obj_id):  #RubyObject enthält Klassenname und ID
        self.rb_class = rb_class
        self.obj_id = obj_id

#Behandlung von von RPC zurückgegebenen Werten
rb_class, obj_id = msgpack.unpackb(obj.data, encoding='utf-8')
RubyObject( rb_class, obj_id )

Wenn ich es in ein Bild schreibe, sieht es so aus, und das Objekt auf der Python-Seite hat nur einen Zeiger auf das Ruby-Objekt.

RbCall.png

Danach ist es in Ordnung, wenn Sie den an RubyObject in Python vorgenommenen Methodenaufruf auf das eigentliche Objekt auf der Ruby-Seite übertragen. Definieren Sie eine getattr- Methode (method_missing in Ruby), die aufgerufen wird, wenn ein nicht vorhandenes Attribut für das RubyObject aufgerufen wird.

class RubyObject():
    ...
    def __getattr__( self, attr ):
        def _method_missing(*args, **kwargs):
            return self.send( attr, *args, **kwargs )
        return _method_missing

    def send(self, method, *args, **kwargs):
        #Objekt-ID in RPC,Senden Sie den Methodennamen und das Argument an Ruby
        obj = self.client.call('send_method', self.obj_id, method, args, kwargs )
        return self.cast(obj)       #Wandeln Sie den Rückgabewert in ein RubyObject um

Code auf der Ruby-Seite aufgerufen

  def send_method( objid, method_name, args = [], kwargs = {})
    obj = find_object(objid)                       #Holen Sie sich das gespeicherte Objekt von objid
    ret = obj.send(method_name, *args, **kwargs)   #Methode ausführen
  end

Dann wird die in Ruby für RubyObject aufgerufene Methode als Ruby-Methode aufgerufen. Damit verhalten sich Ruby-Objekte auch so, als wären sie Python-Variablen zugewiesen, und Ruby-Methoden können natürlich von Python aus aufgerufen werden.

Bei der Montage zu beachtende Punkte

Verwenden Sie den Message Pack-Erweiterungstyp

MessagePack verfügt über eine Spezifikation, mit der Sie einen benutzerdefinierten Typ namens Erweiterungstyp definieren können. https://github.com/msgpack/msgpack/blob/master/spec.md#types-extension-type

Dieses Mal habe ich RubyObject (dh String des Klassennamens und Fixnum der Objekt-ID) als Erweiterungstyp definiert und verwendet. Auf der Ruby-Seite wurde die Methode to_msgpack_ext durch das Patchen von Affen auf Object definiert. Übrigens, obwohl das neueste msgpack-Gem den Erweiterungstyp unterstützt, scheint msgpack-rpc-ruby die Entwicklung gestoppt zu haben und hat nicht begonnen, das neueste msgpack zu verwenden. Gabelte, um sich auf die neuesten Edelsteine zu verlassen. https://github.com/yohm/msgpack-rpc-ruby Der Code sieht folgendermaßen aus:

Object.class_eval
  def self.from_msgpack_ext( data )
    rb_cls, obj_id = MessagePack.unpack( data )
    RbCall.find_object( obj_id )
  end

  def to_msgpack_ext
    RbCall.store_object( self )   #Speichern Sie das Objekt in der Variablen
    MessagePack.pack( [self.class.to_s, self.object_id] )
  end
end
MessagePack::DefaultFactory.register_type(40, Object)

Auch auf der Python-Seite habe ich einen Prozess geschrieben, um den 40. Erweiterungstyp in den RubyObject-Typ zu konvertieren.

Zählen der Anzahl der Objektreferenzen

Bei dieser Rate erhöhen sich die im Ruby-Prozess gespeicherten Variablen jedes Mal, wenn ein Objekt von der Ruby-Seite zur Python-Seite zurückgegeben wird, monoton und es tritt ein Speicherverlust auf. Variablen, auf die auf der Python-Seite nicht mehr verwiesen wird, müssen auch auf der Ruby-Seite dereferenziert werden.

Deshalb habe ich das Python RubyObject __del__ überschrieben. __del__ ist eine Methode, die aufgerufen wird, wenn die Variable, die sich auf ein Objekt in Python bezieht, 0 ist und von GC erfasst werden kann. Zu diesem Zeitpunkt werden auch die Variablen auf der Ruby-Seite gelöscht. http://docs.python.jp/3/reference/datamodel.html#object.del

    def __del__(self):
        self.session.call('del_object', self.obj_id)

Der folgende Code wird auf der Ruby-Seite aufgerufen.

  def del_object
    @@variables.delete(objid)
  end

Wenn Sie diese Methode jedoch einfach verwenden, funktioniert sie nicht ordnungsgemäß, wenn zwei Python-Variablen auf ein Ruby-Objekt verweisen. Daher wird auch die Anzahl der von der Ruby-Seite an die Python-Seite zurückgegebenen Referenzen gezählt, und die Variablen auf der Ruby-Seite werden ebenfalls freigegeben, wenn sie Null werden.

class RbCall
  def self.store_object( obj )
    key = obj.object_id
    if @@variables.has_key?( key )
      @@variables[key][1] += 1
    else
      @@variables[key] = [obj, 1]
    end
  end

  def self.find_object( obj_id )
    @@variables[obj_id][0]
  end

  def del_object( args, kwargs = {} )
    objid = args[0]
    @@variables[objid][1] -= 1
    if @@variables[objid][1] == 0
      @@variables.delete(objid)
    end
    nil
  end
end

Objekte, die aus "@@ Variablen" gelöscht wurden, werden von Ruby's GC ordnungsgemäß freigegeben. Es sollte kein Problem geben, die Lebensdauer des Objekts zu verwalten.

Ausnahme

Aktiviert, um Ruby-Ausnahmeinformationen abzurufen. Wenn im veröffentlichten msgpack-rpc-ruby eine Ausnahme auf der Ruby-Seite auftritt, wird die Ausnahme als "to_s" gesendet, aber diese Methode verliert die meisten Informationen über die Ausnahme. Daher wird das Ausnahmeobjekt von Ruby auch als Instanz von RubyObject von Python gesendet. Wieder habe ich msgpack-rpc-ruby geändert, um Änderungen an der Serialisierung vorzunehmen, wenn Msgpack serialisieren kann, anstatt immer to_s auszuführen.

Die Verarbeitung, wenn eine Ausnahme auftritt, ist wie folgt. Wenn auf der Ruby-Seite eine Ausnahme auftritt, tritt auf der Python-Seite "msgpackrpc.error.RPCError" auf. Dies ist eine Spezifikation von msgpack-rpc-python. Fügen Sie eine Instanz von RubyObject in das Attribut args der Ausnahme ein. Wenn RubyObject enthalten ist, lösen Sie die auf der Python-Seite definierte RubyException aus. Zu diesem Zeitpunkt speichert das Attribut "rb_exception" den Verweis auf das Ausnahmeobjekt, das auf der Ruby-Seite aufgetreten ist. Jetzt können Sie auf die Ausnahmen auf der Ruby-Seite zugreifen.

Die Verarbeitung auf der Python-Seite wird wie folgt vereinfacht und geschrieben.

class RubyObject():
    def send(self, method, *args, **kwargs):
        try:
            obj = self.session.client.call('send_method', self.obj_id, method, args, kwargs )
            return self.cast(obj)
        except msgpackrpc.error.RPCError as ex:
            arg = RubyObject.cast( ex.args[0] )
            if isinstance( arg, RubyObject ):
                raise RubyException( arg.message(), arg ) from None
            else:
                raise

class RubyException( Exception ):
    def __init__(self,message,rb_exception):
        self.args = (message,)
        self.rb_exception = rb_exception

Wenn beispielsweise ein Ruby ArgumentError generiert wird, lautet der Python-Prozess wie folgt.

try:
    obj.my_method("invalid", "number", "of", "arg")  #RubyObject my_Falsche Anzahl von Methodenargumenten
except RubyException as ex:    #RubyException-Ausnahme tritt auf
    ex.args.rb_exception       # ex.args.rb_Die Ausnahme hat ein RubyObject, das auf eine Ruby-Ausnahme verweist

Generator kompatibel

Ziehen Sie beispielsweise die folgende Ruby-Verarbeitung in Betracht.

articles = Article.all

Article.all ist Enumerable, nicht Array, und wird nicht als Array im Speicher erweitert. Nur wenn jeder gedreht wird, wird auf die Datenbank zugegriffen und die Informationen jedes Datensatzes können erfasst werden.

Auch auf der Python-Seite muss ein Generator definiert werden, um die Schleife in Form von "for a in articles" auszuführen.

Definieren Sie dazu die Methode iter in der RubyObject-Klasse auf der Python-Seite. __iter__ ist eine Methode, die einen Iterator zurückgibt, und diese Methode wird implizit in der for-Anweisung aufgerufen. Dies entspricht direkt Rubys "each", also rufen Sie "each" in "iter" auf. http://anandology.com/python-practice-book/iterators.html https://docs.ruby-lang.org/ja/latest/class/Enumerator.html

In Python wird beim Drehen einer Schleife die Methode __next__ für den Rückgabewert von __iter__ aufgerufen. In Ruby gibt es genau die gleiche Entsprechung, und "Enumerator # next" ist die entsprechende Methode. Wenn die Iteration ihr Ende erreicht, löst die Ruby-Seite eine Ausnahme namens "StopIteration" aus. Python hat die gleichen Spezifikationen, und wenn eine Ausnahme auftritt, wird eine Ausnahme namens "StopIteration" ausgelöst. (Es ist zufällig eine Ausnahme mit demselben Namen.)

class RubyObject():
    ...
    def __iter__(self):
        return self.send( "each" )
    def __next__(self):
        try:
            n = self.send( "next" )
            return n
        except RubyException as ex:
            if ex.rb_exception.rb_class == 'StopIteration': #Wenn in Ruby eine Stop-Iterations-Ausnahme ausgelöst wird
                raise StopIteration()  #Löst eine Stop-Iterations-Ausnahme in Python aus
            else:
                raise

Jetzt können Sie die Schleife von Python zu Ruby's Enumerable verwenden.

Andere

Definieren Sie Methoden in RubyObject, damit die integrierten Funktionen von Python ordnungsgemäß funktionieren. Die entsprechenden Ruby-Methoden sind:

-- __eq__ ist == -- __dir __ ist public_methods -- __str__ ist to_s -- __len__ ist size --__getitem__ ist [] --__call__ ist call

Einschränkungen

Als ich es implementierte, stellte ich fest, dass Python und Ruby eine sehr ähnliche Entsprechung haben und dass es sehr ordentlich implementiert werden kann, indem man die entsprechende Methode gut definiert. Es sind ungefähr 200 Codezeilen. Wenn Sie also interessiert sind, lesen Sie bitte die Quelle.

Recommended Posts

Ein Mechanismus zum Aufrufen von Ruby-Methoden aus Python, der in 200 Zeilen ausgeführt werden kann
Verwendung der Methode __call__ in der Python-Klasse
So richten Sie einen einfachen SMTP-Server ein, der lokal in Python getestet werden kann
Ein Datensatz, den GAMEBOY mit Python nicht erstellen konnte. (PYBOY)
Eine Geschichte, die Heroku, die in 5 Minuten gemacht werden kann, tatsächlich 3 Tage dauerte
Ich habe versucht, eine Klasse zu erstellen, mit der Json in Python problemlos serialisiert werden kann
Ich habe ein Docker-Image erstellt, das FBX SDK Python von Node.js aus aufrufen kann
So installieren Sie die Python-Bibliothek, die von Pharmaunternehmen verwendet werden kann
So installieren Sie die Python-Bibliothek, die von Pharmaunternehmen verwendet werden kann
Ein Mechanismus zum Aufrufen von Ruby-Methoden aus Python, der in 200 Zeilen ausgeführt werden kann
Ich habe versucht, den Inhalt jedes von Python pip gespeicherten Pakets in einer Zeile zusammenzufassen
Untersuchung der von Python steuerbaren Gleichstromversorgung
[Python3] Code, der verwendet werden kann, wenn Sie ein Bild in einer bestimmten Größe ausschneiden möchten
Aus einem Buch, das Programmierer lernen können ... (Python): Zeiger
[Python] Erstellt eine Methode zum Konvertieren von Radix in 1 Sekunde
[Python] So rufen Sie eine Funktion von c aus Python auf (ctypes edition)
Konvertieren Sie Bilder aus dem FlyCapture SDK in ein Formular, das mit openCV verwendet werden kann
So schneiden Sie ein Block-Multiple-Array aus einem Multiple-Array in Python
Aus einem Buch, das Programmierer lernen können ... (Python): Über das Sortieren
Python3-Verarbeitung, die in Paiza verwendbar zu sein scheint
Aus einem Buch, das Programmierer lernen können (Python): Nachrichten dekodieren
Textanalyse, die in 5 Minuten durchgeführt werden kann [Word Cloud]
Ich habe versucht "Wie man eine Methode in Python dekoriert"
Skripte, die bei der Verwendung von Bottle in Python verwendet werden können
Wie man Python oder Julia von Ruby aus aufruft (experimentelle Implementierung)
[Python3] Code, der verwendet werden kann, wenn Sie die Größe von Bildern Ordner für Ordner ändern möchten
Flüssigkeitsvisualisierung BOS-Methode, die zu Hause durchgeführt werden kann, um zu sehen, was unsichtbar ist
Ein Memorandum (masOS), das tkinter importiert, konnte nicht mit Python durchgeführt werden, das von pyenv installiert wurde
[Python] Ein Programm, um die Anzahl der Äpfel und Orangen zu ermitteln, die geerntet werden können
Konvertieren Sie aus SpriteUV2 exportierte Netzdaten in ein Format, das von Spine importiert werden kann
Rufen Sie Matlab von Python zur Optimierung auf
[Python] Erstellen Sie ein Diagramm, das mit Plotly verschoben werden kann
Senden Sie eine Nachricht von IBM Cloud Functions an Slack in Python
Rufen Sie popcount von Ruby / Python / C # auf
Über psd-tools, eine Bibliothek, die psd-Dateien in Python verarbeiten kann
So erhalten Sie eine Zeichenfolge aus einem Befehlszeilenargument in Python
Erstellen Sie eine Spinbox, die mit Tkinter in Binär angezeigt werden kann
Aus einem Buch, das der Programmierer lernen kann ... (Python): Finden Sie den häufigsten Wert
Aus einem Buch, das Programmierer lernen können ... (Python): Überprüfung von Arrays
Ein Timer (Ticker), der im Feld verwendet werden kann (kann überall verwendet werden)
Eine Funktion, die die Verarbeitungszeit einer Methode in Python misst
Rufen Sie dlm von Python aus auf, um ein zeitvariables Koeffizientenregressionsmodell auszuführen
Zusammenfassung der Standardeingabe von Python, die in Competition Pro verwendet werden kann
・ <Slack> Schreiben Sie eine Funktion, um Slack zu benachrichtigen, damit sie jederzeit in Anführungszeichen gesetzt werden kann (Python).
Überprüfen Sie, ob Sie in Python eine Verbindung zu einem TCP-Port herstellen können
Erstellen Sie eine Spinbox, die mit Tkinter in HEX angezeigt werden kann
Aus einem Buch, das Programmierer lernen können (Python): Statistischer Verarbeitungsabweichungswert
Ich habe einen Tri-Tree geschrieben, der für die Implementierung von Hochgeschwindigkeitswörterbüchern in D-Sprache und Python verwendet werden kann
In Python gibt es keine "privaten" Instanzvariablen, auf die nur innerhalb eines Objekts zugegriffen werden kann.
[Python] Ein Programm, das ein Paar findet, das durch einen bestimmten Wert geteilt werden kann
Ich habe eine Webanwendung in Python erstellt, die Markdown in HTML konvertiert
[Python] Ein Programm, das die Anzahl der gepaarten Socken berechnet
Wie man einen Janken-Bot macht, der leicht bewegt werden kann (Kommentar)
Serverloser LINE-Bot, der in 2 Stunden ausgeführt werden kann (Erfassung der Quellkennung)
Extrahieren Sie mit Python Zeilen, die den Bedingungen entsprechen, aus einer Textdatei
[Kann in 10 Minuten erledigt werden] Erstellen Sie schnell eine lokale Website mit Django
So erhalten Sie den Wert aus dem Parameterspeicher in Lambda (mit Python)
Aus einem Buch, das der Programmierer lernen kann ... (Python): Bedingte Suche (Maximalwert)
Ich habe versucht, einen Formatierer zu entwickeln, der Python-Protokolle in JSON ausgibt