web-dev-qa-db-fra.com

Comment passer une fonction avec plus d'un argument à python concurrent.futures.ProcessPoolExecutor.map ()?

Je voudrais que concurrent.futures.ProcessPoolExecutor.map() appelle une fonction composée de 2 arguments ou plus. Dans l'exemple ci-dessous, j'ai utilisé une fonction lambda et défini ref comme un tableau de taille égale à numberlist avec une valeur identique.

1ère question: Existe-t-il une meilleure façon de procéder? Dans le cas où la taille de la liste de numéros peut être de l'ordre de millions à milliards d'éléments, donc la taille de référence devrait suivre la liste de numéros, cette approche prend inutilement une mémoire précieuse, que j'aimerais éviter. Je l'ai fait parce que j'ai lu que la fonction map mettra fin à son mappage jusqu'à ce que la fin de tableau la plus courte soit atteinte.

import concurrent.futures as cf

nmax = 10
numberlist = range(nmax)
ref = [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
workers = 3


def _findmatch(listnumber, ref):    
    print('def _findmatch(listnumber, ref):')
    x=''
    listnumber=str(listnumber)
    ref = str(ref)
    print('listnumber = {0} and ref = {1}'.format(listnumber, ref))
    if ref in listnumber:
        x = listnumber
    print('x = {0}'.format(x))
    return x 

a = map(lambda x, y: _findmatch(x, y), numberlist, ref)
for n in a:
    print(n)
    if str(ref[0]) in n:
        print('match')

with cf.ProcessPoolExecutor(max_workers=workers) as executor:
    #for n in executor.map(_findmatch, numberlist):
    for n in executor.map(lambda x, y: _findmatch(x, ref), numberlist, ref):
        print(type(n))
        print(n)
        if str(ref[0]) in n:
            print('match')

En exécutant le code ci-dessus, j'ai constaté que la fonction map était en mesure d'atteindre le résultat souhaité. Cependant, lorsque j'ai transféré les mêmes termes vers concurrent.futures.ProcessPoolExecutor.map (), python3.5 a échoué avec cette erreur:

Traceback (most recent call last):
  File "/usr/lib/python3.5/multiprocessing/queues.py", line 241, in _feed
    obj = ForkingPickler.dumps(obj)
  File "/usr/lib/python3.5/multiprocessing/reduction.py", line 50, in dumps
    cls(buf, protocol).dump(obj)
_pickle.PicklingError: Can't pickle <function <lambda> at 0x7fd2a14db0d0>: attribute lookup <lambda> on __main__ failed

Question 2: Pourquoi cette erreur s'est-elle produite et comment puis-je obtenir concurrent.futures.ProcessPoolExecutor.map () pour appeler une fonction avec plus d'un argument?

12
Sun Bear

Pour répondre à votre deuxième question en premier, vous obtenez une exception car une fonction lambda comme celle que vous utilisez n'est pas picklable. Puisque Python utilise le protocole pickle pour sérialiser les données transmises entre le processus principal et les processus de travail de ProcessPoolExecutor, c'est un problème. Ce n'est pas clair pourquoi vous utilisent un lambda du tout. Le lambda que vous aviez prend deux arguments, tout comme la fonction d'origine. Vous pouvez utiliser _findmatch directement au lieu de lambda et cela devrait fonctionner.

with cf.ProcessPoolExecutor(max_workers=workers) as executor:
    for n in executor.map(_findmatch, numberlist, ref):
        ...

Quant au premier problème concernant le passage du deuxième argument constant sans créer de liste géante, vous pouvez résoudre ce problème de plusieurs manières. Une approche pourrait consister à utiliser itertools.repeat pour créer un objet itérable qui répète la même valeur pour toujours lorsqu'il est itéré.

Mais une meilleure approche serait probablement d'écrire une fonction supplémentaire qui passe l'argument constant pour vous. (C'est peut-être pour cela que vous essayez d'utiliser une fonction lambda?) Cela devrait fonctionner si la fonction que vous utilisez est accessible dans l'espace de noms de niveau supérieur du module:

def _helper(x):
    return _findmatch(x, 5)

with cf.ProcessPoolExecutor(max_workers=workers) as executor:
    for n in executor.map(_helper, numberlist):
        ...
9
Blckknght

(1) Pas besoin de faire une liste. Vous pouvez utiliser itertools.repeat pour créer un itérateur qui répète simplement une certaine valeur.

(2) Vous devez passer une fonction nommée à map car elle sera transmise au sous-processus pour exécution. map utilise le protocole pickle pour envoyer des choses, les lambdas ne peuvent pas être picklés et ne peuvent donc pas faire partie de la carte. Mais c'est totalement inutile. Votre lambda n'a fait qu'appeler une fonction à 2 paramètres avec 2 paramètres. Retirez-le complètement.

Le code de travail est

import concurrent.futures as cf
import itertools

nmax = 10
numberlist = range(nmax)
workers = 3

def _findmatch(listnumber, ref):    
    print('def _findmatch(listnumber, ref):')
    x=''
    listnumber=str(listnumber)
    ref = str(ref)
    print('listnumber = {0} and ref = {1}'.format(listnumber, ref))
    if ref in listnumber:
        x = listnumber
    print('x = {0}'.format(x))
    return x 

with cf.ProcessPoolExecutor(max_workers=workers) as executor:
    #for n in executor.map(_findmatch, numberlist):
    for n in executor.map(_findmatch, numberlist, itertools.repeat(5)):
        print(type(n))
        print(n)
        #if str(ref[0]) in n:
        #    print('match')
6
tdelaney

Concernant votre première question, est-ce que je comprends bien que vous voulez passer un argument dont la valeur est déterminée uniquement au moment où vous appelez map mais constante pour toutes les instances de la fonction mappée? Si c'est le cas, je ferais le map avec une fonction dérivée d'une "fonction modèle" avec le deuxième argument (ref dans votre exemple) incorporé en utilisant functools.partial:

from functools import partial
refval = 5

def _findmatch(ref, listnumber):  # arguments swapped
    ...

with cf.ProcessPoolExecutor(max_workers=workers) as executor:
    for n in executor.map(partial(_findmatch, refval), numberlist):
        ...

Ré. question 2, première partie: je n'ai pas trouvé le morceau de code exact qui tente de décaper (sérialiser) la fonction qui devrait ensuite être exécutée en parallèle, mais il semble naturel que cela se produise - non seulement les arguments mais aussi la fonction doit être transférée aux travailleurs d'une manière ou d'une autre, et elle doit probablement être sérialisée pour ce transfert. Le fait que les fonctions partial peuvent être décapées alors que lambdas ne le sont pas est mentionné ailleurs, par exemple ici: https://stackoverflow.com/a/19279016/6356764 .

Ré. question 2, deuxième partie: si vous vouliez appeler une fonction avec plus d'un argument dans ProcessPoolExecutor.map, vous lui passeriez la fonction comme premier argument, suivi d'un itérable des premiers arguments de la fonction, suivi d'un itérable de ses seconds arguments, etc. Dans votre cas:

for n in executor.map(_findmatch, numberlist, ref):
    ...
4
mkorvas