读取pickle文件时的AttributeError

时间:2018-05-22 10:12:15

标签: python python-3.x pickle spyder

当我在spyder(python 3.6.5)上读取我的.pkl文件时出现以下错误:

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 data

def 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启动程序时,我从未遇到此错误。该程序永远不会崩溃抛出此错误,并且没有明显的问题。

然而,我无法在spyder中读取.pkl文件。每次我尝试时,都会抛出错误。

我知道为什么会出现这种奇怪的行为?

谢谢!

2 个答案:

答案 0 :(得分:5)

当您在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. 编写自定义序列化程序
  4. 以下解决方案将使用由以下代码创建的名为out.pkl的pickle文件(在名为program.py的文件中):

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

    自定义反序列化解决方案

    您可以编写一个客户反序列化器,知道何时遇到对__main__模块的引用,您真正想要的是program模块。

    import pickle
    
    class 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)
    

    这是加载已创建的pickle文件的最简单方法。该程序是它将责任强加到反序列化代码上,当序列化代码真正负责正确创建pickle文件时。

    自定义序列化解决方案

    与之前的解决方案相比,您可以确保任何人都可以轻松地对序列化的pickle对象进行反序列化,而无需了解自定义反序列化逻辑。为此,您可以使用copyreg模块通知pickle如何反序列化各种类。所以在这里,您要做的是告诉pickle__main__类的所有实例反序列化,就好像它们是program类的实例一样。您需要为每个类注册一个自定义序列化程序

    import program
    import pickle
    import copyreg
    
    class MyClass:
        def __init__(self, name):
            self.name = name
    
    def 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)
    

答案 1 :(得分:1)

我认为扩展 python 的泡菜的 dill 模块可能是一个选择。不需要模块路径,比如__main__

只需使用 let pickle 替换为 dill

import dill

# The functions
def write_file(data, folder_path, file_name):
    with open(join(folder_path, file_name), "wb") as output:
        dill.dump(data, output)

def read_file(folder_path, file_name):
    with open(join(folder_path, file_name), "rb") as input:
        data= dill.load(input)
    return data