如何正确地对一个namedtuple实例进行pickle

71

我正在学习如何使用pickle。我创建了一个namedtuple对象,将其添加到列表中,并尝试对该列表进行pickle。但是,我遇到了以下错误:

pickle.PicklingError: Can't pickle <class '__main__.P'>: it's not found as __main__.P

我发现如果不把代码包装在一个函数内运行,它可以完美地工作。当将对象包装在函数内时,是否需要额外的步骤来进行pickle处理?
以下是我的代码:
from collections import namedtuple
import pickle

def pickle_test():
    P = namedtuple("P", "one two three four")
    my_list = []
    abe = P("abraham", "lincoln", "vampire", "hunter")
    my_list.append(abe)
    with open('abe.pickle', 'wb') as f:
        pickle.dump(abe, f)
    
pickle_test()

1
不幸的是,pickle 似乎无法很好地处理命名元组。 - Antimony
10
@Antimony:pickle 可以很好地处理 namedtuple 类;但是在函数局部命名空间中定义的类就不行了。 - Martijn Pieters
2
可能是重复的问题:Python:无法pickle类型X,属性查找失败 - Air
@AirThomas 这个问题已经在一年前被提出和回答了 :) - Dirty Penguin
2
没关系,我只是觉得很有趣。问题链接确实非常有用 :) - Dirty Penguin
显示剩余3条评论
5个回答

97

在函数外创建一个命名元组 outside

from collections import namedtuple
import pickle

P = namedtuple("P", "one two three four")

def pickle_test():
    my_list = []
    abe = P("abraham", "lincoln", "vampire", "hunter")
    my_list.append(abe)
    with open('abe.pickle', 'wb') as f:
        pickle.dump(abe, f)

pickle_test()

现在,pickle 可以找到它了;它已经成为一个模块全局变量。当反序列化时,所有 pickle 模块需要做的就是再次定位 __main__.P。在你的版本中,P 是一个局部变量,属于 pickle_test() 函数,这不可被内省或导入。

请注意,pickle 只存储从类的 __name__ 属性获取的模块和类名。确保 namedtuple() 调用的第一个参数匹配你分配给的全局变量;P.__name__ 必须是 "P"

记住,namedtuple() 是一个类工厂;你提供参数,它返回一个类对象,供你创建实例。pickle仅存储实例中包含的数据,以及对原始类的字符串引用,以便重新构造实例。


10
那么,如果我需要在运行时才知道字段名称,因此动态创建namedtuple,有没有办法解决这个问题?我尝试在类外创建另一个方法,但这并没有起作用。 - Chuim
8
将它分配给你的模块全局变量(使用globals()获取映射),名称保持相同,然后pickle仍然可以找到它。 - Martijn Pieters
似乎在Python 3.x上无法工作。 - facehugger
@facehugger:这种技术在Python 3中同样适用于Python 2。_测试代码_是针对Python 2特定的,但您可以通过将““w””替换为“wb”(以二进制模式打开文件)来轻松修改它。我已经编辑了问题和答案以进行更改。 - Martijn Pieters
@facehugger:请注意,命名元组的变量名称必须与传递给namedtuple函数的名称匹配。在这种情况下,它们都是“P”。如果这些名称不同,则属性查找将失败。 - MRule
显示剩余6条评论

14
我在另一个帖子中找到了这个答案
为了使腌制工作正常,分配给namedtuple的变量必须与namedtuple本身具有相同的名称。
group_t =            namedtuple('group_t', 'field1, field2')  # this will work
mismatched_group_t = namedtuple('group_t', 'field1, field2')  # this will throw the error

11
在我将我的问题作为评论添加到主要答案后,我发现了一种解决动态创建namedtuple可以序列化的方法。在我的情况下,这是必需的,因为我只有在运行时(在DB查询之后)才能确定其字段。我所做的就是通过有效地将其移动到__main__模块中来“猴子补丁”namedtuple
def _CreateNamedOnMain(*args):
    import __main__
    namedtupleClass = collections.namedtuple(*args)
    setattr(__main__, namedtupleClass.__name__, namedtupleClass)
    namedtupleClass.__module__ = "__main__"
    return namedtupleClass

请注意,namedtuple的名称(由args提供)可能会在不小心的情况下覆盖__main__中的另一个成员。


20
只需将其设置为 globals(),如下所示:globals()[namedtupleClass.__name__] = namedtupleClass。然后就不需要设置 __module__ - Martijn Pieters
当我尝试使用globals()[namedtupleClass.__name__] = namedtupleClass时,确实允许我对对象进行pickle,但是当我尝试取消pickle时,它没有所需的namedtupleClass。我的建议是只使用字典,直到他们使pickle足够聪明才能做到这一点。 - Teque5
@Teque5 只要你传递给 namedtuple() 的名称在模块中是唯一的,它就可以正常工作。 - Hubert Kario

6

或者您可以使用 cloudpickledill 进行序列化:

from collections import namedtuple

import cloudpickle
import dill



def dill_test(dynamic_names):
    P = namedtuple('P', dynamic_names)
    my_list = []
    abe = P("abraham", "lincoln", "vampire", "hunter")
    my_list.append(abe)
    with open('deleteme.cloudpickle', 'wb') as f:
        cloudpickle.dump(abe, f)
    with open('deleteme.dill', 'wb') as f:
        dill.dump(abe, f)


dill_test("one two three four")

2
这里的问题在于子进程无法导入对象的类 - 在本例中,对象的类是P类。在多模型项目中,P类应该可以在任何使用子进程的地方导入。
一个快速的解决方法是将其分配给全局变量以使其可导入。
globals()["P"] = P

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