J'ai créé une bibliothèque qui vous permet d'appeler des méthodes Ruby depuis Python. Je vais introduire des chaînes de méthodes et des itérateurs car ils peuvent être utilisés naturellement dans une certaine mesure.
https://github.com/yohm/rb_call
Nous développons une application Rails qui gère les travaux pour les calculs scientifiques et technologiques, et le comportement peut être contrôlé par l'API Ruby. Cependant, comme de nombreuses personnes dans le domaine du calcul scientifique et technologique sont des utilisateurs de Python, il y a eu de nombreuses demandes pour une API Python au lieu de Ruby.
Par exemple, supposons que vous ayez le code Ruby suivant:
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
La même chose peut être écrite en Python comme suit.
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"
Vous pouvez appeler les bibliothèques Ruby à partir de Python presque telles quelles.
Vous pouvez faire des chaînes de méthodes, et vous pouvez itérer en utilisant for
. Bien que pas dans l'exemple, la notation d'inclusion de liste peut être utilisée comme prévu.
Vous pouvez également accéder aux exceptions Ruby.
Par exemple, si vous le combinez avec du code Rails, vous pouvez l'écrire comme ceci.
rails_sample.py
author = Author.find('...id...')
Book.where( {'author':author} ).gt( {'price':100} ).asc( 'year' )
Si vous utilisez bien la méta-programmation et les bibliothèques externes, vous pouvez le réaliser avec un code compact qui tient dans un seul fichier, environ 130 lignes pour Python et environ 80 lignes pour Ruby. Bien sûr, il existe des restrictions comme décrit plus loin, mais cela fonctionne bien pour la plupart des applications.
J'utilise RPC pour appeler des méthodes Ruby à partir de Python. Cette fois, j'ai utilisé une bibliothèque (spécification?) Appelée MessagePack-RPC. Veuillez vous référer ici pour l'utilisation de base de MessagePack RPC. http://qiita.com/yohm13/items/70b626ca3ac6fbcdf939 Ruby est démarré comme un sous-processus du processus Python, et Ruby et Python communiquent entre eux à l'aide de socket. En gros, donnez le nom de la méthode et les arguments que vous souhaitez appeler de Python vers Ruby, et renvoyez la valeur de retour de Ruby vers Python. A ce moment, la spécification de sérialisation des données transmises / reçues est définie dans MessagePack-RPC.
S'il s'agit d'un processus consistant à "simplement donner un argument et renvoyer une valeur", il n'y a aucun problème avec cette méthode. Cependant, Ruby vous donne souvent envie de faire une chaîne de méthodes, et il existe de nombreuses bibliothèques qui le supposent. Par exemple, dans Rails, vous écririez fréquemment le code suivant.
Book.where( author: author ).gt( price: 100 ).asc( :year )
Une telle chaîne de méthodes ne peut pas être réalisée par un RPC ordinaire.
Le problème est essentiellement dû à l'impossibilité de sauvegarder l'état au milieu de la chaîne de méthodes.
RPC entre Ruby et Python ne peut échanger que des objets qui peuvent être sérialisés avec MessagePack, donc l'objet après Book.where
sera sérialisé lorsqu'il est renvoyé du processus Ruby au processus Python, donc c'est un autre Même si vous souhaitez appeler la méthode de, vous ne pouvez pas l'appeler.
En d'autres termes, il est nécessaire de conserver un objet Ruby dans le processus Ruby, et un mécanisme pour y faire référence plus tard si nécessaire est nécessaire.
Par conséquent, cette fois, l'objet Ruby qui ne peut pas être sérialisé par la valeur de retour de Ruby est conservé dans le processus Ruby, et seuls l'ID et la classe de l'objet sont renvoyés du côté Python.
Définissez une classe appelée RubyObject
du côté Python, conservez la paire de (ID, classe) provenant du côté Ruby en tant que membre et déléguez l'appel de méthode à ce RubyObject à l'objet dans le processus Ruby. Faire.
Le traitement lors du renvoi d'une valeur côté Ruby est à peu près le suivant.
@@variables[ obj.object_id ] = obj #Conservez l'objet pour qu'il puisse être référencé ultérieurement par ID
MessagePack.pack( [self.class.to_s, self.object_id] ) #Renvoie l'ID de classe et d'objet du côté Python
Cependant, tout ce qui peut être sérialisé avec MessagePack, tel que String et Fixnum, est envoyé à Python tel quel.
Le traitement lorsqu'il est reçu côté Python
class RubyObject():
def __init__(self, rb_class, obj_id): #RubyObject contient le nom et l'ID de la classe
self.rb_class = rb_class
self.obj_id = obj_id
#Gestion des valeurs renvoyées par RPC
rb_class, obj_id = msgpack.unpackb(obj.data, encoding='utf-8')
RubyObject( rb_class, obj_id )
Quand je l'écris dans une image, cela ressemble à ceci, et l'objet du côté Python n'a qu'un pointeur vers l'objet Ruby.
Après cela, vous pouvez transférer l'appel de méthode effectué vers RubyObject en Python vers l'objet réel côté Ruby.
Définissez une méthode __getattr__
( method_missing
dans Ruby) qui est appelée lorsqu'un attribut qui n'existe pas est appelé pour le RubyObject.
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):
#ID d'objet dans RPC,Envoyer le nom et l'argument de la méthode à Ruby
obj = self.client.call('send_method', self.obj_id, method, args, kwargs )
return self.cast(obj) #Convertir la valeur de retour en RubyObject
Code appelé côté Ruby
def send_method( objid, method_name, args = [], kwargs = {})
obj = find_object(objid) #Récupère l'objet enregistré depuis objid
ret = obj.send(method_name, *args, **kwargs) #Exécuter la méthode
end
Ensuite, la méthode appelée pour RubyObject en Python sera appelée comme méthode de Ruby. Avec cela, les objets Ruby se comportent également comme s'ils étaient affectés à des variables Python, et les méthodes Ruby peuvent être appelées naturellement à partir de Python.
MessagePack a une spécification qui vous permet de définir un type défini par l'utilisateur appelé type d'extension. https://github.com/msgpack/msgpack/blob/master/spec.md#types-extension-type
Cette fois, j'ai défini RubyObject (c'est-à-dire, la chaîne du nom de la classe et le Fixnum de l'ID d'objet) comme type d'extension et je l'ai utilisé.
Du côté Ruby, la méthode to_msgpack_ext
a été définie par monkey patching sur Object.
Au fait, le dernier gem msgpack prend en charge le type Extension, mais msgpack-rpc-ruby semble avoir arrêté le développement et n'a pas utilisé le dernier msgpack. Forked pour s'appuyer sur les dernières gemmes.
https://github.com/yohm/msgpack-rpc-ruby
Le code ressemble à ceci:
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 ) #Enregistrez l'objet dans la variable
MessagePack.pack( [self.class.to_s, self.object_id] )
end
end
MessagePack::DefaultFactory.register_type(40, Object)
Du côté Python également, j'ai écrit un processus pour convertir le 40e type d'extension en type RubyObject
.
À ce rythme, chaque fois qu'un objet est renvoyé du côté Ruby au côté Python, les variables enregistrées dans le processus Ruby augmentent de manière monotone et une fuite de mémoire se produit. Les variables qui ne sont plus référencées côté Python doivent également être déréférencées côté Ruby.
C'est pourquoi j'ai remplacé le RubyObject Python __del__
.
__del__
est une méthode appelée lorsque la variable qui fait référence à un objet en Python est 0 et peut être collectée par GC.
À ce moment, les variables du côté Ruby sont également supprimées.
http://docs.python.jp/3/reference/datamodel.html#object.del
def __del__(self):
self.session.call('del_object', self.obj_id)
Le code suivant est appelé côté Ruby.
def del_object
@@variables.delete(objid)
end
Cependant, si vous utilisez simplement cette méthode, elle ne fonctionnera pas correctement si deux variables Python font référence à un objet Ruby. Par conséquent, le nombre de références renvoyées du côté Ruby au côté Python est également compté, et les variables du côté Ruby sont également libérées lorsqu'il devient nul.
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
Les objets supprimés de @@ variables
sont correctement libérés par Ruby GC.
Il ne devrait y avoir aucun problème à gérer la vie de l'objet.
Activé pour obtenir des informations sur les exceptions Ruby.
Dans le msgpack-rpc-ruby publié, lorsqu'une exception se produit du côté Ruby, l'exception est envoyée en tant que to_s
, mais cette méthode perd la plupart des informations sur l'exception.
Par conséquent, l'objet d'exception de Ruby est également envoyé en tant qu'instance de RubyObject de Python.
Encore une fois, j'ai modifié msgpack-rpc-ruby pour apporter des modifications à sérialiser si Msgpack peut sérialiser, plutôt que de toujours faire to_s.
Le traitement lorsqu'une exception se produit est le suivant.
Lorsqu'une exception se produit du côté Ruby, msgpackrpc.error.RPCError
se produit du côté Python. Ceci est une spécification de msgpack-rpc-python.
Mettez une instance de RubyObject dans l'attribut ʻargsde l'exception. Si RubyObject est inclus, lancez
RubyExceptiondéfini du côté Python. À ce moment, l'attribut
rb_exception` stocke la référence à l'objet d'exception qui s'est produit du côté Ruby.
Vous pouvez maintenant accéder aux exceptions côté Ruby.
Le traitement côté Python est simplifié et écrit comme suit.
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
Par exemple, lorsqu'une Ruby ArgumentError est générée, le processus Python est le suivant.
try:
obj.my_method("invalid", "number", "of", "arg") #RubyObject mon_Nombre incorrect d'arguments de méthode
except RubyException as ex: #Une exception RubyException se produit
ex.args.rb_exception # ex.args.rb_L'exception a un RubyObject qui fait référence à une exception Ruby
Par exemple, envisagez d'effectuer le traitement Ruby suivant.
articles = Article.all
ʻArticle.all` est Enumerable, pas Array, et n'est pas réellement développé en tant que tableau en mémoire. Seulement lorsque chacun est tourné, la base de données est accessible et les informations de chaque enregistrement peuvent être acquises.
Du côté Python également, il est nécessaire de définir un générateur pour exécuter la boucle sous la forme de for a in articles
.
Pour ce faire, définissez la méthode __iter__
dans la classe RubyObject côté Python.
__iter__
est une méthode qui renvoie un itérateur, et cette méthode est implicitement appelée dans l'instruction for.
Cela correspond directement au ʻeach de Ruby, alors appelez ʻeach
dans __iter__
.
http://anandology.com/python-practice-book/iterators.html
https://docs.ruby-lang.org/ja/latest/class/Enumerator.html
En Python, lors de la rotation d'une boucle, la méthode __next__
est appelée pour la valeur de retour de __iter__
. Il y a exactement la même correspondance dans Ruby, et ʻEnumerator # nextest la méthode correspondante. Lorsque l'itération atteint sa fin, le côté Ruby lève une exception appelée
StopIteration. Python a les mêmes spécifications, et lorsqu'une exception se produit, une exception appelée
StopIteration` est levée. (Il se trouve que c'est une exception avec le même nom.)
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': #Lorsqu'une exception Stop Iteration est levée dans Ruby
raise StopIteration() #Déclenche une exception Stop Iteration en Python
else:
raise
Vous pouvez maintenant utiliser la boucle de Python vers Ruby's Enumerable.
Définissez des méthodes dans RubyObject afin que les fonctions intégrées de Python fonctionnent correctement. Les méthodes Ruby correspondantes sont:
-- __eq__
est ==
-- __dir __
est public_methods
-- __str__
est to_s
-- __len__
est "taille"
--__getitem__
est []
--__call__
est un appel
.class
, ʻis_a?` En Ruby, mais pas en Python..send ('class')
, .send ('is_a?')
.to_msgpack_ext
défini dans rb_call sera également non défendu et ne fonctionnera pas correctement.to_msgpack_ext
dans la classe correspondante.Quand je l'ai implémenté, j'ai trouvé que Python et Ruby avaient une correspondance très similaire, et cela pouvait être implémenté très proprement en définissant bien la méthode correspondante. Il s'agit en fait d'environ 200 lignes de code, donc si vous êtes intéressé, veuillez lire la source.