Python3.8 `all()` not short circuiting

Okay I may just be having a bad workday but to my knowledge all() should short circuit according to the docs.

all() should be equivalent to:

def all(iterable):
    for element in iterable:
        if not element:
            return False
    return True

Which is also equivalent to daisy chaining and.

So the following should do the same thing:

all((True, print('This should print and stop'), print("this shouldn't print")))
True and print('This should print and stop') and print("this shouldn't print")

Yet the top one does both prints (i.e. fully evaluated each part) but the bottom short circuited properly.

Is this a bug with Python (3.8.2) or am I not getting this right all of a sudden?

Repl with example: https://repl.it/join/scxbpnhc-syntactical01

Upvotes: 2

Views: 107

Answers (2)

dawg
dawg

Reputation: 104072

Python's all take an iterable as an argument

During the construction of the example in your question, in this case a tuple, the functions inside the tuple literal are called and the return value in inserted into the tuple literal; no differently than:

>>> tup=(True, print('this'), False, print('not this'))
this
not this

If you want to have a form of assemble on the fly you could do something like this:

def example_func(*args):
    return args

g2all=(True, (print, 'this', 'and this'), False, (print, 'not this'), (example_func,1,2,3,4))

This is relying on the fact that you can refer to a function by name and, without the () call operator the function is not called yet:

>>> g2all
(True, (<built-in function print>, 'this', 'and this'), False, (<built-in function print>, 'not this'), (<function example_func at 0x10d0a9430>, 1, 2, 3, 4))

You can then use that in an all with a generator:

>>> all(f[0](*tuple(f[1:])) if isinstance(f, tuple) and callable(f[0]) else f for f in g2all)
this and this
False

and the calling of the functions stop when all finds a Falsie value.

Upvotes: 1

chepner
chepner

Reputation: 532033

The iteration inside all short-circuits, but each call to print has to evaluate in order to create the tuple you pass as an argument before all is ever called.

There's no simple way to create an arbitrary lazy sequence in Python without a def statement. You can write

def foo():
    yield True
    yield print("This should print and stop")
    yield print("this shouldn't print")

all(foo())

but there's no pure expression equivalent. You need to iterate over something to use a generator expression, for example

(x(y) for x, y in zip([id, print, print], 
                      [True, 
                       "This should print and stop",
                       "this shouldn't print"]))

would work as you expect.

>>> all(x(y) for x, y in zip([id, print, print], [True, "This should print and stop", "this shouldn't print"]))
This should print and stop
False

Now, print is called inside all, rather than as part of evaluating the argument to pass to all.

Upvotes: 4

Related Questions