2014-10-27 6 views
15

Ho creato una sottoclasse di numpy ndarray dopo the numpy documentation. In particolare, ho added a custom attribute modificando il codice fornito.Mantieni attributi personalizzati durante il declassamento della sottoclasse di array numpy

Sto manipolando istanze di questa classe in un ciclo parallelo, utilizzando Python multiprocessing. A quanto ho capito, il modo in cui l'oscilloscopio viene essenzialmente "copiato" su più thread utilizza pickle.

Il problema che ora sto affrontando riguarda il modo in cui gli array di numpy vengono decapati. Non riesco a trovare alcuna documentazione esauriente su questo, ma alcuni discussions between the dill developers suggeriscono che dovrei concentrarmi sul metodo __reduce__, che viene chiamato al momento del decapaggio.

Qualcuno può fare più luce su questo? L'esempio di lavoro minima è in realtà solo il codice NumPy esempio ho linkato sopra, copiato qui per completezza:

import numpy as np 

class RealisticInfoArray(np.ndarray): 

    def __new__(cls, input_array, info=None): 
     # Input array is an already formed ndarray instance 
     # We first cast to be our class type 
     obj = np.asarray(input_array).view(cls) 
     # add the new attribute to the created instance 
     obj.info = info 
     # Finally, we must return the newly created object: 
     return obj 

    def __array_finalize__(self, obj): 
     # see InfoArray.__array_finalize__ for comments 
     if obj is None: return 
     self.info = getattr(obj, 'info', None) 

Ora qui è il problema:

import pickle 

obj = RealisticInfoArray([1, 2, 3], info='foo') 
print obj.info # 'foo' 

pickle_str = pickle.dumps(obj) 
new_obj = pickle.loads(pickle_str) 
print new_obj.info # raises AttributeError 

Grazie.

risposta

20

np.ndarray utilizza __reduce__ per sottaceti. Siamo in grado di dare uno sguardo a ciò che restituisce in realtà quando si chiama quella funzione per avere un'idea di quello che sta succedendo:

>>> obj = RealisticInfoArray([1, 2, 3], info='foo') 
>>> obj.__reduce__() 
(<built-in function _reconstruct>, (<class 'pick.RealisticInfoArray'>, (0,), 'b'), (1, (3,), dtype('int64'), False, '\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00')) 

Così, si ottiene un 3-tuple indietro. La documentazione per __reduce__ descrivono ciò che ogni elemento fa:

Quando una tupla viene restituita, deve essere compreso tra due e cinque elementi lunghi. Gli elementi facoltativi possono essere omessi oppure Nessuno può essere fornito come valore . Il contenuto di questa tupla viene decapato come normale e utilizzato per ricostruire l'oggetto al tempo di non prelievo. La semantica di ogni elemento sono:

  • Un oggetto richiamabile che verrà chiamato per creare la versione iniziale dell'oggetto. L'elemento successivo della tupla fornirà gli argomenti per questo callable e gli elementi successivi forniscono informazioni di stato aggiuntive che verranno successivamente utilizzate per ricostruire completamente i dati sottoposti a picking.

    Nell'ambiente deserializzazione questo oggetto deve essere o una classe, un richiamabile registrato come “costruttore sicuro” (vedi sotto), o deve avere un attributo __safe_for_unpickling__ con un valore vero. In caso contrario, un UnpicklingError verrà generato nell'ambiente deselezionante. Si noti che, come al solito, lo stesso callable viene decapitato dal nome .

  • Una tupla di argomenti per l'oggetto callable.

  • Facoltativamente, lo stato dell'oggetto, che verrà passato al metodo dell'oggetto __setstate__() come descritto nella sezione decapaggio e normali istanze di classe deserializzare.Se l'oggetto non ha il metodo __setstate__(), , come sopra, il valore deve essere un dizionario e verrà aggiunto a dell'oggetto __dict__.

Quindi, _reconstruct è la funzione chiamata per ricostruire l'oggetto, (<class 'pick.RealisticInfoArray'>, (0,), 'b') sono gli argomenti passati a quella funzione, e (1, (3,), dtype('int64'), False, '\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00')) viene passato alla classe __setstate__. Questo ci dà un'opportunità; potremmo ignorare lo __reduce__ e fornire la nostra tupla a __setstate__, quindi in aggiunta ignorare __setstate__, per impostare il nostro attributo personalizzato quando annulliamo il prelievo. Abbiamo solo bisogno di essere sicuri di conservare tutti i dati della classe genitore ha bisogno, e la chiamata del genitore __setstate__, troppo:

