Impossible de décaper <type 'instancemethod'> lors de l'utilisation de multiprocessing Pool.map ()

218

J'essaie d'utiliser multiprocessingla Pool.map()fonction de pour répartir 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 qu'il donne est:

PicklingError: Can't pickle <type 'instancemethod'>: attribute lookup
__builtin__.instancemethod failed

Cela se produit lorsque ce qui suit est mon programme principal:

import someClass

if __name__== '__main__' :
    sc = someClass.someClass()
    sc.go()

et voici ma someClassclasse:

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-il quel pourrait être le problème ou comment y remédier facilement?

ventolin
la source
4
si f est une fonction imbriquée, il y a une erreur similairePicklingError: Can't pickle <class 'function'>: attribute lookup builtins.function failed
ggg

Réponses:

122

Le problème est que le multitraitement doit décaper les éléments pour les faire passer entre les processus, et les méthodes liées ne sont pas décapables. La solution de contournement (que vous considériez "facile" ou non ;-) consiste à ajouter l'infrastructure à votre programme pour permettre le décapage 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 réalisable pour permettre le picking / unpickling de méthode via copy_reg.

Alex Martelli
la source
C'est bien merci. Semble avoir progressé d'une façon ou d'une autre: en utilisant le code à pastebin.ca/1693348, j'obtiens maintenant un RuntimeError: profondeur de récursivité maximale dépassée. J'ai regardé autour de moi et un message du forum a recommandé d'augmenter la profondeur maximale à 1500 (par rapport au 1000 par défaut), mais je n'y ai pas eu de joie. Pour être honnête, je ne vois pas quelle partie (de mon code, au moins) pourrait être récursive hors de contrôle, à moins que pour une raison quelconque le code ne soit décapé et décroché dans une boucle, en raison de légères modifications que j'ai apportées afin de faire Le code de Steven OO'd?
ventolin
1
Vos _pickle_methodretours self._unpickle_method, une méthode liée; donc bien sûr, le cornichon essaie maintenant de décapage QUE - et il fait comme vous l'avez dit: en appelant _pickle_method, récursivement. C'est-à-dire qu'en OOingérant le code de cette manière, vous avez inévitablement introduit une récursion infinie. Je suggère de revenir au code de Steven (et de ne pas adorer l'autel d'OO lorsque cela n'est pas approprié: beaucoup de choses en Python sont mieux faites de manière plus fonctionnelle, et celle-ci en est une).
Alex Martelli
15
Pour les super super paresseux , voir la seule réponse qui a pris la peine de poster le vrai code non mutilé ...
Cerin
2
Une autre façon de résoudre / contourner le problème de décapage consiste à utiliser de l'aneth, voir ma réponse stackoverflow.com/questions/8804830/…
rocksportrocker
74

Toutes ces solutions sont laides car le multitraitement et le décapage sont interrompus et limités à moins que vous ne sortiez de la bibliothèque standard.

Si vous utilisez un fork de multiprocessingcalled pathos.multiprocesssing, vous pouvez directement utiliser des classes et des méthodes de classe dans les mapfonctions de multiprocessing . En effet, dillest utilisé à la place de pickleou cPickle, et dillpeut sérialiser presque tout en python.

pathos.multiprocessingfournit également une fonction de carte asynchrone… et il peut mapfonctionner 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 vouliez faire en premier lieu, et vous pouvez le faire à partir de l'interprète, si vous le vouliez.

>>> 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

Mike McKerns
la source
3
Pouvez-vous mettre à jour cette réponse en fonction de pathos.pp car pathos.multiprocessing n'existe plus?
Saheel Godhane
10
Je suis l' pathosauteur. La version à laquelle vous faites référence a plusieurs années. Essayez la version sur github, vous pouvez utiliser pathos.ppou github.com/uqfoundation/ppft .
Mike McKerns
1
ou github.com/uqfoundation/pathos . @SaheelGodhane: Une nouvelle version est attendue depuis longtemps, mais devrait sortir sous peu.
Mike McKerns
3
D'abord pip install setuptools, alors pip install git+https://github.com/uqfoundation/pathos.git@master. Cela obtiendra les dépendances appropriées. Une nouvelle version est presque prête… maintenant presque tout pathosfonctionne également sous Windows et est 3.xcompatible.
Mike McKerns
1
@Rika: Oui. des cartes bloquantes, itératives et asynchrones sont disponibles.
Mike McKerns
35

Vous pouvez également définir une __call__()méthode à l'intérieur de votre someClass(), qui appelle someClass.go()puis transmet une instance de someClass()au pool. Cet objet est picklable et il fonctionne très bien (pour moi) ...

