Is there a Python idiom for evaluating a list of functions/expressions with short-circuiting?
Asked Answered
I

1

8

I wrote a simple script to solve a "logic puzzle", the type of puzzle from school where you are given a number of rules and then must be able to find the solution for problems like "There are five musicians named A, B, C, D, and E playing in a concert, each plays one after the other... if A goes before B, and D is not last ... what is the order of who plays when?" etc.

To evaluate possible solutions, I wrote each "rule" as a separate function which would evaluate if a possible solution (represented simply as a list of strings) is valid, for example

#Fifth slot must be B or D
def rule1(solution):
    return solution[4] == 'B' or solution[4] == 'D'

#There must be at least two spots between A and B
def rule2(solution):
    returns abs(solution.index('A') - solution.index('B')) >= 2

#etc...

I'm interested in finding the Pythonic way to test if a possible solution passes all such rules, with the ability to stop evaluating rules after the first has failed.

At first I wrote the simplest possible thing:

def is_valid(solution):
    return rule1(solution) and rule2(solution) and rule3(solution) and ...

But this seemed rather ugly. I thought perhaps I could make this read a bit more elegant with something like a list comprehension...

def is_valid(solution)
    rules = [rule1, rule2, rule3, rule4, ... ]
    return all([r(solution) for f in rules])

... but then I realized that since the list comprehension is generated before the all() function is evaluated, that this has the side effect of not being short-circuited at all - every rule will be evaluated even if the first returns False.

So my question is: is there a more Pythonic/functional way to be able to evaluate a list of True/False expressions, with short-circuiting, without the need to write out a long list of return f1(s) and f2(s) and f3(s) ... ?

Improve answered 4/8, 2010 at 13:8 Comment(0)
F
13

Use a generator expression:

rules = [ rule1, rule2, rule3, rule4, ... ]
rules_generator = ( r( solution ) for r in rules )
return all( rules_generator )

Syntactic sugar: you can omit the extra parentheses:

rules = [ rule1, rule2, rule3, rule4, ... ]
return all( r( solution ) for r in rules )

A generator is (basically) an object with a .next() method, which returns the next item in some iterable. This means they can do useful things like read a file in chunks without loading it all into memory, or iterate up to huge integers. You can iterate over them with for loops transparently; Python handles it behind-the-scenes. For instance, range is a generator in Py3k.

You can roll your own custom generator expressions by using the yield statement instead of return in a function definition:

def integers():
    i = 0
    while True:
        yield i

and Python will handle saving the function's state and so on. They're awesome!

Fate answered 4/8, 2010 at 13:11 Comment(3)
So is the basic difference here omitting the brackets in return all([r(solution) for r in rules]), thus not creating a list of all results before all() is evaluated?Improve
Yes. In both cases the argument to all is evaluated before being passed on, but evaluating a list comprehension creates the entire list in memory whereas evaluating a generator expression creates a generator object, which loads the elements on demand.Fate
I was looking for similar -- but I wanted a short circuit as soon as anything was True instead of False. I changed all() to any() and it worked.Marinna

© 2022 - 2024 — McMap. All rights reserved.