6

Suppose I have a function with side effects (in the example, the side effect is that something is printed). Is there any version of the any() or any construction of the the list iterable which would NOT trigger side effects after finding a True result?

Example, suppose this function:

def a(x):
   print("A function got: " + str(x))
   return x == 2

One might hope that this call would do the trick. Of course, it does not:

any([
  a(i) for i in range(5)
])

Which prints:

A function got: 0
A function got: 1
A function got: 2
A function got: 3
A function got: 4

But I would like it to print this instead:

A function got: 0
A function got: 1
A function got: 2

Why? Range is an iterable, the list comprehension is producing an iterable, I would sort of expect Python to chain those together and stop executing the whole thing as soon as the any() function stops consuming, which it should do once it reaches the first True.

What am I misunderstanding? What version of this would behave in this way, if any?

Bill Huneke
  • 746
  • 4
  • 12
  • 4
    Um... don't use a list comprehension? – Kelly Bundy Feb 03 '20 at 15:54
  • 7
    Call `any(a(i) for i in range(5))`, without `[` and `]`. With the brackets, you are creating a list comprehension, so first it is creating a list with all the elements produced by the generator and then passing it through `any`. Without the brackets, `any` just iterates as many elements in the generator as necessary. – jdehesa Feb 03 '20 at 15:54
  • Wow. That construction is so great! I did not know. I'm going to use that all over the place now. What a difference a few square brackets makes. mind blown. Can you paste this as an answer @jdehesa ? – Bill Huneke Feb 03 '20 at 16:01
  • 2
    @BillHuneke It's fine, take the one that is already posted as accepted, no need to have duplicate answers. – jdehesa Feb 03 '20 at 16:08

4 Answers4

4

Whenever you use a list, all the values are evaluated. The only way to get lazy evaluation is to keep it as an iterator. You can do this with a generator comprehension:

any(a(i) for i in range(5))

To be clear, using the brackets is the same as doing any(list(a(i) for i in range(5))).

Kevin Raoofi
  • 1,023
  • 11
  • 16
  • 3
    Not quite true, `any(map(a, range(5))` is lazy without generator. – Kelly Bundy Feb 03 '20 at 16:05
  • @HeapOverflow How come? `map` is a generator... It is just like writing `any(a(i) for i in range(5))` – Tomerikoo Feb 03 '20 at 16:08
  • @Tomerikoo What makes you think it's a generator? – Kelly Bundy Feb 03 '20 at 16:08
  • 1
    [this](https://docs.python.org/3/library/functions.html#map): *Return an iterator that applies function to every item of iterable, yielding the results* – Tomerikoo Feb 03 '20 at 16:09
  • @Tomerikoo That doesn't say anything about it being a generator. – Kelly Bundy Feb 03 '20 at 16:10
  • 3
    *__yielding__ the result* – Tomerikoo Feb 03 '20 at 16:11
  • @HeapOverflow `map` was a function that returned a list in Python 2; in Python 3 it is a *type* that implements the iterator protocol, calling `a` on demand as a value is requested from the `map` value. – chepner Feb 03 '20 at 16:12
  • @Tomerikoo That doesn't mean it's a generator. Try [this](https://stackoverflow.com/a/6416585/12671057). – Kelly Bundy Feb 03 '20 at 16:13
  • @chepner Yes. And? What does that have to do with generators? – Kelly Bundy Feb 03 '20 at 16:13
  • 4
    A `map` is a generator (in the colloquial sense of generating values), but it is not a `generator` (which is the Python type created by generator expressions and generator functions). – chepner Feb 03 '20 at 16:13
  • 2
    @HeapOverflow: `any(map(a, range(5))` is indeed lazy. It won't get interpreted until you write the missing `)`. – Eric Duminil Feb 03 '20 at 16:16
  • @EricDuminil Darn, I had even specifically looked at the parentheses and somehow convinced myself that there are enough :-) – Kelly Bundy Feb 03 '20 at 16:21
  • Yeah, that's right. It's important that it's kept as an iterator to be lazy. I suppose I meant that a generator comprehension is required over a list comprehension. – Kevin Raoofi Feb 03 '20 at 17:43
1

You can also simply run the loop and break out of it:

for i in range(5):
    if a(i):
        break

So

def my_any(func, it):
    for i in it:
        if func(i):
            break

my_any(a, range(5))
Jan Christoph Terasa
  • 5,781
  • 24
  • 34
0

another valid solution is as follow first import from itertool takewhile then,

l=list(takewhile(lambda x:a(x) is False, range(5)))
Mohammed Khalid
  • 155
  • 1
  • 6
-3

You can use below code by adding condition inside your list comprehension

def a(x):
   print("A function got: " + str(x))

any([
  a(i) for i in range(5) if i<3 
])
Akash senta
  • 483
  • 7
  • 16
  • 3
    This assumes, that you know the return condition and can directly apply it to the data. In this extremely simple example this might work, but `a(x)` might be a large calculation which you cannot expect to know in advance (this is why you have it as a function in the first place). This answer completely misses the point of the question... – LeoE Feb 03 '20 at 16:06
  • 2
    Also, the question is not about **HOW** to achieve the desired output, but more as to **WHY** we get the actual one – Tomerikoo Feb 03 '20 at 16:07