Skip to content

Commit 884eba3

Browse files
bpo-26579: Add object.__getstate__(). (GH-2821)
Copying and pickling instances of subclasses of builtin types bytearray, set, frozenset, collections.OrderedDict, collections.deque, weakref.WeakSet, and datetime.tzinfo now copies and pickles instance attributes implemented as slots.
1 parent f82f9ce commit 884eba3

25 files changed

+389
-255
lines changed

Doc/library/pickle.rst

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,8 @@ The following types can be pickled:
509509

510510
* classes that are defined at the top level of a module
511511

512-
* instances of such classes whose :attr:`~object.__dict__` or the result of
513-
calling :meth:`__getstate__` is picklable (see section :ref:`pickle-inst` for
514-
details).
512+
* instances of such classes whose the result of calling :meth:`__getstate__`
513+
is picklable (see section :ref:`pickle-inst` for details).
515514

516515
Attempts to pickle unpicklable objects will raise the :exc:`PicklingError`
517516
exception; when this happens, an unspecified number of bytes may have already
@@ -611,11 +610,31 @@ methods:
611610

612611
.. method:: object.__getstate__()
613612

614-
Classes can further influence how their instances are pickled; if the class
615-
defines the method :meth:`__getstate__`, it is called and the returned object
616-
is pickled as the contents for the instance, instead of the contents of the
617-
instance's dictionary. If the :meth:`__getstate__` method is absent, the
618-
instance's :attr:`~object.__dict__` is pickled as usual.
613+
Classes can further influence how their instances are pickled by overriding
614+
the method :meth:`__getstate__`. It is called and the returned object
615+
is pickled as the contents for the instance, instead of a default state.
616+
There are several cases:
617+
618+
* For a class that has no instance :attr:`~object.__dict__` and no
619+
:attr:`~object.__slots__`, the default state is ``None``.
620+
621+
* For a class that has an instance :attr:`~object.__dict__` and no
622+
:attr:`~object.__slots__`, the default state is ``self.__dict__``.
623+
624+
* For a class that has an instance :attr:`~object.__dict__` and
625+
:attr:`~object.__slots__`, the default state is a tuple consisting of two
626+
dictionaries: ``self.__dict__``, and a dictionary mapping slot
627+
names to slot values. Only slots that have a value are
628+
included in the latter.
629+
630+
* For a class that has :attr:`~object.__slots__` and no instance
631+
:attr:`~object.__dict__`, the default state is a tuple whose first item
632+
is ``None`` and whose second item is a dictionary mapping slot names
633+
to slot values described in the previous bullet.
634+
635+
.. versionchanged:: 3.11
636+
Added the default implementation of the ``__getstate__()`` method in the
637+
:class:`object` class.
619638

620639

621640
.. method:: object.__setstate__(state)

Doc/whatsnew/3.11.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,15 @@ Other Language Changes
187187
protocols correspondingly.
188188
(Contributed by Serhiy Storchaka in :issue:`12022`.)
189189

190+
* Added :meth:`object.__getstate__` which provides the default
191+
implementation of the ``__getstate__()`` method. :mod:`Copying <copy>`
192+
and :mod:`pickling <pickle>` instances of subclasses of builtin types
193+
:class:`bytearray`, :class:`set`, :class:`frozenset`,
194+
:class:`collections.OrderedDict`, :class:`collections.deque`,
195+
:class:`weakref.WeakSet`, and :class:`datetime.tzinfo` now copies and
196+
pickles instance attributes implemented as :term:`slots <__slots__>`.
197+
(Contributed by Serhiy Storchaka in :issue:`26579`.)
198+
190199

191200
Other CPython Implementation Changes
192201
====================================

Include/object.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,11 @@ PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *);
299299
*/
300300
PyAPI_FUNC(PyObject *) PyObject_Dir(PyObject *);
301301

302+
/* Pickle support. */
303+
#ifndef Py_LIMITED_API
304+
PyAPI_FUNC(PyObject *) _PyObject_GetState(PyObject *);
305+
#endif
306+
302307

303308
/* Helpers for printing recursive container types */
304309
PyAPI_FUNC(int) Py_ReprEnter(PyObject *);

Lib/_weakrefset.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ def __contains__(self, item):
8080
return wr in self.data
8181

8282
def __reduce__(self):
83-
return (self.__class__, (list(self),),
84-
getattr(self, '__dict__', None))
83+
return self.__class__, (list(self),), self.__getstate__()
8584

