J'essaie d'utiliser la fonction Pool.map()
de multiprocessing
pour diviser le travail simultanément. Lorsque j'utilise le code suivant, cela fonctionne bien:
import multiprocessing
def f(x):
return x*x
def go():
pool = multiprocessing.Pool(processes=4)
print pool.map(f, range(10))
if __name__== '__main__' :
go()
Cependant, lorsque je l'utilise dans une approche plus orientée objet, cela ne fonctionne pas. Le message d'erreur que cela donne est:
PicklingError: Can't pickle <type 'instancemethod'>: attribute lookup
__builtin__.instancemethod failed
Cela se produit lorsque mon programme principal est le suivant:
import someClass
if __name__== '__main__' :
sc = someClass.someClass()
sc.go()
et ce qui suit est ma classe someClass
:
import multiprocessing
class someClass(object):
def __init__(self):
pass
def f(self, x):
return x*x
def go(self):
pool = multiprocessing.Pool(processes=4)
print pool.map(self.f, range(10))
Quelqu'un sait ce que le problème pourrait être, ou un moyen facile de le contourner?
Le problème est que le multitraitement doit décaper les choses pour les relier entre les processus, et les méthodes liées ne sont pas décapables. La solution de contournement (que vous considériez cela comme "facile" ou non ;-) est d'ajouter l'infrastructure à votre programme pour permettre le pickling de telles méthodes, en l'enregistrant avec la méthode de bibliothèque standard copy_reg .
Par exemple, la contribution de Steven Bethard à ce fil (vers la fin du fil) montre une approche parfaitement exploitable pour permettre le pickling/le picking de méthode via copy_reg
.
Toutes ces solutions sont laides, car le multitraitement et le décapage sont interrompus et limités à moins que vous ne sautiez en dehors de la bibliothèque standard.
Si vous utilisez un fork de multiprocessing
appelé pathos.multiprocesssing
, vous pouvez directement utiliser des classes et des méthodes de classe dans les fonctions map
du multitraitement. Ceci est dû au fait que dill
est utilisé à la place de pickle
ou cPickle
, et dill
peut sérialiser presque tout en python.
pathos.multiprocessing
fournit également une fonction de carte asynchrone… et il peut map
fonctions avec plusieurs arguments (par exemple, map(math.pow, [1,2,3], [4,5,6])
).
Voir: Que peuvent faire le multitraitement et l'aneth ensemble?
et: http://matthewrocklin.com/blog/work/2013/12/05/Parallelism-and-Serialization/
>>> import pathos.pools as pp
>>> p = pp.ProcessPool(4)
>>>
>>> def add(x,y):
... return x+y
...
>>> x = [0,1,2,3]
>>> y = [4,5,6,7]
>>>
>>> p.map(add, x, y)
[4, 6, 8, 10]
>>>
>>> class Test(object):
... def plus(self, x, y):
... return x+y
...
>>> t = Test()
>>>
>>> p.map(Test.plus, [t]*4, x, y)
[4, 6, 8, 10]
>>>
>>> p.map(t.plus, x, y)
[4, 6, 8, 10]
Et juste pour être explicite, vous pouvez faire exactement ce que vous voulez faire, et vous pouvez le faire à partir de l'interprète, si vous le souhaitez.
>>> import pathos.pools as pp
>>> class someClass(object):
... def __init__(self):
... pass
... def f(self, x):
... return x*x
... def go(self):
... pool = pp.ProcessPool(4)
... print pool.map(self.f, range(10))
...
>>> sc = someClass()
>>> sc.go()
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>>
Obtenez le code ici: https://github.com/uqfoundation/pathos
Vous pouvez également définir une méthode __call__()
à l'intérieur de votre someClass()
, qui appelle someClass.go()
, puis transmet une instance de someClass()
au pool. Cet objet est décapable et cela fonctionne bien (pour moi) ...
Quelques limites cependant à la solution de Steven Bethard:
Lorsque vous enregistrez votre méthode de classe en tant que fonction, le destructeur de votre classe est appelé de manière surprenante chaque fois que le traitement de votre méthode est terminé. Donc, si vous avez 1 instance de votre classe qui appelle n fois sa méthode, les membres peuvent disparaître entre 2 exécutions et vous pouvez recevoir un message malloc: *** error for object 0x...: pointer being freed was not allocated
(par exemple, un fichier de membre ouvert) ou pure virtual method called, terminate called without an active exception
(ce qui signifie que la durée de vie d'un objet membre que j'ai utilisé était plus courte que ce que je pensais). Je l'ai eu lorsqu'il s'agit de n supérieur à la taille de la piscine. Voici un court exemple:
from multiprocessing import Pool, cpu_count
from multiprocessing.pool import ApplyResult
# --------- see Stenven's solution above -------------
from copy_reg import pickle
from types import MethodType
def _pickle_method(method):
func_name = method.im_func.__name__
obj = method.im_self
cls = method.im_class
return _unpickle_method, (func_name, obj, cls)
def _unpickle_method(func_name, obj, cls):
for cls in cls.mro():
try:
func = cls.__dict__[func_name]
except KeyError:
pass
else:
break
return func.__get__(obj, cls)
class Myclass(object):
def __init__(self, nobj, workers=cpu_count()):
print "Constructor ..."
# multi-processing
pool = Pool(processes=workers)
async_results = [ pool.apply_async(self.process_obj, (i,)) for i in range(nobj) ]
pool.close()
# waiting for all results
map(ApplyResult.wait, async_results)
lst_results=[r.get() for r in async_results]
print lst_results
def __del__(self):
print "... Destructor"
def process_obj(self, index):
print "object %d" % index
return "results"
pickle(MethodType, _pickle_method, _unpickle_method)
Myclass(nobj=8, workers=3)
# problem !!! the destructor is called nobj times (instead of once)
Sortie:
Constructor ...
object 0
object 1
object 2
... Destructor
object 3
... Destructor
object 4
... Destructor
object 5
... Destructor
object 6
... Destructor
object 7
... Destructor
... Destructor
... Destructor
['results', 'results', 'results', 'results', 'results', 'results', 'results', 'results']
... Destructor
La méthode __call__
n'est pas aussi équivalente car [None, ...] sont lus à partir des résultats:
from multiprocessing import Pool, cpu_count
from multiprocessing.pool import ApplyResult
class Myclass(object):
def __init__(self, nobj, workers=cpu_count()):
print "Constructor ..."
# multiprocessing
pool = Pool(processes=workers)
async_results = [ pool.apply_async(self, (i,)) for i in range(nobj) ]
pool.close()
# waiting for all results
map(ApplyResult.wait, async_results)
lst_results=[r.get() for r in async_results]
print lst_results
def __call__(self, i):
self.process_obj(i)
def __del__(self):
print "... Destructor"
def process_obj(self, i):
print "obj %d" % i
return "result"
Myclass(nobj=8, workers=3)
# problem !!! the destructor is called nobj times (instead of once),
# **and** results are empty !
Donc, aucune des deux méthodes n'est satisfaisante ...
Il existe un autre raccourci que vous pouvez utiliser, bien que cela puisse être inefficace en fonction du contenu de vos instances de classe.
Comme tout le monde l’a dit, le problème est que le code multiprocessing
doit consigner les éléments qu’il envoie aux sous-processus qu’il a démarrés et que le préparateur ne fait pas d’instance-méthodes.
Cependant, au lieu d’envoyer la méthode-instance, vous pouvez envoyer l’instance réelle de la classe, ainsi que le nom de la fonction à appeler, à une fonction ordinaire qui utilise ensuite getattr
pour appeler la méthode-instance, créant ainsi le lien. méthode dans le sous-processus Pool
. Ceci est similaire à la définition d'une méthode __call__
sauf que vous pouvez appeler plusieurs fonctions membres.
Voler le code de @ EricH. Dans sa réponse et l'annoter un peu (je l'ai ressaisi donc tous les changements de nom, etc., pour une raison quelconque, cela semblait plus facile que couper-coller :-)) pour illustrer toute la magie:
import multiprocessing
import os
def call_it(instance, name, args=(), kwargs=None):
"indirect caller for instance methods and multiprocessing"
if kwargs is None:
kwargs = {}
return getattr(instance, name)(*args, **kwargs)
class Klass(object):
def __init__(self, nobj, workers=multiprocessing.cpu_count()):
print "Constructor (in pid=%d)..." % os.getpid()
self.count = 1
pool = multiprocessing.Pool(processes = workers)
async_results = [pool.apply_async(call_it,
args = (self, 'process_obj', (i,))) for i in range(nobj)]
pool.close()
map(multiprocessing.pool.ApplyResult.wait, async_results)
lst_results = [r.get() for r in async_results]
print lst_results
def __del__(self):
self.count -= 1
print "... Destructor (in pid=%d) count=%d" % (os.getpid(), self.count)
def process_obj(self, index):
print "object %d" % index
return "results"
Klass(nobj=8, workers=3)
La sortie montre que le constructeur est appelé une fois (dans le pid original) et le destructeur est appelé 9 fois (une fois pour chaque copie effectuée = 2 ou 3 fois par processus pool-worker-process selon les besoins, plus une fois dans l’original). processus). Ceci est souvent correct, comme dans ce cas, car le sélecteur par défaut crée une copie de l'instance entière et la recompose (semi-) secrètement - en l'occurrence, en effectuant:
obj = object.__new__(Klass)
obj.__dict__.update({'count':1})
C'est pourquoi, même si le destructeur est appelé huit fois dans les trois processus de travail, il compte à rebours de 1 à 0 à chaque fois, mais vous pouvez bien sûr avoir des ennuis de cette façon. Si nécessaire, vous pouvez fournir votre propre __setstate__
:
def __setstate__(self, adict):
self.count = adict['count']
dans ce cas par exemple.
Vous pouvez également définir une méthode __call__()
à l'intérieur de votre someClass()
, qui appelle someClass.go()
, puis transmet une instance de someClass()
au pool. Cet objet est décapable et cela fonctionne bien (pour moi) ...
class someClass(object):
def __init__(self):
pass
def f(self, x):
return x*x
def go(self):
p = Pool(4)
sc = p.map(self, range(4))
print sc
def __call__(self, x):
return self.f(x)
sc = someClass()
sc.go()
La solution de parisjohn ci-dessus me convient parfaitement. De plus, le code semble propre et facile à comprendre. Dans mon cas, il y a quelques fonctions à appeler avec Pool, j'ai donc modifié le code de parisjohn un peu plus bas. J'ai fait appeler pour pouvoir appeler plusieurs fonctions, et les noms de fonction sont passés dans l'argument dict de go()
:
from multiprocessing import Pool
class someClass(object):
def __init__(self):
pass
def f(self, x):
return x*x
def g(self, x):
return x*x+1
def go(self):
p = Pool(4)
sc = p.map(self, [{"func": "f", "v": 1}, {"func": "g", "v": 2}])
print sc
def __call__(self, x):
if x["func"]=="f":
return self.f(x["v"])
if x["func"]=="g":
return self.g(x["v"])
sc = someClass()
sc.go()
Pourquoi ne pas utiliser des fonctions séparées?
def func(*args, **kwargs):
return inst.method(args, kwargs)
print pool.map(func, arr)
J'ai rencontré le même problème, mais j'ai découvert qu'il existe un encodeur JSON qui peut être utilisé pour déplacer ces objets entre les processus.
from pyVmomi.VmomiSupport import VmomiJSONEncoder
Utilisez ceci pour créer votre liste: jsonSerialized= json.dumps(pfVmomiObj, cls=VmomiJSONEncoder)
Ensuite, dans la fonction mappée, utilisez-la pour récupérer l'objet: pfVmomiObj = json.loads(jsonSerialized)
Une solution potentiellement triviale consiste à utiliser multiprocessing.dummy
. Ceci est une implémentation basée sur les threads de l'interface de multitraitement qui ne semble pas avoir ce problème dans Python 2.7. Je n'ai pas beaucoup d'expérience ici, mais ce changement d'importation rapide m'a permis d'appeler apply_async sur une méthode de classe.
Quelques bonnes ressources sur multiprocessing.dummy
:
https://docs.python.org/2/library/multiprocessing.html#module-multiprocessing.dummy
Dans ce cas simple, où someClass.f
n'hérite d'aucune donnée de la classe et n'attache rien à la classe, une solution possible consisterait à séparer f
, afin qu'elle puisse être conservée:
import multiprocessing
def f(x):
return x*x
class someClass(object):
def __init__(self):
pass
def go(self):
pool = multiprocessing.Pool(processes=4)
print pool.map(f, range(10))
Mise à jour: à compter de la date de rédaction de ce document, les Tubes nommés sont sélectionnables (à partir de python 2.7)
Le problème ici est que les processus enfants ne sont pas en mesure d'importer la classe de l'objet - dans ce cas, la classe P-, dans le cas d'un projet multimodèle, la classe P doit pouvoir être importée partout où le processus enfant est utilisé.
une solution rapide consiste à le rendre importable en l’affectant à globals ()
globals()["P"] = P