7 votes

Exemple de classe mémorisée par Cant Pickle

Voici le code que j'utilise

import funcy

@funcy.memoize
class mystery(object):

    def __init__(self, num):
        self.num = num

feat = mystery(1)

with open('num.pickle', 'wb') as f:
    pickle.dump(feat,f)

Ce qui me donne l'erreur suivante :

PicklingError: Can't pickle <class '__main__.mystery'>: it's not the 
same object as __main__.mystery

J'espère 1) comprendre pourquoi cela se produit, et 2) trouver une solution qui me permette de récupérer l'objet (sans supprimer la mémorisation). Idéalement, la solution ne devrait pas modifier l'appel à pickle.

Exécution de python 3.6 avec funcy==1.10

9voto

abarnert Points 94246

Le problème est que vous avez appliqué à une classe un décorateur conçu pour les fonctions. Le résultat n'est pas une classe, mais une fonction qui englobe un appel à la classe. Cela pose un certain nombre de problèmes (par exemple, comme l'a fait remarquer Aran-Fey dans les commentaires, vous ne pouvez pas utiliser le décorateur pour les fonctions). isinstance(feat, mystery) parce que mystery ).

Mais le problème particulier qui vous préoccupe est que vous ne pouvez pas récupérer les instances de classes inaccessibles.

En fait, c'est en gros ce que vous dit le message d'erreur :

PicklingError: Can't pickle <class '__main__.mystery'>: it's not the 
same object as __main__.mystery

Votre feat pense que son type est __main__.mystery mais ce n'est pas du tout un type, c'est la fonction renvoyée par le décorateur qui englobe ce type.


Le moyen le plus simple de résoudre ce problème serait de trouver un décorateur de classe qui fasse ce que vous voulez. Il pourrait s'appeler quelque chose comme flyweight au lieu de memoize mais je suis sûr que de nombreux exemples existent.


Mais vous pouvez construire une classe légère en mémorisant simplement le constructeur, au lieu de mémoriser la classe :

class mystery:
    @funcy.memoize
    def __new__(cls, num):
        return super().__new__(cls)
    def __init__(self, num):
        self.num = num

bien que vous souhaitiez probablement déplacer l'initialisation dans le constructeur dans ce cas. Sinon, appeler mystery(1) et ensuite mystery(1) retournera le même objet que précédemment, mais le réinitialisera également avec self.num = 1 ce qui est au mieux un gaspillage, et au pire une erreur. Donc :

class mystery:
    @funcy.memoize
    def __new__(cls, num):
        self = super().__new__(cls)
        self.num = num
        return self

Et maintenant :

>>> feat = mystery(1)
>>> feat
<__main__.mystery at 0x10eeb1278>
>>> mystery(2)
<__main__.mystery at 0x10eeb2c18>
>>> mystery(1)
<__main__.mystery at 0x10eeb1278>

Et, parce que le type de feat est maintenant une classe qui est accessible sous le nom de module-global mystery , pickle n'auront aucun problème avec ça :

>>> pickle.dumps(feat)
b'\x80\x03c__main__\nmystery\nq\x00)\x81q\x01}q\x02X\x03\x00\x00\x00numq\x03K\x01sb.'

Vous faire Il faut encore réfléchir à la façon dont cette classe devrait jouer avec le décapage. En particulier, voulez-vous que le dépicklage passe par le cache ? Par défaut, ce n'est pas le cas :

>>> pickle.loads(pickle.dumps(feat)) is feat
False

Ce qui se passe, c'est que l'on utilise l'option par défaut __reduce_ex__ pour le décapage, qui fait par défaut l'équivalent de (seulement légèrement simplifié) :

result = object.__new__(__main__.mystery)
result.__dict__.update({'num': 1})

Si vous voulez qu'il passe par le cache, la solution la plus simple est la suivante :

class mystery:
    @funcy.memoize
    def __new__(cls, num):
        self = super().__new__(cls)
        self.num = num
        return self
    def __reduce__(self):
        return (type(self), (self.num,))

Si vous prévoyez de faire cela souvent, vous pouvez penser à écrire votre propre décorateur de classe :

def memoclass(cls):
    @funcy.memoize
    def __new__(cls, *args, **kwargs):
        return super(cls, cls).__new__(cls)
    cls.__new__ = __new__
    return cls

Mais ça :

  • est plutôt moche,
  • ne fonctionne qu'avec les classes qui n'ont pas besoin de passer des arguments de constructeur à une classe de base,
  • ne fonctionne qu'avec les classes qui n'ont pas d'attribut __init__ (ou, au moins, qui ont une idempotente et rapide __init__ qu'il est inoffensif d'appeler à plusieurs reprises),
  • ne fournit pas un moyen facile d'accrocher le décapage, et
  • ne documente ni ne teste aucune de ces restrictions.

Donc, je pense que c'est mieux d'être explicite et de mémoriser le __new__ ou écrire (ou trouver) quelque chose de beaucoup plus sophistiqué qui fait l'introspection nécessaire pour rendre la mémorisation d'une classe de cette manière totalement générale. (Ou bien, alternativement, écrire une méthode qui ne fonctionne qu'avec un ensemble restreint de classes - par exemple, une méthode @memodataclass c'est juste comme @dataclass mais avec un constructeur mémorisé serait beaucoup plus facile qu'un constructeur entièrement général @memoclass .)

1voto

Lars Ericson Points 342

Une autre approche consiste à

class _mystery(object):

    def __init__(self, num):
        self.num = num

@funcy.memoize
def mystery(num):
    return _mystery(num)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X