Травление объекта, содержащего большой массив numpy - PullRequest
3 голосов
/ 13 марта 2020

Я выбираю объект, который имеет следующую структуру:

obj
  |---metadata
  |---large numpy array

Я хотел бы получить доступ к метаданным. Однако, если я выбираю объект pickle.load () и перебираю каталог (скажем, потому что я ищу определенные метаданные c, чтобы определить, какой из них возвращать), то он становится длиннее. Я предполагаю, что pickle хочет загрузить весь объект.

Есть ли способ получить доступ только к метаданным верхнего уровня объекта, не загружая его целиком?

Я думал о поддержании индекса, но тогда это означает, что я должен реализовать логику c и поддерживать ее в актуальном состоянии, чего я бы предпочел избежать, если есть более простое решение ....

1 Ответ

2 голосов
/ 13 марта 2020

Да, обычный рассол будет загружать все. В Python 3.8 новый протокол Pickle позволяет управлять тем, как объекты сериализуются, и использовать побочный канал для большой части данных, но это в основном полезно при использовании pickle в межпроцессном взаимодействии. Это потребует индивидуальной реализации протравливания для ваших объектов.

Однако даже в более старых версиях Python можно настроить порядок сериализации объектов на диск.

Например, вместо того, чтобы ваши массивы были обычными членами ваших объектов, вы могли бы «жить» в другой структуре данных - скажем, в словаре и косвенно осуществлять доступ к данным для ваших массивов через этот словарь.

In Python версии 3.8, для этого потребуется «обмануть» настройку pickle, в том смысле, что после сериализации вашего объекта пользовательский метод должен сохранять отдельные данные в качестве побочного эффекта. Но кроме этого, оно должно быть прямым.

Если говорить более конкретно, когда у вас есть что-то вроде:


class MyObject:
     def __init__(self, data: NP.NDArray, meta_data: any):
            self.data = data
            self.meta_data = meta_data

Увеличьте это таким образом - вы все равно должны делать что угодно вы делаете с вашими объектами, но выборка теперь будет только выбирать метаданные - массивы numpy будут «жить» в отдельной структуре данных, которая не будет автоматически сериализована:


from uuid import uuid4

VAULT = dict()

class SeparateSerializationDescriptor:
    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value):
        id = instance.__dict__[self.name] = str(uuid4())
        VAULT[id] = value

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return VAULT[instance.__dict__[self.name]]

    def __delete__(self, instance):
        del VAULT[instance.__dict__[self.name]]
        del instance.__dict__[self.name]

class MyObject:

    data = SeparateSerializationDescriptor()

    def __init__(self, data: NP.NDArray, meta_data: any):
        self.data = data
        self.meta_data = meta_data

Действительно, это все, что необходимо для настройки доступа к атрибуту: все обычные варианты использования атрибута self.data будут без проблем извлекать исходный массив numpy - self.data[0:10] будет просто работать. Но в этот момент pickle будет извлекать содержимое экземпляра __dict__, который содержит только ключ к реальным данным в объекте «хранилище».

Помимо того, что вы можете сериализовать метаданные и данные в разделенные файлы, он также позволяет вам детализировать данные в памяти, манипулируя «VAULT».

А теперь, измените выбор класса, чтобы он сохранял данные отдельно на диск, и получить его при чтении. На Python 3.8 это, вероятно, может быть сделано «в рамках правил» (я возьму время, так как я отвечаю на это, чтобы взглянуть на это). Для традиционного рассола мы «нарушаем правила», в которых мы сохраняем дополнительные данные на диск, и загружаем их, как побочные эффекты сериализации.

На самом деле, мне просто пришло в голову, что я обычно настраивал методы, используемые напрямую. протокол протравливания , такой как __reduce_ex__ и __setstate__, пока будет работать, снова автоматически извлечет весь объект с диска.

Путь к go будет следующим: сериализацию, сохраните полные данные в отдельном файле и создайте еще несколько метаданных, чтобы можно было найти файл массива. После десериализации всегда загружайте только метаданные - и встраивайте в дескриптор выше механизм ленивой загрузки массивов по мере необходимости.

Итак, мы предоставляем класс Mixin, и вместо * вместо него должен вызываться метод dump pickle.dump - данные записываются в отдельных файлах. Чтобы открепить объект, обычно используйте Python pickle.load: он получит только «нормальные» атрибуты объекта. Затем метод .load() объекта может быть вызван явно для загрузки всех массивов, или он будет вызываться автоматически при первом обращении к данным, ленивым образом:

import pathlib
from uuid import uuid4
import pickle

VAULT = dict()

class SeparateSerializationDescriptor:
    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value):
        id = instance.__dict__[self.name] = str(uuid4())
        VAULT[id] = value

    def __get__(self, instance, owner):
        if instance is None:
            return self
        try:
            return VAULT[instance.__dict__[self.name]]
        except KeyError:
            # attempt so silently load missing data from disk upon first array access after unpickling:
            instance.load()
            return VAULT[instance.__dict__[self.name]]

    def __delete__(self, instance):
        del VAULT[instance.__dict__[self.name]]
        del instance.__dict__[self.name]


class SeparateSerializationMixin:

    def _iter_descriptors(self, data_dir):

        for attr in self.__class__.__dict__.values():
            if not isinstance(attr, SeparateSerializationDescriptor):
                continue
            id = self.__dict__[attr.name]
            if not data_dir:
                # use saved absolute path instead of passed in folder
                data_path = pathlib.Path(self.__dict__[attr.name + "_path"])
            else:
                data_path = data_dir / (id + ".pickle")
            yield attr, id, data_path

    def dump(self, file, protocol=None, **kwargs):
        data_dir = pathlib.Path(file.name).absolute().parent

        # Annotate paths and pickle all numpyarrays into separate files:
        for attr, id, data_path in self._iter_descriptors(data_dir):
            self.__dict__[attr.name + "_path"] = str(data_path)
            pickle.dump(getattr(self, attr.name), data_path.open("wb"), protocol=protocol)

        # Pickle the metadata as originally intended:
        pickle.dump(self, file, protocol, **kwargs)


    def load(self, data_dir=None):
        """Load all saved arrays associated with this object.

        if data_dir is not passed, the the absolute path used on picking is used. Otherwise
        the files are searched by their name in the given folder
        """
        if data_dir:
            data_dir = pathlib.Path(data_dir)

        for attr, id, data_path in self._iter_descriptors(data_dir):
            VAULT[id] = pickle.load(data_path.open("rb"))

    def __del__(self):

        for attr, id, path in self._iter_descriptors(None):
            VAULT.pop(id, None)
        try:
            super().__del__()
        except AttributeError:
            pass

class MyObject(SeparateSerializationMixin):

    data = SeparateSerializationDescriptor()

    def __init__(self, data, meta_data):
        self.data = data
        self.meta_data = meta_data

Конечно, это не так идеально, и есть, вероятно, угловые случаи. Я включил некоторые меры предосторожности на случай, если файлы данных будут перемещены в другой каталог, но я не проверял это.

Кроме этого, использование этих файлов в интерактивном сеансе здесь прошло гладко, и я смог создать MyObject экземпляр, который будет замаскирован отдельно от его атрибута data, который затем будет загружен именно тогда, когда это необходимо при снятии закалки.

Что касается предложения просто "сохранить содержимое в базе данных" - часть кода здесь может также использоваться с вашими объектами, если они находятся в базе данных, и вы предпочитаете, чтобы необработанные данные в файловой системе, а не в «столбце BLOB-объектов» в базе данных.

...