8685
def add(self, item):
8786
if self._pending_removals:

Lib/collections/__init__.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,22 @@ def __repr__(self):
271271

272272
def __reduce__(self):
273273
'Return state information for pickling'
274-
inst_dict = vars(self).copy()
275-
for k in vars(OrderedDict()):
276-
inst_dict.pop(k, None)
277-
return self.__class__, (), inst_dict or None, None, iter(self.items())
274+
state = self.__getstate__()
275+
if state:
276+
if isinstance(state, tuple):
277+
state, slots = state
278+
else:
279+
slots = {}
280+
state = state.copy()
281+
slots = slots.copy()
282+
for k in vars(OrderedDict()):
283+
state.pop(k, None)
284+
slots.pop(k, None)
285+
if slots:
286+
state = state, slots
287+
else:
288+
state = state or None
289+
return self.__class__, (), state, None, iter(self.items())
278290

279291
def copy(self):
280292
'od.copy() -> a shallow copy of od'

Lib/copyreg.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ def _reduce_ex(self, proto):
8989
except AttributeError:
9090
dict = None
9191
else:
92+
if (type(self).__getstate__ is object.__getstate__ and
93+
getattr(self, "__slots__", None)):
94+
raise TypeError("a class that defines __slots__ without "
95+
"defining __getstate__ cannot be pickled")
9296
dict = getstate()
9397
if dict:
9498
return _reconstructor, args, dict

Lib/datetime.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,15 +1169,7 @@ def __reduce__(self):
11691169
args = getinitargs()
11701170
else:
11711171
args = ()
1172-
getstate = getattr(self, "__getstate__", None)
1173-
if getstate:
1174-
state = getstate()
1175-
else:
1176-
state = getattr(self, "__dict__", None) or None
1177-
if state is None:
1178-
return (self.__class__, args)
1179-
else:
1180-
return (self.__class__, args, state)
1172+
return (self.__class__, args, self.__getstate__())
11811173

11821174

11831175
class IsoCalendarDate(tuple):

Lib/email/headerregistry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def __reduce__(self):
218218
self.__class__.__bases__,
219219
str(self),
220220
),
221-
self.__dict__)
221+
self.__getstate__())
222222

223223
@classmethod
224224
def _reconstruct(cls, value):

Lib/test/datetimetester.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ class PicklableFixedOffset(FixedOffset):
139139
def __init__(self, offset=None, name=None, dstoffset=None):
140140
FixedOffset.__init__(self, offset, name, dstoffset)
141141

142-
def __getstate__(self):
143-
return self.__dict__
142+
class PicklableFixedOffsetWithSlots(PicklableFixedOffset):
143+
__slots__ = '_FixedOffset__offset', '_FixedOffset__name', 'spam'
144144

145145
class _TZInfo(tzinfo):
146146
def utcoffset(self, datetime_module):
@@ -202,6 +202,7 @@ def test_pickling_subclass(self):
202202
offset = timedelta(minutes=-300)
203203
for otype, args in [
204204
(PicklableFixedOffset, (offset, 'cookie')),
205+
(PicklableFixedOffsetWithSlots, (offset, 'cookie')),
205206
(timezone, (offset,)),
206207
(timezone, (offset, "EST"))]:
207208
orig = otype(*args)
@@ -217,6 +218,7 @@ def test_pickling_subclass(self):
217218
self.assertIs(type(derived), otype)
218219
self.assertEqual(derived.utcoffset(None), offset)
219220
self.assertEqual(derived.tzname(None), oname)
221+
self.assertFalse(hasattr(derived, 'spam'))
220222

221223
def test_issue23600(self):
222224
DSTDIFF = DSTOFFSET = timedelta(hours=1)

Lib/test/pickletester.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2382,9 +2382,11 @@ def test_reduce_calls_base(self):
23822382
def test_bad_getattr(self):
23832383
# Issue #3514: crash when there is an infinite loop in __getattr__
23842384
x = BadGetattr()
2385-
for proto in protocols:
2385+
for proto in range(2):
23862386
with support.infinite_recursion():
23872387
self.assertRaises(RuntimeError, self.dumps, x, proto)
2388+
for proto in range(2, pickle.HIGHEST_PROTOCOL + 1):
2389+
s = self.dumps(x, proto)
23882390

