Conservar los atributos personalizados al seleccionar la subclase de la matriz numpy

He creado una subclase de numpy ndarray siguiendo la documentación de numpy . En particular, he agregado un atributo personalizado modificando el código provisto.

Estoy manipulando instancias de esta clase dentro de un bucle paralelo, usando el multiprocessing Python. Como lo entiendo, la forma en que el scope es esencialmente ‘copiado’ a varios subprocesos es utilizando pickle .

El problema al que me estoy enfrentando ahora se relaciona con la forma en que se escabullan las matrices numpy. No puedo encontrar ninguna documentación completa sobre esto, pero algunas discusiones entre los desarrolladores de eneldo sugieren que debería __reduce__ en el método __reduce__ , que se está solicitando en el proceso de decapado.

¿Alguien puede arrojar más luz sobre esto? El ejemplo de trabajo mínimo es realmente solo el código de ejemplo numpy al que he vinculado anteriormente, copiado aquí para completar:

 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) 

Ahora aquí está el 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 

Gracias.

np.ndarray usa __reduce__ para __reduce__ . Podemos ver lo que realmente devuelve cuando llama a esa función para tener una idea de lo que está sucediendo:

 >>> obj = RealisticInfoArray([1, 2, 3], info='foo') >>> obj.__reduce__() (, (, (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')) 

Por lo tanto, obtenemos una vuelta de 3 tuple. Los documentos para __reduce__ describen lo que hace cada elemento:

Cuando se devuelve una tupla, debe tener entre dos y cinco elementos de longitud. Los elementos opcionales se pueden omitir, o Ninguno se puede proporcionar como su valor. Los contenidos de esta tupla se decapan de forma normal y se utilizan para reconstruir el objeto en el momento de la eliminación. Las semánticas de cada elemento son:

  • Un objeto invocable que se llamará para crear la versión inicial del objeto. El siguiente elemento de la tupla proporcionará argumentos para este reclamable, y los elementos posteriores proporcionarán información de estado adicional que posteriormente se utilizará para reconstruir completamente los datos encurtidos.

    En el entorno de descifrado, este objeto debe ser una clase, un llamable registrado como un “constructor seguro” (ver más abajo), o debe tener un atributo __safe_for_unpickling__ con un valor verdadero. De lo contrario, se UnpicklingError un UnpicklingError en el entorno de desempaquetado. Tenga en cuenta que, como de costumbre, el nombre de la misma es decapado.

  • Una tupla de argumentos para el objeto llamable.

  • Opcionalmente, el estado del objeto, que se pasará al método __setstate__() del objeto como se describe en la sección Decapado y descifrado de instancias de clases normales. Si el objeto no tiene un __setstate__() , entonces, como arriba, el valor debe ser un diccionario y se agregará al __dict__ del objeto.

Entonces, _reconstruct es la función llamada para reconstruir el objeto, (, (0,), 'b') son los argumentos pasados ​​a esa función y (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')) se pasa a la clase’ __setstate__ . Esto nos da una oportunidad; podríamos anular __reduce__ y proporcionar nuestra propia tupla a __setstate__ , y luego, adicionalmente, anular a __setstate__ , para establecer nuestro atributo personalizado cuando no seleccionamos. Solo tenemos que asegurarnos de que conservamos todos los datos que necesita la clase principal y también llamar al __setstate__ los padres también:

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

Uso:

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

Soy el autor de dill (y pathos ). dill estaba numpy.array un numpy.array antes de que numpy pudiera hacerlo por sí mismo. La explicación de @Dano es bastante precisa. Yo personalmente, solo usaría dill y dejaría que hiciera el trabajo por ti. Con dill , no necesita __reduce__ , ya que dill tiene varias formas de __reduce__ atributos subclase … una de las cuales es almacenar __dict__ para cualquier objeto de clase. pickle no hace esto, b / c por lo general funciona con clases por referencia de nombre y no almacena el objeto de la clase en sí … así que tienes que trabajar con __reduce__ para que el pickle funcione para ti. No es necesario, en la mayoría de los casos, 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 puede extenderse a pickle (esencialmente por copy_reg todo lo que sabe), por lo que puede usar todos los tipos de dill en cualquier cosa que use pickle . Ahora, si vas a usar multiprocessing , estás un poco atornillado, ya que usa cPickle . Sin embargo, existe la bifurcación de pathos del multiprocessing (llamado pathos.multiprocessing ), que básicamente el único cambio es que usa dill lugar de cPickle … y, por lo tanto, puede serializar muchísimo más en un Pool.map . Creo que (actualmente) si quieres trabajar con tu subclase de un numpy.array en multiprocessing (o pathos.multiprocessing ), deberías hacer algo como @dano sugiere, pero no estoy seguro, ya que no pensé un buen caso de la parte superior de mi cabeza para probar tu subclase.

Si está interesado, obtenga pathos aquí: https://github.com/uqfoundation