Pickling set 子类引发不可散列类型:'list'

问题描述 投票:0回答:2

此代码(使用自定义

list
子类)对我来说效果很好:

import pickle

class Numbers(list):
    def __init__(self, *numbers: int) -> None:
        super().__init__()
        self.extend(numbers)

numbers = Numbers(12, 34, 56)
numbers.append(78)

numbers = pickle.loads(pickle.dumps(numbers, protocol=3))

但是当我将父类更改为

set
时:

import pickle

class Numbers(set):
    def __init__(self, *numbers: int) -> None:
        super().__init__()
        self.update(numbers)

numbers = Numbers(12, 34, 56)
numbers.add(78)

numbers = pickle.loads(pickle.dumps(numbers, protocol=3))

代码会通过以下回溯引发

TypeError

Traceback (most recent call last):
  File "test.py", line 11, in <module>
    numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
  File "test.py", line 6, in __init__
    self.update(numbers)
TypeError: unhashable type: 'list'

set
子类已成功初始化并且运行良好,但尝试腌制它会引发一个非常令人困惑的异常,因为实际上我的代码中没有使用
list

python set pickle subclass
2个回答
0
投票

总结

修改内置类型的构造函数非常困难且容易出错,因为其他方法可能依赖于它。尽可能避免。

错误检查

首先,通过强制使用 Python 实现 覆盖 pickle 模块的

 已编译 C 实现
,我们可以在回溯中获得更多信息:

Traceback (most recent call last):
  File "test.py", line 14, in <module>
    numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
  File "/usr/local/lib/python3.7/pickle.py", line 1604, in _loads
    encoding=encoding, errors=errors).load()
  File "/usr/local/lib/python3.7/pickle.py", line 1086, in load
    dispatch[key[0]](self)
  File "/usr/local/lib/python3.7/pickle.py", line 1437, in load_reduce
    stack[-1] = func(*args)
  File "test.py", line 6, in __init__
    self.update(numbers)
TypeError: unhashable type: 'list'

尽管文档指出:

当一个类实例被 unpickle 时,它的

__init__
方法通常不会被调用。

我们可以从上面的回溯中看到,如果类的 pickled 表示包含

__init__
操作码(通常当类实现自定义 REDUCE
 方法
时),则调用 __reduce__
 方法
,并且,作为
检查腌制表示显示,REDUCE
操作码确实存在:

0: \x80 PROTO 3 2: c GLOBAL '__main__ Numbers' 20: q BINPUT 0 22: ] EMPTY_LIST 23: q BINPUT 1 25: ( MARK 26: K BININT1 56 28: K BININT1 34 30: K BININT1 12 32: K BININT1 78 34: e APPENDS (MARK at 25) 35: \x85 TUPLE1 36: q BINPUT 2 38: R REDUCE 39: q BINPUT 3 41: } EMPTY_DICT 42: q BINPUT 4 44: b BUILD 45: . STOP
解决方案

避免修改构造函数:

import pickle class Numbers(set): pass numbers = Numbers([12, 34, 56]) numbers.add(78) numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
或者,如果你真的必须这样做,至少确保将所有参数传递给父构造函数:

import pickle class Numbers(set): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) numbers = Numbers([12, 34, 56]) numbers.add(78) numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
    

0
投票

pickle.dumps()

实际上是调用对象的
__reduce_ex_()
方法来序列化。

__reduce_ex__()

__reduce__()
 方法的更新版本,如果未定义,它将调用 
__reduce__()
 来代替。

对于

set

 对象, 
__reduce_ex__(<protocol>)
 相当于 
__reduce__()
 ,它返回一个包含三个项目的元组:

(<the class of instance>, (a tuple storing arguments), {a dict storing attributes})
您的情况:

class Numbers(set): def __init__(self, *numbers: int) -> None: super().__init__() self.update(numbers)

Numbers

 继承自 
set
,并且您不重写 
__reduce__
__reduce_ex__
 方法,因此它遵循 
set
 类的模式。

尝试一下你可能会看到:

numbers = Numbers(12, 34, 56) pickled = numbers.__reduce__() # print(pickled) # (<class '__main__.Numbers'>, ([56, 34, 12],), None)

pickle.loads()

 则相反,它使用元组来重建实例。它首先使用元组中的第一项和第二项创建一个实例,然后使用第三个元素更新属性

同样,

instance = pickled[0](*pickled[1]) instance.__dict__.update(pickled[2])
如果您运行此类代码,它会准确地重现您发布的错误。

再看一下:

  1. pickled[0]

     是对类 
    Numbers
    ,
    的引用

  2. pickled[1]

    是元组
    ([56, 34, 12],)
    ,它首先被
    *
    运算符解包为列表
    [56, 34, 12]
    ,然后这个列表作为类
    Numbers
    的参数传递

  3. 放在一起,

    pickle.loads()

    的第一步其实就是做
    Numbers([56, 34, 12])
    
    

您定义了

def __init__(self, *numbers: int) -> None:

,因此列表 
[56, 34, 12]
 是传递给 
__init__
 方法的唯一单个参数,然后将其传递给 
self.update([56, 34, 12])
self
 确实是对 
set
 类对象的引用,一个
set
 不能接受 
list
 对象作为元素,因为它是可变的(即不可哈希)。

这就是你的示例中失败的原因。

为了使您的示例正常工作,可以直接覆盖

__reduce__()

 方法。

class Numbers(set): def __init__(self, *numbers: int) -> None: super().__init__() self.update(numbers) def __reduce__(self): return (self.__class__, tuple(num for num in self), self.__dict__)
然后再试一次:

import pickle numbers = Numbers(12, 34, 56) print(numbers) # Numbers({56, 34, 12}) numbers = pickle.loads(pickle.dumps(numbers, protocol=3)) print(numbers) # Numbers({56, 34, 12})
工作得很好,没有出现异常!

© www.soinside.com 2019 - 2024. All rights reserved.