Using iter() with sentinel to replace while loops

2019-07-27 05:07发布

问题:

Oftentimes the case arises where one would need to loop indefinitely until a certain condition has been attained. For example, if I want keep collecting random integers until I find a number == n, following which I break. I'd do this:

import random

rlist = []
n = ...
low, high = ..., ...
while True:
    num = random.randint(low, high)
    if num == n:
        break
    rlist.append(num)

And this works, but is quite clunky. There is a much more pythonic alternative using iter:

iter(o[, sentinel])

Return an iterator object. The first argument is interpreted very differently depending on the presence of the second argument. [...] If the second argument, sentinel, is given, then o must be a callable object. The iterator created in this case will call o with no arguments for each call to its next() method; if the value returned is equal to sentinel, StopIteration will be raised, otherwise the value will be returned.

The loop above can be replaced with

import random
from functools import partial

f = partial(random.randint, low, high)
rlist = list(iter(f, 10))

To extend this principle to lists that have already been created, a slight change is needed. I'll need to define a partial function like this:

f = partial(next, iter(x)) # where x is some list I want to keep taking items from until I hit a sentinel

The rest remains the same, but the main caveat with this approach versus the while loop is I cannot apply generic boolean conditions.

For example, I cannot apply a "generate numbers until the first even number greater than 1000 is encountered".


The bottom line is this: Is there another alternative to the while loop and iter that supports a callback sentinel?

回答1:

If you want generic boolean conditions, then iter(object, sentinel) is insufficiently expressive for your needs. itertools.takewhile(), in contrast, seems to be more or less what you want: It takes an iterator, and cuts it off once a given predicate stops being true.

rlist = list(itertools.takewhile(lambda x: x >= 20, inputlist))

Incidentally, partial is not very Pythonic, and neither is itertools. GvR is on record as disliking higher-order functional-style programming (note the downgrading of reduce from built-in to a module member in 3.0). Attributes like "elegant" and "readable" are in the eye of the beholder, but if you're looking for Pythonic in the purest sense, you want the while loop.