23892391
def test_reduce_bad_iterator(self):
23902392
# Issue4176: crash when 4th and 5th items of __reduce__()

Lib/test/test_bytes.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,28 +1940,30 @@ def test_join(self):
19401940
def test_pickle(self):
19411941
a = self.type2test(b"abcd")
19421942
a.x = 10
1943-
a.y = self.type2test(b"efgh")
1943+
a.z = self.type2test(b"efgh")
19441944
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
19451945
b = pickle.loads(pickle.dumps(a, proto))
19461946
self.assertNotEqual(id(a), id(b))
19471947
self.assertEqual(a, b)
19481948
self.assertEqual(a.x, b.x)
1949-
self.assertEqual(a.y, b.y)
1949+
self.assertEqual(a.z, b.z)
19501950
self.assertEqual(type(a), type(b))
1951-
self.assertEqual(type(a.y), type(b.y))
1951+
self.assertEqual(type(a.z), type(b.z))
1952+
self.assertFalse(hasattr(b, 'y'))
19521953

19531954
def test_copy(self):
19541955
a = self.type2test(b"abcd")
19551956
a.x = 10
1956-
a.y = self.type2test(b"efgh")
1957+
a.z = self.type2test(b"efgh")
19571958
for copy_method in (copy.copy, copy.deepcopy):
19581959
b = copy_method(a)
19591960
self.assertNotEqual(id(a), id(b))
19601961
self.assertEqual(a, b)
19611962
self.assertEqual(a.x, b.x)
1962-
self.assertEqual(a.y, b.y)
1963+
self.assertEqual(a.z, b.z)
19631964
self.assertEqual(type(a), type(b))
1964-
self.assertEqual(type(a.y), type(b.y))
1965+
self.assertEqual(type(a.z), type(b.z))
1966+
self.assertFalse(hasattr(b, 'y'))
19651967

19661968
def test_fromhex(self):
19671969
b = self.type2test.fromhex('1a2B30')
@@ -1994,6 +1996,9 @@ def __init__(me, *args, **kwargs):
19941996
class ByteArraySubclass(bytearray):
19951997
pass
19961998

1999+
class ByteArraySubclassWithSlots(bytearray):
2000+
__slots__ = ('x', 'y', '__dict__')
2001+
19972002
class BytesSubclass(bytes):
19982003
pass
19992004

@@ -2014,6 +2019,9 @@ def __init__(me, newarg=1, *args, **kwargs):
20142019
x = subclass(newarg=4, source=b"abcd")
20152020
self.assertEqual(x, b"abcd")
20162021

2022+
class ByteArraySubclassWithSlotsTest(SubclassTest, unittest.TestCase):
2023+
basetype = bytearray
2024+
type2test = ByteArraySubclassWithSlots
20172025

20182026
class BytesSubclassTest(SubclassTest, unittest.TestCase):
20192027
basetype = bytes

Lib/test/test_deque.py

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,9 @@ def test_runtime_error_on_empty_deque(self):
781781
class Deque(deque):
782782
pass
783783

784+
class DequeWithSlots(deque):
785+
__slots__ = ('x', 'y', '__dict__')
786+
784787
class DequeWithBadIter(deque):
785788
def __iter__(self):
786789
raise TypeError
@@ -810,40 +813,28 @@ def test_basics(self):
810813
self.assertEqual(len(d), 0)
811814

