读取pickle文件时出现AttributeError错误

18

当我在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文件。每次尝试时,都会抛出错误。
有什么想法为什么会出现这种奇怪的行为吗?
谢谢!

您在 Spyder 中使用的 Python 版本可能与命令行中使用的版本不同。您是否检查过版本号? - IonicSolutions
@IonicSolutions,你能告诉我如何正确地做吗?我不认为这是问题,因为我的笔记本电脑上唯一安装的版本是Python 3.6.5。Spyder是通过cmd和命令行安装的:py -3.6 -m pip install spyder。(两个工作站的情况略有不同,还安装了Python 2.7)。 - Mathieu
import sys print(sys.version) 可以让你得到解释器所使用的精确版本。 - IonicSolutions
@IonicSolutions 如预期所示:3.6.5 (v3.6.5:f59c0932b4, 2018年3月28日,17:00:18) [MSC v.1900 64位(AMD64)] - Mathieu
这个回答解决了你的问题吗?使用pickle或dill在__main__中序列化对象 - Ian Goldby
2个回答

41
当你在pickle中转储内容时,应避免将主模块中声明的类和函数进行封存。你的问题(部分原因)是因为程序中只有一个文件。pickle是惰性的,不会序列化类定义或函数定义。相反,它保存了如何找到类的引用(它所在的模块及其名称)。
当Python直接运行脚本/文件时,它将程序作为__main__模块运行(无论其实际文件名如何)。但是,当加载文件且不是主模块(例如,当您执行import program之类的操作时),则其模块名称基于其名称。因此,program.py被称为program
当您从命令行运行时,您正在执行前者,模块被称为__main__。因此,pickle创建对类的引用,例如__main__.Signal。当spyder尝试加载pickle文件时,它被告知导入__main__并查找Signal。但是,spyder的__main__模块是用于启动spyder而不是program.py的模块,因此pickle无法找到Signal
您可以通过运行(-a打印每个命令的描述)来检查pickle文件的内容。从中,您将看到类被引用为__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. 编写自定义的反序列化器
  3. 编写自定义的序列化器

以下解决方案将使用由以下代码创建的pickle文件out.pkl(在名为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)

好的,非常感谢您的详细解释。对于已经计算好的 .pkl 文件,在 Spyder 中访问它们的唯一方法是像我目前正在做的那样编写一个程序来加载它们,然后通过自定义方法(通过 cmd)重新序列化它们。正确吗? - Mathieu
在这种情况下,我建议尝试使用第一种解决方案(将类保持在__main__模块之外)。完成后,使用自定义反序列化器加载任何现有的pickle文件,然后再次保存它们(这将使用非__main__引用保存它们)。 - Dunes
好的,但是现在我只明白了一件事:我实际上已经在做这个了。我的类在# -*- coding: utf-8 -*-和导入之后定义在程序的顶部;然后我有函数,最后是这个:if __name__ == '__main__':,接着是多进程程序,调用上面定义的函数。 - Mathieu
这是否意味着我需要将类放在另一个文件中并进行导入? - Mathieu
收到一个错误信息:“AttributeError: 模块 'program' 没有 'MyClass' 属性”。 - echan00
显示剩余2条评论

5
我认为扩展Python的pickle的dill模块可能是一个选择。不需要像__main__那样输入模块路径。
只需使用dill替换pickle即可。
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

我之前不知道dill,谢谢你,它看起来很有用! - Mathieu
非常感谢您!我强烈推荐使用dill来腌制带有嵌入类和复杂函数的对象。 - Prithvi Shetty

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接