Skip to content

Commit 948d3de

Browse files
committed
Add cache for repr, str and invert on Flag and IntFlag
Benchmark: Testing with 10000 repeats, result is average of 100 tests: >>> str(~NewFlag.baz) # 5.6313761773697415 ms 'NewFlag.0' >>> str(~OldFlag.baz) # 146.97604789150913 ms 'OldFlag.0' >>> repr(~NewFlag.baz) # 4.422742834372443 ms '<NewFlag.0: 0>' >>> repr(~OldFlag.baz) # 151.49518529891247 ms '<OldFlag.0: 0>' >>> ~(~NewFlag.foo) # 3.465736084655711 ms <NewFlag.foo: 1> >>> ~(~OldFlag.foo) # 177.88357201820338 ms <OldFlag.foo: 1> NewFlag: total: 13.5198551 ms, average: 4.4194400 ms (Fastest) OldFlag: total: 476.3548052 ms, average: 158.2196475 ms, ~ x35.80 times slower than NewFlag
1 parent ecd41fe commit 948d3de

File tree

2 files changed

+94
-5
lines changed

2 files changed

+94
-5
lines changed

Lib/enum.py

+42-5
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ def __new__(metacls, cls, bases, classdict):
191191
# Reverse value->name map for hashable values.
192192
enum_class._value2member_map_ = {}
193193

194+
# used to speedup __str__, __repr__ and __invert__ calls when applicable
195+
enum_class._repr_ = None
196+
enum_class._str_ = None
197+
enum_class._invert_ = None
198+
194199
# If a custom type is mixed into the Enum, and it does not know how
195200
# to pickle itself, pickle.dumps will succeed but pickle.loads will
196201
# fail. Rather than have the error show up later and possibly far
@@ -265,6 +270,15 @@ def __new__(metacls, cls, bases, classdict):
265270
except TypeError:
266271
pass
267272

273+
# after all members created, cache result of this
274+
# methods for immutable values
275+
for member in enum_class._value2member_map_.copy().values():
276+
for static_attr in ('__repr__', '__str__', '__invert__'):
277+
method = getattr(member, static_attr, None)
278+
if method is None:
279+
continue
280+
setattr(member, static_attr[1:-1], method())
281+
268282
# double check that repr and friends are not the mixin's or various
269283
# things break (such as pickle)
270284
for name in {'__repr__', '__str__', '__format__', '__reduce_ex__'}:
@@ -630,10 +644,16 @@ def _value_(self):
630644

631645
@_value_.setter
632646
def _value_(self, value):
647+
self._repr_ = self._str_ = None
648+
if '_invert_' in self.__dict__:
649+
self._invert_ = None
633650
object.__setattr__(self, 'value', value)
634651

635652
@_name_.setter
636653
def _name_(self, name):
654+
self._repr_ = self._str_ = None
655+
if '_invert_' in self.__dict__:
656+
self._invert_ = None
637657
object.__setattr__(self, 'name', name)
638658

639659
def _generate_next_value_(name, start, count, last_values):
@@ -768,18 +788,27 @@ def __repr__(self):
768788
cls = self.__class__
769789
if self.name is not None:
770790
return f'<{cls.__name__}.{self.name}: {self.value!r}>'
791+
cached = self._repr_
792+
if cached is not None:
793+
return cached
771794
members, uncovered = _decompose(cls, self.value)
772-
return f"<{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}: {self.value!r}>"
795+
members = '|'.join([str(m.name or m.value) for m in members])
796+
self._repr_ = result = f"<{cls.__name__}.{members}: {self.value!r}>"
797+
return result
773798

774799
def __str__(self):
775800
cls = self.__class__
776801
if self.name is not None:
777802
return f'{cls.__name__}.{self.name}'
803+
cached = self._str_
804+
if cached is not None:
805+
return cached
778806
members, uncovered = _decompose(cls, self.value)
779807
if len(members) == 1 and members[0].name is None:
780-
return f'{cls.__name__}.{members[0].value!r}'
808+
self._str_ = result = f'{cls.__name__}.{members[0].value!r}'
781809
else:
782-
return f"{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}"
810+
self._str_ = result = f"{cls.__name__}.{'|'.join([str(m.name or m.value) for m in members])}"
811+
return result
783812

