Skip to content

Commit a785898

Browse files
gh-151907: Avoid creating temporary objects in some list comprehensions (GH-151908)
Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
1 parent 066811e commit a785898

7 files changed

Lines changed: 222 additions & 41 deletions

File tree

Lib/test/test_compile.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,7 +2063,7 @@ def test_multiline_async_generator_expression(self):
20632063

20642064
def test_multiline_list_comprehension(self):
20652065
snippet = textwrap.dedent("""\
2066-
[(x,
2066+
_ = [(x,
20672067
2*x)
20682068
for x
20692069
in [1,2,3] if (x > 0
@@ -2073,14 +2073,14 @@ def test_multiline_list_comprehension(self):
20732073
compiled_code, _ = self.check_positions_against_ast(snippet)
20742074
self.assertIsInstance(compiled_code, types.CodeType)
20752075
self.assertOpcodeSourcePositionIs(compiled_code, 'LIST_APPEND',
2076-
line=1, end_line=2, column=1, end_column=8, occurrence=1)
2076+
line=1, end_line=2, column=5, end_column=8, occurrence=1)
20772077
self.assertOpcodeSourcePositionIs(compiled_code, 'JUMP_BACKWARD',
2078-
line=1, end_line=2, column=1, end_column=8, occurrence=1)
2078+
line=1, end_line=2, column=5, end_column=8, occurrence=1)
20792079

20802080
def test_multiline_async_list_comprehension(self):
20812081
snippet = textwrap.dedent("""\
20822082
async def f():
2083-
[(x,
2083+
_ = [(x,
20842084
2*x)
20852085
async for x
20862086
in [1,2,3] if (x > 0
@@ -2093,11 +2093,11 @@ async def f():
20932093
compiled_code = g['f'].__code__
20942094
self.assertIsInstance(compiled_code, types.CodeType)
20952095
self.assertOpcodeSourcePositionIs(compiled_code, 'LIST_APPEND',
2096-
line=2, end_line=3, column=5, end_column=12, occurrence=1)
2096+
line=2, end_line=3, column=9, end_column=12, occurrence=1)
20972097
self.assertOpcodeSourcePositionIs(compiled_code, 'JUMP_BACKWARD',
2098-
line=2, end_line=3, column=5, end_column=12, occurrence=1)
2098+
line=2, end_line=3, column=9, end_column=12, occurrence=1)
20992099
self.assertOpcodeSourcePositionIs(compiled_code, 'RETURN_VALUE',
2100-
line=2, end_line=7, column=4, end_column=36, occurrence=1)
2100+
line=2, end_line=2, column=4, end_column=5, occurrence=1)
21012101

21022102
def test_multiline_set_comprehension(self):
21032103
snippet = textwrap.dedent("""\

Lib/test/test_compiler_codegen.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,39 @@ def test_frozenset_optimization(self):
216216
('RETURN_VALUE', None)
217217
]
218218
self.codegen_test(snippet, expected)
219+
220+
def test_comp_without_target_optimization(self):
221+
snippet = "[i for i in range(10)]"
222+
expected = [
223+
('RESUME', 0),
224+
('ANNOTATIONS_PLACEHOLDER', None),
225+
('LOAD_NAME', 0),
226+
('PUSH_NULL', None),
227+
('LOAD_CONST', 0),
228+
('CALL', 1),
229+
('LOAD_FAST_AND_CLEAR', 0),
230+
('SWAP', 2),
231+
('SETUP_FINALLY', 20),
232+
('COPY', 1),
233+
('GET_ITER', 0),
234+
('FOR_ITER', 16),
235+
('STORE_FAST', 0),
236+
('LOAD_FAST', 0),
237+
('POP_TOP', None),
238+
('JUMP', 11),
239+
('END_FOR', None),
240+
('POP_ITER', None),
241+
('POP_BLOCK', None),
242+
('JUMP_NO_INTERRUPT', 25),
243+
('SWAP', 2),
244+
('POP_TOP', None),
245+
('SWAP', 2),
246+
('STORE_FAST_MAYBE_NULL', 0),
247+
('RERAISE', 0),
248+
('SWAP', 2),
249+
('STORE_FAST_MAYBE_NULL', 0),
250+
('POP_TOP', None),
251+
('LOAD_CONST', 1),
252+
('RETURN_VALUE', None),
253+
]
254+
self.codegen_test(snippet, expected)

Lib/test/test_dictcomps.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ def iter_raises():
165165
self.assertEqual(f.line[f.colno - indent : f.end_colno - indent],
166166
expected)
167167

168+
def test_hash_error(self):
169+
class Unhashable:
170+
def __hash__(self):
171+
0/0
172+
173+
with self.assertRaises(ZeroDivisionError):
174+
{unhashable: 1 for unhashable in [Unhashable()]}
175+
168176

169177
if __name__ == "__main__":
170178
unittest.main()

Lib/test/test_listcomps.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,75 @@ def iter_raises():
793793
self.assertEqual(f.line[f.colno - indent : f.end_colno - indent],
794794
expected)
795795

796+
def test_optimization_with_side_effects(self):
797+
# List comprehensions that aren't used as a value are optimized
798+
# to avoid creating a list. Ensure that side effects are still
799+
# retained when this happens.
800+
with self.assertRaises(ZeroDivisionError):
801+
[0/0 for _ in [1]]
802+
803+
count = 0
804+
def increment():
805+
nonlocal count
806+
count += 1
807+
808+
[increment() for _ in range(5)]
809+
self.assertEqual(count, 5)
810+
811+
def test_async_optimization_with_side_effects(self):
812+
async def gen1(aiterator):
813+
with self.assertRaises(ZeroDivisionError):
814+
[0/0 async for _ in aiterator]
815+
816+
async def gen2(aiterator):
817+
[increment() async for _ in aiterator]
818+
819+
async def numbers():
820+
for i in range(5):
821+
yield i
822+
823+
count = 0
824+
def increment():
825+
nonlocal count
826+
count += 1
827+
828+
def exhaust(coro):
829+
try:
830+
coro.send(None)
831+
except StopIteration:
832+
pass
833+
834+
exhaust(gen1(numbers()))
835+
exhaust(gen2(numbers()))
836+
self.assertEqual(count, 5)
837+
838+
def test_optimization_with_starred_unpack(self):
839+
with self.assertRaises(TypeError):
840+
[*i for i in [1, 2, 3]]
841+
842+
async def coro():
843+
async def gen():
844+
yield 1
845+
846+
with self.assertRaises(TypeError):
847+
[*i async for i in gen()]
848+
849+
c = coro()
850+
while True:
851+
try:
852+
c.send(None)
853+
except StopIteration:
854+
break
855+
856+
count = 0
857+
def weird():
858+
nonlocal count
859+
count += 1
860+
yield 0
861+
862+
[*weird() for _ in range(5)]
863+
self.assertEqual(count, 5)
864+
796865
__test__ = {'doctests' : doctests}
797866

798867
def load_tests(loader, tests, pattern):

Lib/test/test_setcomps.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ def iter_raises():
188188
self.assertEqual(f.line[f.colno - indent : f.end_colno - indent],
189189
expected)
190190

191+
def test_hash_error(self):
192+
class Unhashable:
193+
def __hash__(self):
194+
0/0
195+
196+
with self.assertRaises(ZeroDivisionError):
197+
{unhashable for unhashable in [Unhashable()]}
198+
199+
200+
if __name__ == "__main__":
201+
unittest.main()
202+
191203
__test__ = {'doctests' : doctests}
192204

193205
def load_tests(loader, tests, pattern):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Avoid creating :class:`list` objects in comprehensions when the comprehension
2+
is not used as a value.

0 commit comments

Comments
 (0)