AttributeError при чтении файла pickle

Mathieu спросил: 28 апреля 2018 в 08:32 в: python

Я получаю следующую ошибку, когда я читаю свои .pkl-файлы на spyder (python 3.6.5):

IN: with open(file, "rb") as f:
       data = pickle.load(f)  Traceback (most recent call last): File "<ipython-input-5-d9796b902b88>", line 2, in <module>
   data = pickle.load(f)AttributeError: Can't get attribute 'Signal' on <module '__main__' from 'C:\\Python36\\lib\\site-packages\\spyder\\utils\\ipython\\start_kernel.py'>

Контекст:

Моя программа состоит из одного файла: program.py В программе определен класс Signal, а также многие функции. Ниже приведен упрощенный обзор программы:

import numpy as np
import _pickle as pickle
import os# The unique class
class Signal:
    def __init__(self, fq, t0, tf):
        self.fq = fq
        self.t0 = t0
        self.tf = tf
        self.timeline = np.round(np.arange(t0, tf, 1/fq*1000), 3)# The functions
def write_file(data, folder_path, file_name):
    with open(join(folder_path, file_name), "wb") as output:
        pickle.dump(data, output, -1)def read_file(folder_path, file_name):
    with open(join(folder_path, file_name), "rb") as input:
        data= pickle.load(input)
    return datadef compute_data(# parameters):
    # do stuff

Функция compute_data вернет список кортежей формы:

data = [((Signal_1_1, Signal_1_2, ...), val 1), ((Signal_2_1, Signal_2_2, ...), val 2)...]

С, конечно, Signal_i_k является объектом Signal. Этот список будет сохранен в формате .pkl. Более того, я выполняю много итераций с различными параметрами для функций compute_data. Многие итерации будут использовать прошедшие вычисленные данные в качестве отправной точки и, таким образом, будут читать соответствующие и необходимые файлы .pkl. Наконец, я использую несколько компьютеров одновременно, каждый из которых сохраняет вычисленные данные в локальной сети. Таким образом, каждый компьютер может получить доступ к данным, сгенерированным другими, и использовать его в качестве отправной точки.

Вернуться к ошибке:

Моя основная проблема заключается в том, что У меня никогда не было этой ошибки, когда я запускаю свои программы, дважды щелкнув файл или Windows cmd или PowerShell. Программа никогда не сбрасывает эту ошибку и запускается без видимых проблем.

Однако я не могу прочитать файл .pkl в spyder. Каждый раз, когда я пытаюсь, возникает ошибка.

Любая идея, почему я получил это странное поведение?

Спасибо!


1 ответ

Есть решение
Dunes ответил: 28 апреля 2018 в 03:08

Когда вы выгружаете материал в pickle, вам следует избегать классов травления и функций, объявленных в основном модуле. Ваша проблема (частично), потому что у вас есть только один файл в вашей программе. pickle ленив и не сериализует определения классов или определения функций. Вместо этого он сохраняет ссылку на то, как найти класс (модуль, в котором он живет, и его имя).

Когда python запускает скрипт / файл, он запускает программу как __main__ (независимо от его фактического имени файла). Однако, когда файл загружен и не главный модуль (например, когда вы делаете что-то вроде import program), его имя модуля основано на его имени. Поэтому program.py получает вызов program.

Когда вы работаете из командной строки, вы делаете первую, а модуль называется __main__. Таким образом, pickle создает ссылки на ваши классы, такие как __main__.Signal. Когда spyder пытается загрузить файл pickle, ему предлагается импортировать __main__ и искать Signal. Но модуль spyder __main__ - это модуль, который используется для запуска spyder, а не вашего program.py, и поэтому pickle не находит Signal.

Вы можете проверить содержимое файла pickle, выполнив (-a распечатывает описание каждой команды). Из этого вы увидите, что на ваш класс ссылаются как __main__.Signal.

python -m pickletools -a file.pkl

И вы увидите что-то вроде:

    0: \x80 PROTO      3              Protocol version indicator.
    2: c    GLOBAL     '__main__ Signal' Push a global object (module.attr) on the stack.
   19: q    BINPUT     0                 Store the stack top into the memo.  The stack is not popped.
   21: )    EMPTY_TUPLE                  Push an empty tuple.
   22: \x81 NEWOBJ                       Build an object instance.
   23: q    BINPUT     1                 Store the stack top into the memo.  The stack is not popped.
   ...
   51: b    BUILD                        Finish building an object, via __setstate__ or dict update.
   52: .    STOP                         Stop the unpickling machine.
