无法对defaultdict进行pickle处理

60

我有一个类似这样的defaultdict:

dict1 = defaultdict(lambda: defaultdict(int))

问题是,我无法使用cPickle对其进行pickle。我在这里发现的解决方案之一是使用模块级函数而不是lambda表达式。我的问题是,什么是模块级函数?我如何使用字典和cPickle?

10个回答

77

除了 Martijn的解释 之外:

模块级函数是在模块级别定义的函数,这意味着它不是类的实例方法,也不是嵌套在另一个函数中的函数,而是一个有名称的“真实”函数,而不是一个 lambda 函数。

因此,为了将您的 defaultdict 进行 pickle 化,请使用模块级函数创建它,而不是使用 lambda 函数:

def dd():
    return defaultdict(int)

dict1 = defaultdict(dd) # dd is a module-level function

你可以腌制它

tmp = pickle.dumps(dict1) # no exception
new = pickle.loads(tmp)

这个解决方案抛出了一个错误,但是 def dd(): return 'something' mydict = defaultdict(dd) 起作用了。 - joshi123
1
你说得对,@joshi123。为了让它正常工作,需要将返回的 defaultdict(int) 替换为实际的默认值。在我的情况下,我返回了一个字符串字面量。然后是 'return 'n''。谢谢! - lwb
刚刚意识到 @Addishiwot Shimels 已经在下面回答了这个问题。 - joshi123
@joshi123,你遇到了哪个错误?这段代码运行得很好:https://ideone.com/ZoNKnh。另外,我不明白Addishiwot Shimels的答案与我的有何不同,因为它们都是一样的:用模块级函数替换lambda函数。 - sloth
@sloth现在似乎无法再现它,抱歉我的错误。我认为调用defaultdict两次有点令人困惑。 - joshi123
@joshi123 是的,我同意那很令人困惑。 - sloth

21

Pickle希望存储所有实例属性,defaultdict实例存储对default可调用对象的引用。Pickle递归处理每个实例属性。

Pickle无法处理lambda函数;pickle只能处理数据,而不是代码,而lambda函数包含代码。函数可以被pickled,但是像类定义一样,只有在函数可以被导入时才能pickled。在模块级别定义的函数可以被导入。在这种情况下,Pickle仅存储一个字符串,即要导入和在反pickling时引用的函数的完整'路径'。


14
你可以使用partial来实现这一点:
>>> from collections import defaultdict
>>> from functools import partial
>>> pickle.loads(pickle.dumps(defaultdict(partial(defaultdict, int))))
defaultdict(<functools.partial object at 0x94dd16c>, {})

@Fred,这基本上只是一个defaultdict,默认值为一个defaultdict(int)。该代码演示了它可以成功地被pickle。 - jamylak

7

为了实现这一点,只需编写您想要编写的代码。我建议使用dill,它可以序列化lambda和defaultdict。Dill可以序列化Python中几乎任何东西。

>>> import dill
>>> from collections import defaultdict
>>>
>>> dict1 = defaultdict(lambda: defaultdict(int))
>>> pdict1 = dill.dumps(dict1)
>>> _dict1 = dill.loads(pdict1)
>>> _dict1
defaultdict(<function <lambda> at 0x10b31b398>, {})

这个很好用。有没有一种方法可以将dict1转储到临时文件中,然后再重新加载它?类似于pickle操作从文件中读写的方式。 - Hypothetical Ninja
1
当然。dill 提供了通常的 dumpload,可以像 pickle 中的 dumpload 一样使用。此外,您可能还想查看 dill.temp.dump,它可以将数据转储到一个 NamedTemporaryFile 中。 - Mike McKerns
谢谢,可以查看我的个人资料上的最新问题。你可以在那里发布你的答案。 :) - Hypothetical Ninja

7
这是一个仍然适用于这种情况的单行解决方案,实际上比lambda函数(或等效的def函数)更高效。
dict1 = defaultdict(defaultdict(int).copy)