dorvak
la source
3
C'est beaucoup plus facile que la technique proposée par Alex Martelli, mais vous êtes limité à envoyer une seule méthode par classe à votre pool de multiprocesseurs.
déconseillé le
6
Un autre détail à garder à l'esprit est que seul l'objet (instance de classe) est décapé, pas la classe elle-même. Par conséquent, si vous avez modifié des attributs de classe par rapport à leurs valeurs par défaut, ces modifications ne se propageront pas aux différents processus. La solution consiste à vous assurer que tout ce dont votre fonction a besoin est stocké en tant qu'attribut d'instance.
déconseillé le
2
@dorvak pourriez-vous s'il vous plaît montrer un exemple simple avec __call__()? Je pense que votre réponse pourrait être la plus propre - j'ai du mal à comprendre cette erreur, et la première fois que je viens voir l'appel. Soit dit en passant, cette réponse aide également à clarifier ce que fait le multitraitement: [ stackoverflow.com/a/20789937/305883]
user305883
1
Pouvez-vous en donner un exemple?
frmsaul
1
Il y a une nouvelle réponse publiée (actuellement en dessous de celle-ci) avec un exemple de code pour cela.
Aaron
22

Quelques limitations 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 façon 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 obtenir un messagemalloc: *** error for object 0x...: pointer being freed was not allocated (par exemple, ouvrir le fichier membre) 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). J'ai obtenu ceci quand il s'agit de n supérieur à la taille de la piscine. Voici un petit 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)

Production:

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

le __call__ méthode n'est pas si équivalente, car [Aucun, ...] est lu à 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 ...

Eric H.
la source
7
Vous Nonerevenez parce que votre définition de __call__manque le return: il devrait l'être return self.process_obj(i).
torek
1
@Eric J'obtenais la même erreur et j'ai essayé cette solution, mais j'ai commencé à obtenir une nouvelle erreur comme "cPickle.PicklingError: Can't pickle <type 'function'>: l'attribut lookup builtin .function a échoué". Savez-vous quelle peut être une raison probable derrière cela?
Naman
15

Il existe un autre raccourci que vous pouvez utiliser, bien qu'il puisse être inefficace selon le contenu de vos instances de classe.

Comme tout le monde l’a dit, le problème est que le multiprocessing code doit décaper les choses qu'il envoie aux sous-processus qu'il a démarrés et que le sélecteur ne fait pas de méthodes d'instance.

Cependant, au lieu d'envoyer la méthode d'instance, vous pouvez envoyer l'instance de classe réelle, plus le nom de la fonction à appeler, à une fonction ordinaire qui utilise ensuite getattrpour appeler la méthode d'instance, créant ainsi la méthode liée dans le Poolsous - processus. Cela revient à définir une __call__méthode, sauf que vous pouvez appeler plusieurs fonctions membres.

Voler le code @ EricH. De sa réponse et l'annoter un peu (je l'ai retapé d'où tous les changements de nom et ainsi de suite, pour une raison quelconque, cela semblait plus facile que le copier-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 qu'en effet, le constructeur est appelé une fois (dans le pid d'origine) et le destructeur est appelé 9 fois (une fois pour chaque copie effectuée = 2 ou 3 fois par pool-worker-process selon les besoins, plus une fois dans l'original processus). C'est souvent OK, comme dans ce cas, car le sélecteur par défaut fait une copie de l'instance entière et (semi) la recopie secrètement - dans ce cas, en faisant:

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 de 1 à 0 à chaque fois - mais bien sûr, vous pouvez toujours avoir des ennuis de cette façon. Si nécessaire, vous pouvez fournir les vôtres __setstate__:

    def __setstate__(self, adict):
        self.count = adict['count']

dans ce cas par exemple.

torek
la source
1
C'est de loin la meilleure réponse à ce problème, car c'est la plus facile à appliquer au comportement par défaut non décapable
Matt Taylor
12

Vous pouvez également définir une __call__()méthode à l'intérieur de votre someClass(), qui appelle someClass.go()puis transmet une instance de someClass()au pool. Cet objet est picklable et il fonctionne très 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()
parisjohn
la source
3

La solution de parisjohn ci-dessus fonctionne très bien avec moi. De plus, le code semble propre et facile à comprendre. Dans mon cas, il y a quelques fonctions à appeler en utilisant Pool, j'ai donc modifié le code de parisjohn un peu plus bas. J'ai fait un appel pour pouvoir appeler plusieurs fonctions, et les noms des fonctions sont passés dans l'argument dict à partir 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()
neobot
la source
1

Une solution potentiellement triviale à cela est de passer à l'utilisation multiprocessing.dummy. Il s'agit d'une implémentation basée sur les threads de l'interface 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

http://chriskiehl.com/article/parallelism-in-one-line/

David Parks
la source
1

Dans ce cas simple, où someClass.fn'hérite pas de données de la classe et n'attache rien à la classe, une solution possible serait de les séparer f, afin qu'elles puissent être décapées:

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))
mhh
la source
1

Pourquoi ne pas utiliser des fonctions séparées?

def func(*args, **kwargs):
    return inst.method(args, kwargs)

print pool.map(func, arr)
0script0
la source
1

J'ai rencontré ce 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)
George
la source
0

Mise à jour: au jour de la rédaction de ce document, les nommésTuples sont sélectionnables (à partir de python 2.7)

Le problème ici est que les processus enfants ne peuvent pas importer la classe de l'objet -dans ce cas, la classe P-, dans le cas d'un projet multimodèle, la classe P doit être importable partout où le processus enfant est utilisé

une solution rapide consiste à le rendre importable en l'affectant aux globaux ()

globals()["P"] = P
rachid el kedmiri
la source