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
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.
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.
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.
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.
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.
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.
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.
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
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.
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
.class
und is_a?
In Ruby verwenden, aber nicht in Python.
--Dieses Problem kann vermieden werden, indem .send ('class')
, .send ('is_a?')
Aufgerufen wird.to_msgpack_ext
ebenfalls undefiniert und funktioniert nicht richtig.to_msgpack_ext
in der entsprechenden Klasse neu definieren.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.