class RealisticInfoArray(np.ndarray): 
    def __new__(cls, input_array, info=None): 
     obj = np.asarray(input_array).view(cls) 
     obj.info = info 
     return obj 

    def __array_finalize__(self, obj): 
     if obj is None: return 
     self.info = getattr(obj, 'info', None) 

    def __reduce__(self): 
     # Get the parent's __reduce__ tuple 
     pickled_state = super(RealisticInfoArray, self).__reduce__() 
     # Create our own tuple to pass to __setstate__ 
     new_state = pickled_state[2] + (self.info,) 
     # Return a tuple that replaces the parent's __setstate__ tuple with our own 
     return (pickled_state[0], pickled_state[1], new_state) 

    def __setstate__(self, state): 
     self.info = state[-1] # Set the info attribute 
     # Call the parent's __setstate__ with the other tuple elements. 
     super(RealisticInfoArray, self).__setstate__(state[0:-1]) 

Usage:

>>> obj = pick.RealisticInfoArray([1, 2, 3], info='foo') 
>>> pickle_str = pickle.dumps(obj) 
>>> pickle_str 
"cnumpy.core.multiarray\n_reconstruct\np0\n(cpick\nRealisticInfoArray\np1\n(I0\ntp2\nS'b'\np3\ntp4\nRp5\n(I1\n(I3\ntp6\ncnumpy\ndtype\np7\n(S'i8'\np8\nI0\nI1\ntp9\nRp10\n(I3\nS'<'\np11\nNNNI-1\nI-1\nI0\ntp12\nbI00\nS'\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00'\np13\nS'foo'\np14\ntp15\nb." 
>>> new_obj = pickle.loads(pickle_str) 
>>> new_obj.info 
'foo' 
+0

Grande, che ha riparato. Grazie anche per l'esempio di codice molto chiaro. In realtà sto trasferendo attraverso l'oggetto '__dict__' per renderlo più generico. Fortunatamente np.ndarray non * sembra * usarlo, quindi sono libero di usarlo per i miei scopi. – Gabriel

6

Sono l'autore dill (e pathos) . dill stava decapitando uno numpy.array prima che lo numpy potesse farlo da solo. la spiegazione di @ dano è abbastanza accurata. Io personalmente, vorrei solo usare dill e lasciare che faccia il lavoro per voi. Con dill, non è necessario __reduce__, come dill ha diversi modi in cui acquisisce gli attributi sottoclassi ... uno dei quali sta memorizzando il __dict__ per qualsiasi oggetto di classe. pickle non lo fa, b/c di solito funziona con le classi per riferimento al nome e non memorizza l'oggetto classe stesso ... quindi devi lavorare con __reduce__ per far funzionare pickle per te. Non è necessario, nella maggior parte dei casi, con dill.

>>> import numpy as np 
>>> 
>>> class RealisticInfoArray(np.ndarray): 
...  def __new__(cls, input_array, info=None): 
...   # Input array is an already formed ndarray instance 
...   # We first cast to be our class type 
...   obj = np.asarray(input_array).view(cls) 
...   # add the new attribute to the created instance 
...   obj.info = info 
...   # Finally, we must return the newly created object: 
...   return obj 
...  def __array_finalize__(self, obj): 
...   # see InfoArray.__array_finalize__ for comments 
...   if obj is None: return 
...   self.info = getattr(obj, 'info', None) 
... 
>>> import dill as pickle 
>>> obj = RealisticInfoArray([1, 2, 3], info='foo') 
>>> print obj.info # 'foo' 
foo 
>>> 
>>> pickle_str = pickle.dumps(obj) 
>>> new_obj = pickle.loads(pickle_str) 
>>> print new_obj.info 
foo 

dill può estendersi in pickle (essenzialmente copy_reg tutto quello che sa), in modo da poter poi usare tutti i tipi dill in tutto ciò che utilizza pickle. Ora, se hai intenzione di usare multiprocessing, sei un po 'fregato, dal momento che usa cPickle. C'è, tuttavia, il fork pathos di multiprocessing (chiamato pathos.multiprocessing), che in pratica l'unica modifica è che utilizza dill anziché cPickle ... e quindi può serializzare molto di più in un Pool.map. Penso (al momento) se vuoi lavorare con la tua sottoclasse di numpy.array in multiprocessing (o pathos.multiprocessing), potresti dover fare qualcosa come suggerisce @dano - ma non sono sicuro, dato che non pensavo a una buona causa la parte superiore della mia testa per testare la tua sottoclasse.

Se siete interessati, ottenere pathos qui: https://github.com/uqfoundation

+0

Risposta brillante, grazie mille. L'unico motivo per cui non ho accettato questo è perché il suggerimento di @ dano funziona senza cambiare pacchetto. FWIW, sono d'accordo sul fatto che aneto è sempre preferibile sottaceto e proverà a valutare il pathos in futuro. – Gabriel

+0

Gabriel, In questo caso, avrei anche preso la risposta di @ dano sulla mia, ma ho pensato che più informazioni per te erano migliori. :) –