3

In Python, I often find myself implementing the same pattern: count the number of "valid" iterations while processing within a loop, where an "invalid" iteration is skipped over with a continue statement. I use the continue statement instead of if-else blocks to improve readability. Essentially, I do the following:

count = 0
for item in collection:
    do_something_1(item)
    if not evaluate_some_condition(item):
        continue
    count += 1
    do_something_2(item)
return count

There are several nifty tricks one can use to implement similar patterns in a Pythonic manner. For example, enumerate, continue, break, for-else, and while-else come to mind. I am looking for a Pythonic construct to implement the scenario described above.

This works (below) but would require the evaluate_some_condition function be executed twice for every element, which can sometimes be unacceptable (it is also less readable in my opinion):

count = sum(1 for item in collection if not evaluate_some_condition(item))
for item in collection:
    do_something_1(item)
    if not evaluate_some_condition(item):
        continue
    do_something_2(item)
return count

Some construct like the below would be ideal:

for count, item in uninterrupted_enumerate(collection):
    do_something_1(item)
    if not evaluate_some_condition(item):
        continue
    do_something_2(item)
return count

Any ideas of a built-in Python feature, third-party feature, or future plans to include such a feature?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197

3 Answers3

1

No, I think the very first version:

count = 0
for item in collection:
    do_something_1(item)
    if not evaluate_some_condition(item):
        continue
    count += 1
    do_something_2(item)
return count

is pretty much how it is done and it very obvious (i.e. readable) what is happening. If you compare it with your last version, where it would be very unclear that any counting is happening. If you really want to solve it that way, you could always increase some global variable within the evaluate_some_condition function, depending whether the internal check was successful or not. But again, this is probably less readable than your first version.

Bastian Venthur
  • 12,515
  • 5
  • 44
  • 78
0

I think the most interesting alternative is to use decorators. Because this was described in a different post, I will simply provide the link: https://stackoverflow.com/a/44969343/8033585

Another alternative is to use classes. For example:

class Doer2:
    def __init__(self):
        self.count = 0
    def __call__(self, item):
        self.count += 1
        # put here the code from 'do_something_2()'
        ....

Then:

doer2 = Doer2()
for item in collection:
    do_something_1(item)
    if not evaluate_some_condition(item):
        continue
    doer2(item)

return doer2.count
AGN Gazer
  • 8,025
  • 2
  • 27
  • 45
-1

Here is a solution using decorators:

from typing import Callable


def do_something_1(item) -> None:
    ...


def do_something_2(item) -> None:
    ...


def evaluate_some_condition(item: int) -> bool:
    return item % 2 == 0


def count_complete_iterations(
        process_item: Callable[..., bool]
) -> Callable[..., int]:
    count = 0
    def wrapper(*args, **kwargs) -> int:
        nonlocal count
        if process_item(*args, **kwargs):
            count += 1
        return count
    return wrapper


@count_complete_iterations
def process_int(item: int) -> bool:
    do_something_1(item)
    if not evaluate_some_condition(item):
        return False
    do_something_2(item)
    return True


collection = range(10)
for item in collection:
    count = process_int(item)
print(count)  # Prints 5

Here, do_something_1 and do_something_2 do nothing; evaluate_some_condition returns True if the item is even and False if it is odd; count_complete_iterations is a decorator function that returns the most up to date count of the number of times the process_item returns True; process_int is the inner part of the for loop for-loop, where continue statements have been replaced with return False to indicate an invalid item.

Pros are that it is slightly more concise. Cons are that it is significantly less transparent and the logic within the loops needs to be placed in its own function. I'd say this is probably a bad solution, but it is interesting to consider none the less.