This code (using custom list
subclass) works well for me:
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))
but when I change the parent class to 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))
the code raises a TypeError
with this traceback:
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'
The set
subclass is successfully initialized and works well but trying to pickle it raises a very confusing exception since there is actually no list
used in my code.
Modifying the constructor of a built-in type is hard and error-prone since other methods migth depend on it. Avoid whenever possible.
First, by forcing the Python implementation over the compiled C implementation of the pickle
module we get more information in the traceback:
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'
Although the documentation states:
When a class instance is unpickled, its
__init__
method is usually not invoked.
we can see from the traceback above that the __init__
method is invoked if the pickled representation of the class contains the REDUCE
opcode (generally when the class implements a custom __reduce__
method) and, as the inspection of the pickled representation shows, the REDUCE
opcode is indeed present:
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
Avoid modifying the constructor:
import pickle
class Numbers(set):
pass
numbers = Numbers([12, 34, 56])
numbers.add(78)
numbers = pickle.loads(pickle.dumps(numbers, protocol=3))
or, if you really have to, at least make sure to pass all the arguments to the parent constructor:
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))