Skip to content

Commit ee6f841

Browse files
authored
gh-102578: Optimise setting and deleting mutable attributes on non-dataclass subclasses of frozen dataclasses (gh-102573)
1 parent 90f1d77 commit ee6f841

File tree

3 files changed

+52
-6
lines changed

3 files changed

+52
-6
lines changed

Lib/dataclasses.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -616,21 +616,19 @@ def _repr_fn(fields, globals):
616616
def _frozen_get_del_attr(cls, fields, globals):
617617
locals = {'cls': cls,
618618
'FrozenInstanceError': FrozenInstanceError}
619+
condition = 'type(self) is cls'
619620
if fields:
620-
fields_str = '(' + ','.join(repr(f.name) for f in fields) + ',)'
621-
else:
622-
# Special case for the zero-length tuple.
623-
fields_str = '()'
621+
condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}'
624622
return (_create_fn('__setattr__',
625623
('self', 'name', 'value'),
626-
(f'if type(self) is cls or name in {fields_str}:',
624+
(f'if {condition}:',
627625
' raise FrozenInstanceError(f"cannot assign to field {name!r}")',
628626
f'super(cls, self).__setattr__(name, value)'),
629627
locals=locals,
630628
globals=globals),
631629
_create_fn('__delattr__',
632630
('self', 'name'),
633-
(f'if type(self) is cls or name in {fields_str}:',
631+
(f'if {condition}:',
634632
' raise FrozenInstanceError(f"cannot delete field {name!r}")',
635633
f'super(cls, self).__delattr__(name)'),
636634
locals=locals,

Lib/test/test_dataclasses.py

+44
Original file line numberDiff line numberDiff line change
@@ -2767,6 +2767,19 @@ class C:
27672767
c.i = 5
27682768
self.assertEqual(c.i, 10)
27692769

2770+
def test_frozen_empty(self):
2771+
@dataclass(frozen=True)
2772+
class C:
2773+
pass
2774+
2775+
c = C()
2776+
self.assertFalse(hasattr(c, 'i'))
2777+
with self.assertRaises(FrozenInstanceError):
2778+
c.i = 5
2779+
self.assertFalse(hasattr(c, 'i'))
2780+
with self.assertRaises(FrozenInstanceError):
2781+
del c.i
2782+
27702783
def test_inherit(self):
27712784
@dataclass(frozen=True)
27722785
class C:
@@ -2890,6 +2903,37 @@ class S(D):
28902903
self.assertEqual(s.y, 10)
28912904
self.assertEqual(s.cached, True)
28922905

2906+
with self.assertRaises(FrozenInstanceError):
2907+
del s.x
2908+
self.assertEqual(s.x, 3)
2909+
with self.assertRaises(FrozenInstanceError):
2910+
del s.y
2911+
self.assertEqual(s.y, 10)
2912+
del s.cached
2913+
self.assertFalse(hasattr(s, 'cached'))
2914+
with self.assertRaises(AttributeError) as cm:
2915+
del s.cached
2916+
self.assertNotIsInstance(cm.exception, FrozenInstanceError)
2917+
2918+
def test_non_frozen_normal_derived_from_empty_frozen(self):
2919+
@dataclass(frozen=True)
2920+
class D:
2921+
pass
2922+
2923+
class S(D):
2924+
pass
2925+
2926+
s = S()
2927+
self.assertFalse(hasattr(s, 'x'))
2928+
s.x = 5
2929+
self.assertEqual(s.x, 5)
2930+
2931+
del s.x
2932+
self.assertFalse(hasattr(s, 'x'))
2933+
with self.assertRaises(AttributeError) as cm:
2934+
del s.x
2935+
self.assertNotIsInstance(cm.exception, FrozenInstanceError)
2936+
28932937
def test_overwriting_frozen(self):
28942938
# frozen uses __setattr__ and __delattr__.
28952939
with self.assertRaisesRegex(TypeError,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Speed up setting or deleting mutable attributes on non-dataclass subclasses of
2+
frozen dataclasses. Due to the implementation of ``__setattr__`` and
3+
``__delattr__`` for frozen dataclasses, this previously had a time complexity
4+
of ``O(n)``. It now has a time complexity of ``O(1)``.

0 commit comments

Comments
 (0)