读取 pickle 文件时出现 AttributeError

AttributeError when reading a pickle file

当我在 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 程序中定义了一个classSignal以及很多函数。下面提供了该程序的简化概述:

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 将 return 一个元组列表,格式如下:

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 文件。每次尝试都会抛出错误。

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

谢谢!

当您将内容转储到 pickle 中时,您应该避免 pickling classes 和在主模块中声明的函数。您的问题(部分)是因为您的程序中只有一个文件。 pickle 是惰性的,不会序列化 class 定义或函数定义。相反,它保存了如何找到 class(它所在的模块及其名称)的参考。

当python 直接运行script/file 时,它将程序作为__main__ 模块运行(无论其实际文件名如何)。然而,当一个文件被加载并且 不是 主模块时(例如,当你做类似 import program 的事情时),那么它的模块名称是基于它的名称。所以 program.py 被称为 program.

当您从命令行 运行 执行前者时,该模块称为 __main__。因此,pickle 会创建对 class 的引用,例如 __main__.Signal。当 spyder 尝试加载 pickle 文件时,它被告知导入 __main__ 并查找 Signal。但是,spyder 的 __main__ 模块是用于启动 spyder 而不是你的 program.py 的模块,因此 pickle 无法找到 Signal

您可以通过 运行 检查 pickle 文件的内容(-a 是打印每个命令的描述)。从这里你会看到你的 class 被引用为 __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__ 模块中定义的 classes 的实例。最简单和最好的解决方案。而是将这些 classes 移动到另一个模块,或者编写一个 main.py 脚本来调用你的程序(两者都意味着这样的 classes 不再在 __main__ 模块中找到) .
  2. 编写自定义反序列化器
  3. 编写自定义序列化程序

以下解决方案将使用由以下代码创建的名为 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 如何反序列化各种 classes。所以在这里,你要做的是告诉 pickle 反序列化 __main__ classes 的所有实例,就好像它们是 program classes 的实例一样。您需要为每个 class

注册一个自定义序列化程序
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)

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

只需使用 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