784813
def __bool__(self):
785814
return bool(self.value)
@@ -803,13 +832,18 @@ def __xor__(self, other):
803832
return cls(self.value ^ other.value)
804833

805834
def __invert__(self):
835+
cached = self._invert_
836+
if cached is not None:
837+
return cached
806838
cls = self.__class__
807839
members, uncovered = _decompose(cls, self.value)
808840
inverted = cls(0)
809841
for m in cls:
810842
if m not in members and not (m.value & self.value):
811843
inverted = inverted | m
812-
return cls(inverted)
844+
self._invert_ = result = cls(inverted)
845+
result._invert_ = self
846+
return result
813847

814848

815849
class IntFlag(int, Flag):
@@ -880,7 +914,10 @@ def __xor__(self, other):
880914
__rxor__ = __xor__
881915

882916
def __invert__(self):
883-
result = self.__class__(~self.value)
917+
cached = self._invert_
918+
if cached is not None:
919+
return cached
920+
self._invert_ = result = self.__class__(~self.value)
884921
return result
885922

886923

Lib/test/test_enum.py

+52
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717

1818
# for pickle tests
19+
from unittest import mock
20+
1921
try:
2022
class Stooges(Enum):
2123
LARRY = 1
@@ -3112,5 +3114,55 @@ def test_get_raise(self):
31123114
getattr(self.SquareFoo.b, attr)
31133115

31143116

3117+
class TestStaticAttrs(unittest.TestCase):
3118+
3119+
def test_invert_cache(self):
3120+
class Foo(Flag):
3121+
a = 1
3122+
b = 2
3123+
c = 3
3124+
3125+
with mock.patch('enum._decompose') as decompose_mock:
3126+
self.assertIs(Foo.a._invert_, Foo.b)
3127+
self.assertIs(Foo.b._invert_, Foo.a)
3128+
self.assertIsNotNone(Foo.c._invert_)
3129+
self.assertIs(~(~Foo.a), Foo.a)
3130+
self.assertFalse(decompose_mock.called)
3131+
3132+
def test_repr_str_cache(self):
3133+
class Foo(Flag):
3134+
a = 1
3135+
b = 2
3136+
c = 3
3137+
3138+
with mock.patch('enum._decompose') as decompose_mock:
3139+
no_name = ~Foo.c
3140+
self.assertFalse(decompose_mock.called)
3141+
no_name_repr = repr(no_name)
3142+
no_name_str = str(no_name)
3143+
self.assertEqual(no_name_repr, '<Foo.0: 0>')
3144+
self.assertEqual(no_name_str, 'Foo.0')
3145+
3146+
with mock.patch('enum._decompose') as decompose_mock:
3147+
self.assertIs(repr(no_name), no_name_repr)
3148+
self.assertIs(str(no_name), no_name_str)
3149+
self.assertFalse(decompose_mock.called)
3150+
3151+
def test_cache_invalidate(self):
3152+
class Foo(Flag):
3153+
a = 1
3154+
b = 2
3155+
c = 3
3156+
3157+
self.assertIs(Foo.a._invert_, Foo.b)
3158+
Foo.a._value_ = 3
3159+
self.assertIsNone(Foo.a._invert_)
3160+
self.assertIsNone(Foo.a._str_)
3161+
self.assertIsNone(Foo.a._repr_)
3162+
self.assertIs(~Foo.a, ~Foo.c)
3163+
self.assertEqual(repr(Foo.a), '<Foo.a: 3>')
3164+
self.assertEqual(str(Foo.a), 'Foo.a')
3165+
3166+
31153167
if __name__ == '__main__':
31163168
unittest.main()

0 commit comments

Comments
 (0)