Сохранять пользовательские атрибуты при подгонке подкласса массива numpy

Я создал подкласс numpy ndarray после документации numpy. В частности, я добавил пользовательский атрибут, изменив предоставленный код.

Я манипулирую экземпляры этого класса в параллельном цикле, используя Python multiprocessing. Как я понимаю, способ, по которому объем по существу "копируется" в несколько потоков, использует pickle.

Проблема, с которой я сейчас сталкиваюсь, связана с тем, как массируются массивы numpy. Я не могу найти исчерпывающей документации об этом, но некоторые обсуждения между разработчиками укропов предполагают, что я должен сосредоточиться на методе __reduce__, который вызывает травление.

Может ли кто-нибудь пролить свет на это? Минимальный рабочий пример - это просто код примера numpy, связанный с ним выше, скопированный здесь для полноты:

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

Спасибо.

Ответ 1

np.ndarray использует __reduce__ для рассоления. Мы можем взглянуть на то, что он действительно возвращает, когда вы вызываете эту функцию, чтобы получить представление о том, что происходит:

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

Итак, мы получаем 3-кортеж назад. Документы для __reduce__ описывают, что делает каждый элемент:

Когда возвращается кортеж, он должен быть между двумя и пятью элементами длинный. Необязательные элементы могут быть опущены или None могут быть предоставлены как их ценность. Содержимое этого кортежа мариновали, как обычно, и используется для восстановления объекта при расписании. Семантика каждый элемент:

  • Вызываемый объект, который будет вызываться для создания начальной версии объект. Следующий элемент кортежа предоставит аргументы для этот вызываемый и последующие элементы предоставляют дополнительную информацию о состоянии которые впоследствии будут использованы для полной реконструкции маринованных данных.

    В непривязанной среде этот объект должен быть либо классом, либо может быть зарегистрирован как "безопасный конструктор" (см. ниже), или он должен имеют атрибут __safe_for_unpickling__ с истинным значением. В противном случае a UnpicklingError будет поднят в раскалывающей Окружающая среда. Обратите внимание, что, как обычно, вызываемый сам маринован имя.

  • Кортеж аргументов для вызываемого объекта.

  • Необязательно, состояние объектов, которое будет передано объектам __setstate__(), как описано в разделе Травление и рассыпание экземпляров обычного класса. Если объект не имеет метода __setstate__()то, как и выше, значение должно быть словарем, и оно будет добавлено к объекты __dict__.

Итак, _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')) передается классу '__setstate__. Это дает нам возможность; мы можем переопределить __reduce__ и предоставить наш собственный кортеж __setstate__, а затем дополнительно переопределить __setstate__, чтобы установить наш пользовательский атрибут, когда мы распаковываем. Нам просто нужно убедиться, что мы сохраняем все данные, которые должен выполнять родительский класс, и также вызываем родительский __setstate__:

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 __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 __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 __setstate__ with the other tuple elements.
        super(RealisticInfoArray, self).__setstate__(state[0:-1])

Использование:

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

Ответ 2

Я автор dillpathos). dill травил numpy.array, прежде чем numpy мог сделать это сам. @dano описание довольно точно. Я лично, я просто использую dill и позволяю ему выполнять эту работу за вас. С dill вам не нужно __reduce__, так как dill имеет несколько способов захвата атрибутов подкласса... один из которых хранит __dict__ для любого объекта класса. pickle не делает этого, b/c обычно работает с классами по ссылке имени и не сохраняет сам объект класса... поэтому вам нужно работать с __reduce__, чтобы сделать pickle работать для вас. В большинстве случаев нет необходимости в 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 может распространяться на pickle (по существу через copy_reg все, что он знает), поэтому вы можете использовать все типы dill во всем, что использует pickle. Теперь, если вы собираетесь использовать multiprocessing, вы немного ввернуты, так как он использует cPickle. Существует, однако, pathos fork multiprocessing (называемый pathos.multiprocessing), который в основном единственное изменение заключается в том, что он использует dill вместо cPickle... и, следовательно, может сериализовать черту намного больше в Pool.map. Я думаю (в настоящее время), если вы хотите работать с вашим подклассом numpy.array в multiprocessing (или pathos.multiprocessing), вам, возможно, придется сделать что-то вроде @dano, но не уверен, подумайте о хорошем случае с моей головы, чтобы проверить свой подкласс.

Если вам интересно, получите pathos здесь: https://github.com/uqfoundation