Skip to content

Commit 18013eb

Browse files
authored
New message use-yield-from (R1737) (#9419)
Suggest refactoring when yielding from a for loop can be replaced by 'yield from'. This message is only emitted when there are no other statements in the containing for loop. There are too many possible generators to use in a for loop and handling all the cases to provide a human friendly display name in emitted messages seems too cumbersome compared to the benefit it brings, specially considering this message will have a doc page with examples. It is invalid to use 'yield from' in async functions. Replacing yielding from an 'async for' loop may not be the same as using 'yield from' so the message won't be emitted for async loops as well. Closes #9229
1 parent ad8d88e commit 18013eb

File tree

20 files changed

+128
-9
lines changed

20 files changed

+128
-9
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def bad_yield_from(generator):
2+
for item in generator: # [use-yield-from]
3+
yield item
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:code:`yield from` can be thought of as removing the intermediary (your for loop) between the function caller and the
2+
requested generator. This enables the caller to directly communicate with the generator (e.g. using :code:`send()`).
3+
This communication is not possible when manually yielding each element one by one in a loop.
4+
5+
PEP 380 describes the possibility of adding optimizations specific to :code:`yield from`. It looks like they
6+
have not been implemented as of the time of writing. Even without said optimizations, the following snippet shows
7+
that :code:`yield from` is marginally faster.
8+
9+
.. code-block:: sh
10+
11+
$ python3 -m timeit "def yield_from(): yield from range(100)" "for _ in yield_from(): pass"
12+
100000 loops, best of 5: 2.44 usec per loop
13+
$ python3 -m timeit "def yield_loop():" " for item in range(100): yield item" "for _ in yield_loop(): pass"
14+
100000 loops, best of 5: 2.49 usec per loop
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def good_yield_from(generator):
2+
yield from generator
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `PEP 380 <https://peps.python.org/pep-0380/>`_

doc/user_guide/checkers/features.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,9 @@ Refactoring checker Messages
906906
:unnecessary-comprehension (R1721): *Unnecessary use of a comprehension, use %s instead.*
907907
Instead of using an identity comprehension, consider using the list, dict or
908908
set constructor. It is faster and simpler.
909+
:use-yield-from (R1737): *Use 'yield from' directly instead of yielding each element one by one*
910+
Yielding directly from the iterator is faster and arguably cleaner code than
911+
yielding each element one by one in the loop.
909912
:use-a-generator (R1729): *Use a generator instead '%s(%s)'*
910913
Comprehension inside of 'any', 'all', 'max', 'min' or 'sum' is unnecessary. A
911914
generator would be sufficient and faster.

doc/user_guide/messages/messages_overview.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ All messages in the refactor category:
544544
refactor/use-dict-literal
545545
refactor/use-list-literal
546546
refactor/use-set-for-membership
547+
refactor/use-yield-from
547548
refactor/useless-object-inheritance
548549
refactor/useless-option-value
549550
refactor/useless-return

doc/whatsnew/fragments/9229.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
New message `use-yield-from` added to the refactoring checker. This message is emitted when yielding from a loop can be replaced by `yield from`.
2+
3+
Closes #9229.

pylint/checkers/refactoring/refactoring_checker.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,12 @@ class RefactoringChecker(checkers.BaseTokenChecker):
483483
"value by index lookup. "
484484
"The value can be accessed directly instead.",
485485
),
486+
"R1737": (
487+
"Use 'yield from' directly instead of yielding each element one by one",
488+
"use-yield-from",
489+
"Yielding directly from the iterator is faster and arguably cleaner code than yielding each element "
490+
"one by one in the loop.",
491+
),
486492
}
487493
options = (
488494
(
@@ -1123,6 +1129,27 @@ def visit_call(self, node: nodes.Call) -> None:
11231129
self._check_use_list_literal(node)
11241130
self._check_use_dict_literal(node)
11251131

1132+
@utils.only_required_for_messages("use-yield-from")
1133+
def visit_yield(self, node: nodes.Yield) -> None:
1134+
if not isinstance(node.value, nodes.Name):
1135+
return
1136+
1137+
parent = node.parent.parent
1138+
if (
1139+
not isinstance(parent, nodes.For)
1140+
or isinstance(parent, nodes.AsyncFor)
1141+
or len(parent.body) != 1
1142+
):
1143+
return
1144+
1145+
if parent.target.name != node.value.name:
1146+
return
1147+
1148+
if isinstance(node.frame(), nodes.AsyncFunctionDef):
1149+
return
1150+
1151+
self.add_message("use-yield-from", node=parent, confidence=HIGH)
1152+
11261153
@staticmethod
11271154
def _has_exit_in_scope(scope: nodes.LocalsDictNodeNG) -> bool:
11281155
exit_func = scope.locals.get("exit")

tests/functional/b/bad_reversed_sequence.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
""" Checks that reversed() receive proper argument """
22
# pylint: disable=missing-docstring
3-
# pylint: disable=too-few-public-methods
3+
# pylint: disable=too-few-public-methods,use-yield-from
44
from collections import deque, OrderedDict
55
from enum import IntEnum
66

tests/functional/c/consider/consider_using_enumerate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Emit a message for iteration through range and len is encountered."""
22

3-
# pylint: disable=missing-docstring, import-error, unsubscriptable-object, too-few-public-methods, unnecessary-list-index-lookup
3+
# pylint: disable=missing-docstring, import-error, unsubscriptable-object, too-few-public-methods, unnecessary-list-index-lookup, use-yield-from
44

55
def bad():
66
iterable = [1, 2, 3]

0 commit comments

Comments
 (0)