812815
def test_copy_pickle(self):
813-
814-
d = Deque('abc')
815-
816-
e = d.__copy__()
817-
self.assertEqual(type(d), type(e))
818-
self.assertEqual(list(d), list(e))
819-
820-
e = Deque(d)
821-
self.assertEqual(type(d), type(e))
822-
self.assertEqual(list(d), list(e))
823-
824-
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
825-
s = pickle.dumps(d, proto)
826-
e = pickle.loads(s)
827-
self.assertNotEqual(id(d), id(e))
828-
self.assertEqual(type(d), type(e))
829-
self.assertEqual(list(d), list(e))
830-
831-
d = Deque('abcde', maxlen=4)
832-
833-
e = d.__copy__()
834-
self.assertEqual(type(d), type(e))
835-
self.assertEqual(list(d), list(e))
836-
837-
e = Deque(d)
838-
self.assertEqual(type(d), type(e))
839-
self.assertEqual(list(d), list(e))
840-
841-
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
842-
s = pickle.dumps(d, proto)
843-
e = pickle.loads(s)
844-
self.assertNotEqual(id(d), id(e))
845-
self.assertEqual(type(d), type(e))
846-
self.assertEqual(list(d), list(e))
816+
for cls in Deque, DequeWithSlots:
817+
for d in cls('abc'), cls('abcde', maxlen=4):
818+
d.x = ['x']
819+
d.z = ['z']
820+
821+
e = d.__copy__()
822+
self.assertEqual(type(d), type(e))
823+
self.assertEqual(list(d), list(e))
824+
825+
e = cls(d)
826+
self.assertEqual(type(d), type(e))
827+
self.assertEqual(list(d), list(e))
828+
829+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
830+
s = pickle.dumps(d, proto)
831+
e = pickle.loads(s)
832+
self.assertNotEqual(id(d), id(e))
833+
self.assertEqual(type(d), type(e))
834+
self.assertEqual(list(d), list(e))
835+
self.assertEqual(e.x, d.x)
836+
self.assertEqual(e.z, d.z)
837+
self.assertFalse(hasattr(e, 'y'))
847838

848839
def test_pickle_recursive(self):
849840
for proto in range(pickle.HIGHEST_PROTOCOL + 1):

Lib/test/test_descrtut.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ def merge(self, other):
181181
'__ge__',
182182
'__getattribute__',
183183
'__getitem__',
184+
'__getstate__',
184185
'__gt__',
185186
'__hash__',
186187
'__iadd__',

Lib/test/test_ordered_dict.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ def test_copying(self):
287287
# and have a repr/eval round-trip
288288
pairs = [('c', 1), ('b', 2), ('a', 3), ('d', 4), ('e', 5), ('f', 6)]
289289
od = OrderedDict(pairs)
290+
od.x = ['x']
291+
od.z = ['z']
290292
def check(dup):
291293
msg = "\ncopy: %s\nod: %s" % (dup, od)
292294
self.assertIsNot(dup, od, msg)
@@ -295,13 +297,27 @@ def check(dup):
295297
self.assertEqual(len(dup), len(od))
296298
self.assertEqual(type(dup), type(od))
297299
check(od.copy())
298-
check(copy.copy(od))
299-
check(copy.deepcopy(od))
300+
dup = copy.copy(od)
301+
check(dup)
302+
self.assertIs(dup.x, od.x)
303+
self.assertIs(dup.z, od.z)
304+
self.assertFalse(hasattr(dup, 'y'))
305+
dup = copy.deepcopy(od)
306+
check(dup)
307+
self.assertEqual(dup.x, od.x)
308+
self.assertIsNot(dup.x, od.x)
309+
self.assertEqual(dup.z, od.z)
310+
self.assertIsNot(dup.z, od.z)
311+
self.assertFalse(hasattr(dup, 'y'))
300312
# pickle directly pulls the module, so we have to fake it
301313
with replaced_module('collections', self.module):
302314
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
303315
with self.subTest(proto=proto):
304-
check(pickle.loads(pickle.dumps(od, proto)))
316+
dup = pickle.loads(pickle.dumps(od, proto))
317+
check(dup)
318+
self.assertEqual(dup.x, od.x)
319+
self.assertEqual(dup.z, od.z)
320+
self.assertFalse(hasattr(dup, 'y'))
305321
check(eval(repr(od)))
306322
update_test = OrderedDict()
307323
update_test.update(od)
@@ -846,6 +862,23 @@ class OrderedDict(c_coll.OrderedDict):
846862
pass
847863

848864

865+
class PurePythonOrderedDictWithSlotsCopyingTests(unittest.TestCase):
866+
867+
module = py_coll
868+
class OrderedDict(py_coll.OrderedDict):
869+
__slots__ = ('x', 'y')
870+
test_copying = OrderedDictTests.test_copying
871+
872+
873+
@unittest.skipUnless(c_coll, 'requires the C version of the collections module')
874+
class CPythonOrderedDictWithSlotsCopyingTests(unittest.TestCase):
875+
876+
module = c_coll
877+
class OrderedDict(c_coll.OrderedDict):
878+
__slots__ = ('x', 'y')
879+
test_copying = OrderedDictTests.test_copying
880+
881+
849882
class PurePythonGeneralMappingTests(mapping_tests.BasicTestMappingProtocol):
850883

851884
@classmethod

0 commit comments

Comments
 (0)