highest protocol among opcodes = 2

Доступно несколько решений:

  1. Не сериализуйте экземпляры классов, которые определены в вашем модуле __main__. Самое простое и лучшее решение. Вместо этого переместите эти классы в другой модуль или напишите сценарий main.py для вызова вашей программы (оба будут означать, что такие классы больше не будут найдены в модуле __main__).
  2. Напишите пользовательский derserialiser
  3. Напишите пользовательский сериализатор

Следующие решения будут работать с файлом pickle с именем out.pkl, созданным следующим код (в файле с именем program.py):

import pickleclass MyClass:
    def __init__(self, name):
        self.name = nameif __name__ == '__main__':
    o = MyClass('test')
    with open('out.pkl', 'wb') as f:
        pickle.dump(o, f)

Решение пользовательского десеризатора

Вы можете написать десеризатор клиента, который знает, когда он встречает ссылку на модуль __main__, что вы действительно имеете в виду, это модуль program.

import pickleclass MyCustomUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == "__main__":
            module = "program"
        return super().find_class(module, name)with open('out.pkl', 'rb') as f:
    unpickler = MyCustomUnpickler(f)
    obj = unpickler.load()print(obj)
print(obj.name)

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

В отличие от предыдущего решения вы можете убедиться, что сериализованный маринованные объекты могут быть легко десериализированы кем угодно, не зная индивидуальной логики десериализации. Для этого вы можете использовать модуль copyreg, чтобы сообщить pickle, как десериализовать различные классы. Итак, здесь вы должны сказать pickle десериализировать все экземпляры классов __main__, как если бы они были экземплярами классов program. Вам нужно будет зарегистрировать собственный сериализатор для каждого класса

import program
import pickle
import copyregclass MyClass:
    def __init__(self, name):
        self.name = namedef pickle_MyClass(obj):
    assert type(obj) is MyClass
    return program.MyClass, (obj.name,)copyreg.pickle(MyClass, pickle_MyClass)if __name__ == '__main__':
    o = MyClass('test')
    with open('out.pkl', 'wb') as f:
        pickle.dump(o, f)
Mathieu ответил: 29 апреля 2018 в 05:53
Хорошо спасибо за великолепное объяснение. Для уже вычисленных файлов .pkl единственный способ получить к ним доступ в spyder будет заключаться в том, чтобы программа загружала их, как я сейчас делаю, а затем ресериализую их с помощью настраиваемого метода (через cmd). Правильный?
Dunes ответил: 29 апреля 2018 в 07:20
В этом случае я попытаюсь перейти к использованию первого решения (сохраняя ваши классы из модуля __main__.) После того, как вы это сделали, загрузите любые существующие файлы pickle с помощью настраиваемого десериализатора, а затем сохраните их снова (это сохранит их с помощью ссылок __main__).
Mathieu ответил: 30 апреля 2018 в 05:52
Хорошо, но теперь я понял только одно: я на самом деле уже это делаю. Мой класс определяется в верхней части моей программы после # -*- coding: utf-8 -*- и импорта; затем я получил функции и, наконец, следующее: if __name__ == '__main__': с последовательностью программы многопроцессорности, которая вызывает функции, определенные выше.
Mathieu ответил: 30 апреля 2018 в 05:53
Означает ли это, что я должен поместить класс в другой файл и импортировать его?