Skip to content

Commit

Permalink
gh-102578: Optimise setting and deleting mutable attributes on non-da…
Browse files Browse the repository at this point in the history
…taclass subclasses of frozen dataclasses (gh-102573)
  • Loading branch information
XuehaiPan committed Mar 11, 2023
1 parent 90f1d77 commit ee6f841
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 6 deletions.
10 changes: 4 additions & 6 deletions Lib/dataclasses.py
Expand Up @@ -616,21 +616,19 @@ def _repr_fn(fields, globals):
def _frozen_get_del_attr(cls, fields, globals):
locals = {'cls': cls,
'FrozenInstanceError': FrozenInstanceError}
condition = 'type(self) is cls'
if fields:
fields_str = '(' + ','.join(repr(f.name) for f in fields) + ',)'
else:
# Special case for the zero-length tuple.
fields_str = '()'
condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}'
return (_create_fn('__setattr__',
('self', 'name', 'value'),
(f'if type(self) is cls or name in {fields_str}:',
(f'if {condition}:',
' raise FrozenInstanceError(f"cannot assign to field {name!r}")',
f'super(cls, self).__setattr__(name, value)'),
locals=locals,
globals=globals),
_create_fn('__delattr__',
('self', 'name'),
(f'if type(self) is cls or name in {fields_str}:',
(f'if {condition}:',
' raise FrozenInstanceError(f"cannot delete field {name!r}")',
f'super(cls, self).__delattr__(name)'),
locals=locals,
Expand Down
44 changes: 44 additions & 0 deletions Lib/test/test_dataclasses.py
Expand Up @@ -2767,6 +2767,19 @@ class C:
c.i = 5
self.assertEqual(c.i, 10)

def test_frozen_empty(self):
@dataclass(frozen=True)
class C:
pass

c = C()
self.assertFalse(hasattr(c, 'i'))
with self.assertRaises(FrozenInstanceError):
c.i = 5
self.assertFalse(hasattr(c, 'i'))
with self.assertRaises(FrozenInstanceError):
del c.i

def test_inherit(self):
@dataclass(frozen=True)
class C:
Expand Down Expand Up @@ -2890,6 +2903,37 @@ class S(D):
self.assertEqual(s.y, 10)
self.assertEqual(s.cached, True)

with self.assertRaises(FrozenInstanceError):
del s.x
self.assertEqual(s.x, 3)
with self.assertRaises(FrozenInstanceError):
del s.y
self.assertEqual(s.y, 10)
del s.cached
self.assertFalse(hasattr(s, 'cached'))
with self.assertRaises(AttributeError) as cm:
del s.cached
self.assertNotIsInstance(cm.exception, FrozenInstanceError)

def test_non_frozen_normal_derived_from_empty_frozen(self):
@dataclass(frozen=True)
class D:
pass

class S(D):
pass

s = S()
self.assertFalse(hasattr(s, 'x'))
s.x = 5
self.assertEqual(s.x, 5)

del s.x
self.assertFalse(hasattr(s, 'x'))
with self.assertRaises(AttributeError) as cm:
del s.x
self.assertNotIsInstance(cm.exception, FrozenInstanceError)

def test_overwriting_frozen(self):
# frozen uses __setattr__ and __delattr__.
with self.assertRaisesRegex(TypeError,
Expand Down
@@ -0,0 +1,4 @@
Speed up setting or deleting mutable attributes on non-dataclass subclasses of
frozen dataclasses. Due to the implementation of ``__setattr__`` and
``__delattr__`` for frozen dataclasses, this previously had a time complexity
of ``O(n)``. It now has a time complexity of ``O(1)``.

0 comments on commit ee6f841

Please sign in to comment.