Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,17 +1184,21 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
return cls


# _dataclass_getstate and _dataclass_setstate are needed for pickling frozen
# classes with slots. These could be slightly more performant if we generated
# _dataclass_reduce and _dataclass_produce are needed because there is a
# bug in pickle when there is a cycle in the object graph and the object
# serves as a key for a dict, set or frozenset in one of its descendants.
# cf. gh python/cpython#124937.
#
# These could be slightly more performant if we generated
# the code instead of iterating over fields. But that can be a project for
# another day, if performance becomes an issue.
def _dataclass_getstate(self):
return [getattr(self, f.name) for f in fields(self)]
def _dataclass_reduce(self):
return _dataclass_produce, ([getattr(self, f.name) for f in fields(self)],)


def _dataclass_setstate(self, state):
for field, value in zip(fields(self), state):
# use setattr because dataclass may be frozen
def _dataclass_produce(self, data):
for field, value in zip(fields(self), data):
# use `object.__setattr__` because dataclass may be frozen.
object.__setattr__(self, field.name, value)


Expand Down Expand Up @@ -1305,12 +1309,8 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
if qualname is not None:
newcls.__qualname__ = qualname

if is_frozen:
# Need this for pickling frozen classes with slots.
if '__getstate__' not in cls_dict:
newcls.__getstate__ = _dataclass_getstate
if '__setstate__' not in cls_dict:
newcls.__setstate__ = _dataclass_setstate
if '__reduce__' not in cls_dict:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this keep the is_frozen condition? If not, can you explain why not? (nothing uses that arg any more if not, so remove the arg and update the caller if so)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, i don't think so, because we need the new reduce function no matter if the dataclass is frozen or not.

however, looking at the tests, it looks like maybe __getstate__ and __setstate__ are required if the dataclass has slots?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pondering... I wonder if always having a __reduce__ has meaningful performance implications?

cls.__reduce__ = _dataclass_reduce

# Fix up any closures which reference __class__. This is used to
# fix zero argument super so that it points to the correct class
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,28 @@ class R:
self.assertEqual(new_sample.x, another_new_sample.x)
self.assertEqual(sample.y, another_new_sample.y)

def test_dataclasses_hash_pickleable(self):
global P, Q
class Q:
def __init__(self):
self.p = set()

@dataclass(frozen=True)
class P:
q: Q

q = Q()
q.p = P(q)
q.p.add(q)
new_q = pickle.loads(pickle.dumps(q))

self.assertEqual(q, new_q)
self.assertIsNot(q, new_q)

self.assertEqual(q.p, new_q.p)
self.assertIsNot(q.p, new_q.p)


def test_dataclasses_qualnames(self):
@dataclass(order=True, unsafe_hash=True, frozen=True)
class A:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix unpickling for :mod:`dataclasses` with hash-based data structures in their
descendants in the presence of cycles.
Loading