这只是创建了一个模板defaultdict(int),并将其copy方法绑定为外部defaultdict的默认工厂。里面的所有内容都是可选的,并且在CPython(其中defaultdict是用C实现的内置类型)中,它比调用任何用户定义的函数更有效地完成相同的工作。不需要额外的导入、包装等操作。


1
优雅的解决方案 - abhinonymous
美妙的想法! - Oliver Baumann
1
@OliverBaumann: 谢谢!恰巧,有关性能的评论不再适用(请参见此处答案和评论的更新),尽管这可能是暂时的问题(他们优化了影响lambda的代码路径,defaultdict(int).copy的代码路径可以进一步优化并且应该能够击败lambda如果这样做)。虽然它仍然是一种pickle友好的单行代码。 - ShadowRanger

4
dict1 = defaultdict(lambda: defaultdict(int))
cPickle.dump(dict(dict1), file_handle)

对我有用。

and for me. Thanks - AKMalkadi

3

通过普通函数实现匿名 lambda 函数对我很有帮助。正如 Mike 指出的那样,Pickle 无法处理 lambda 表达式;它只能处理数据。因此,将 defaultdict 方法从以下方式转换:

    dict_ = defaultdict(lambda: default_value)

致:

    def default_():
        return default_value

然后按照以下方式创建默认字典对我有帮助:

    dict_ = defaultdict(default_)

我不明白这对sloth六年前的答案有何补充。 - ShadowRanger
1
@ShadowRanger 随着语言的发展,了解六年前仍然适用的知识确实有一定价值。也许这可以在回答中明确说明。 - Salmonstrikes

2

如果您不关心保留defaultdict类型,请将其转换:

fname = "file.pkl"

for value in nested_default_dict:
    nested_default_dict[value] = dict(nested_default_dict[value])
my_dict = dict(nested_default_dict)

with open(fname, "wb") as f:
    pickle.dump(my_dict, f)  # Now this will work

我认为这是一个很好的替代方法,因为当您进行数据序列化时,对象可能已经处于其最终形式... 而且,如果您确实需要 defaultdict 类型,您可以在反序列化后将其转换回来:

for value in my_dict:
    my_dict[value] = defaultdict(type, my_dict[value])
nested_default_dict = defaultdict(type, my_dict)

1
我目前在做类似于问题提出者的事情,但是我使用了defaultdict的一个子类,它有一个成员函数作为default_factory。为了使我的代码正常工作(我需要在运行时定义函数),我只需添加一些代码来准备对象进行pickling。
而不是:
...
pickle.dump(dict, file)
...

我使用这个:

....
factory = dict.default_factory
dict.default_factory = None
pickle.dump(dict, file)
dict.default_factory = factory
...

这不是我使用的确切代码,因为我的树是一个对象,它创建了相同类型的树的实例,当索引被请求时(所以我使用递归成员函数来执行预处理/后处理),但这种模式也回答了这个问题。


请注意,如果您不介意丢失 pickled 字典的 default_factory,那么这只是一个好方法。如果您不再需要工厂,则可以将其设置为 None 并完成操作 (: - drevicko

0
这里有一个函数,用于任意深度嵌套的任意基数defaultdict。
def wrap_defaultdict(instance, times):
    """Wrap an instance an arbitrary number of `times` to create nested defaultdict.
    
    Parameters
    ----------
    instance - e.g., list, dict, int, collections.Counter
    times - the number of nested keys above `instance`; if `times=3` dd[one][two][three] = instance
    
    Notes
    -----
    using `x.copy` allows pickling (loading to ipyparallel cluster or pkldump)
        - thanks https://dev59.com/sWQn5IYBdhLWcg3w9K_3
    """
    from collections import defaultdict

    def _dd(x):
        return defaultdict(x.copy)

    dd = defaultdict(instance)
    for i in range(times-1):
        dd = _dd(dd)

    return dd

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