Existe-t-il un moyen simple de sélectionner une fonction python (ou de sérialiser autrement son code)?


100

J'essaye de transférer une fonction sur une connexion réseau (en utilisant asyncore). Existe-t-il un moyen simple de sérialiser une fonction python (qui, dans ce cas au moins, n'aura aucun effet secondaire) pour un transfert comme celui-ci?

J'aimerais idéalement avoir une paire de fonctions similaires à celles-ci:

def transmit(func):
    obj = pickle.dumps(func)
    [send obj across the network]

def receive():
    [receive obj from the network]
    func = pickle.loads(s)
    func()

Réponses:


120

Vous pouvez sérialiser le bytecode de la fonction, puis le reconstruire sur l'appelant. Le module marshal peut être utilisé pour sérialiser des objets de code, qui peuvent ensuite être réassemblés en une fonction. c'est à dire:

import marshal
def foo(x): return x*x
code_string = marshal.dumps(foo.func_code)

Puis dans le processus distant (après avoir transféré code_string):

import marshal, types

code = marshal.loads(code_string)
func = types.FunctionType(code, globals(), "some_func_name")

func(10)  # gives 100

Quelques mises en garde:

  • Le format de marshal (n'importe quel bytecode python d'ailleurs) peut ne pas être compatible entre les principales versions de python.

  • Ne fonctionnera que pour l'implémentation cpython.

  • Si la fonction fait référence à des globaux (y compris des modules importés, d'autres fonctions, etc.) que vous devez récupérer, vous devrez également les sérialiser ou les recréer du côté distant. Mon exemple lui donne simplement l'espace de noms global du processus distant.

  • Vous devrez probablement faire un peu plus pour prendre en charge des cas plus complexes, tels que les fermetures ou les fonctions de générateur.


1
En Python 2.5, le "nouveau" module est obsolète. 'new.function' devrait être remplacé par 'types.FunctionType', après un "import types", je crois.
Eric O Lebigot

2
Merci. Ceci est exactement ce que je cherchais. Basé sur quelques tests superficiels, il fonctionne tel quel pour les générateurs.
Michael Fairley

2
Si vous lisez les deux premiers paragraphes du module marshal, vous voyez qu'il suggère fortement d'utiliser pickle à la place? Idem pour la page cornichon. docs.python.org/2/library/marshal.html
dgorissen

1
J'essaye d'appliquer le marshalmodule pour sérialiser un dictionnaire de dictionnaires initialisé en tant que defaultdict(lambda : defaultdict(int)). Mais cela renvoie l'erreur ValueError: unmarshallable object. Notez que j'utilise python2.7. Une idée? Merci
user17375

2
Sur Python 3.5.3, foo.func_codeaugmente AttributeError. Existe-t-il un autre moyen d'obtenir le code de fonction?
AlQuemist

41

Découvrez Dill , qui étend la bibliothèque pickle de Python pour prendre en charge une plus grande variété de types, y compris des fonctions:

>>> import dill as pickle
>>> def f(x): return x + 1
...
>>> g = pickle.dumps(f)
>>> f(1)
2
>>> pickle.loads(g)(1)
2

Il prend également en charge les références aux objets dans la fermeture de la fonction:

>>> def plusTwo(x): return f(f(x))
...
>>> pickle.loads(pickle.dumps(plusTwo))(1)
3

2
dill fait également un très bon travail pour obtenir le code source des fonctions et des lambdas et les enregistrer sur le disque, si vous préférez cela au décapage d'objets.
Mike McKerns

14

Pyro est capable de le faire pour vous .


Je devrais m'en tenir à la bibliothèque standard pour ce projet particulier.
Michael Fairley

21
Mais cela ne veut pas dire que vous ne pouvez pas regarder le code de Pyro pour voir comment c'est fait :)
Aaron Digulla

4
@ AaronDigulla- vrai, mais il convient de mentionner qu'avant de lire une seule ligne de code publié par quelqu'un d'autre, vous devez toujours vérifier la licence du logiciel. Lire le code de quelqu'un d'autre et réutiliser les idées sans citer la source ou respecter les contraintes de licence / copie peut être considéré comme du plagiat et / ou une violation du droit d'auteur dans de nombreux cas.
mdscruggs

12

Le moyen le plus simple est probablement inspect.getsource(object)(voir le module inspect ) qui retourne une chaîne avec le code source d'une fonction ou d'une méthode.


Cela semble bon, sauf que le nom de la fonction est explicitement défini dans le code, ce qui est légèrement problématique. Je pourrais supprimer la première ligne du code, mais c'est cassable en faisant quelque chose comme 'def \ / n func ():'. Je pourrais choisir le nom de la fonction avec la fonction elle-même, mais je n'aurais aucune garantie que le nom ne se heurterait pas, ou je devrais mettre la fonction dans un wrapper, ce qui n'est toujours pas la solution la plus propre, mais cela pourrait avoir à faire.
Michael Fairley

1
Notez que le module inspect demande simplement à la fonction où elle a été définie, puis lit ces lignes à partir du fichier de code source - ce qui n'est guère sophistiqué.
trop de php

1
Vous pouvez trouver le nom de la fonction en utilisant son attribut .__ name__. Vous pouvez faire un remplacement regex sur ^ def \ s * {nom} \ s * (et lui donner le nom que vous voulez. Ce n'est pas infaillible, mais cela fonctionnera pour la plupart des choses.
trop de php

6

Tout dépend si vous générez la fonction au moment de l'exécution ou non:

Si vous le faites - inspect.getsource(object)ne fonctionnera pas pour les fonctions générées dynamiquement car il obtient la source de l'objet à partir du .pyfichier, donc seules les fonctions définies avant l'exécution peuvent être récupérées comme source.

Et si vos fonctions sont de toute façon placées dans des fichiers, pourquoi ne pas leur donner accès au récepteur et ne transmettre que les noms de modules et de fonctions.

La seule solution pour les fonctions créées dynamiquement à laquelle je peux penser est de construire la fonction sous forme de chaîne avant la transmission, de transmettre la source, puis eval()du côté du récepteur.

Edit: la marshalsolution semble également assez intelligente, je ne savais pas que vous pouviez sérialiser quelque chose d'autre que des éléments intégrés



2
code_string = '' '
def foo (x):
    retour x * 2
barre de définition (x):
    retour x ** 2
'' '

obj = pickle.dumps (chaîne_code)

Maintenant

exec (pickle.loads (obj))

toto (1)
> 2
barre (3)
> 9

2

Tu peux le faire:

def fn_generator():
    def fn(x, y):
        return x + y
    return fn

Maintenant, transmit(fn_generator())enverra la définition réelle de fn(x,y)au lieu d'une référence au nom du module.

Vous pouvez utiliser la même astuce pour envoyer des classes sur le réseau.


1

Les fonctions de base utilisées pour ce module couvrent votre requête, et vous obtenez la meilleure compression sur le fil; voir le code source instructif:

module y_serial.py :: entreposer des objets Python avec SQLite

"Sérialisation + persistance :: en quelques lignes de code, compressez et annotez les objets Python dans SQLite; puis récupérez-les plus tard par mots-clés sans SQL. Module" standard "le plus utile pour une base de données pour stocker des données sans schéma."

http://yserial.sourceforge.net


1

Cloudpickle est probablement ce que vous recherchez. Cloudpickle est décrit comme suit:

cloudpickle est particulièrement utile pour le calcul en cluster où le code Python est expédié sur le réseau pour s'exécuter sur des hôtes distants, éventuellement à proximité des données.

Exemple d'utilisation:

def add_one(n):
  return n + 1

pickled_function = cloudpickle.dumps(add_one)
pickle.loads(pickled_function)(42)

0

Voici une classe d'aide que vous pouvez utiliser pour envelopper des fonctions afin de les rendre picklable. Les mises en garde déjà mentionnées marshals'appliquent, mais un effort est fait pour utiliser le cornichon chaque fois que possible. Aucun effort n'est fait pour préserver les globaux ou les fermetures lors de la sérialisation.

    class PicklableFunction:
        def __init__(self, fun):
            self._fun = fun

        def __call__(self, *args, **kwargs):
            return self._fun(*args, **kwargs)

        def __getstate__(self):
            try:
                return pickle.dumps(self._fun)
            except Exception:
                return marshal.dumps((self._fun.__code__, self._fun.__name__))

        def __setstate__(self, state):
            try:
                self._fun = pickle.loads(state)
            except Exception:
                code, name = marshal.loads(state)
                self._fun = types.FunctionType(code, {}, name)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.