Jace Browning
Jace Browning

Reputation: 12672

Pythonic way to "run X before Y if Y fails"?

I am looking for a better way to implement this sort of logic:

if not a():
    if not b():
        c()
        b()
    a()

Another form:

try:
   a()
except:
   try:
      b()
      a()
   except:
      c()
      b()
      a()

In words, "Try to run A. If we can't do A, we need to do B first. If we can't do B, we need to do C first, etc."

Upvotes: 2

Views: 127

Answers (6)

Preet Kukreti
Preet Kukreti

Reputation: 8617

Create a function like fallback_until_success(func_list), where func_list = [a, b, c]. If you have arguments, you can bind them e.g. by passing tuples of (func, *args, **kwargs).

Then you can go through the list in a while loop (including fallback-backtracking per iteration) until you get a success or hit the end of the list; if you dont get a success, return the last exception (or a list of exceptions).

However, this seems like a case where having an initial test to inform your code path is better than trying to do the damage first and backtracking. What you are doing is abusing exceptions as a message-passing service.

Update: well its too late now anyway, but here is a concrete example:

def fallback_until_success(func_list):
    index = 0
    results = []
    exceptions = []
    while (index < len(func_list)):
        try:
            print func_list[index::-1] # debug printing
            for func_spec in func_list[index::-1]:
                #func, args, kwargs = func_spec  # args variant
                #result = func(*args, **kwargs)
                func = func_spec 
                result = func()
                results.append(result)
            break
        except Exception, e:
            exceptions.append(e)
            index += 1
            results = []
            continue
        break
    return results, exceptions

# global "environment" vars
D = {
        "flag1": False,
        "flag2": False,
    }

def a():
    if not D["flag1"]:
        failstr = "a(): failure: flag1 not set"
        print failstr
        raise Exception(failstr)
    print "a(): success"
    return D["flag1"]

def b():
    if not D["flag2"]:
        failstr = "b(): failure: flag2 not set"
        print failstr
        raise Exception(failstr)
    else:
        D["flag1"] = True
        print "b(): success"
    return D["flag2"]

def c():
    D["flag2"] = True
    print "c(): success"
    return True

# args variant
#results, exceptions = fallback_until_success([(a, [], {}), (b, [], {}), (c, [], {})])

results, exceptions = fallback_until_success([a, b, c])
print results
print exceptions

The output:

[<function a at 0x036C6F70>]
a(): failure: flag1 not set
[<function b at 0x03720430>, <function a at 0x036C6F70>]
b(): failure: flag2 not set
[<function c at 0x037A1A30>, <function b at 0x03720430>, <function a at 0x036C6F70>]
c(): success
b(): success
a(): success
[True, True, True]
[Exception('a(): failure: flag1 not set',), Exception('b(): failure: flag2 not set',)]

Of course, this is based on exceptions, but you could modify this to base success/failure on return values also.

Upvotes: 1

neilr8133
neilr8133

Reputation: 152

Why expose all of that to the caller? Caller shouldn't know/care about the details of how a widget works.. Why not insulate the client code from the "guts" by doing something like:

do_stuff()  # This is the only call you make directly

def do_stuff():
    ## commands for Step A, in this case
    ## try to update the changeset
    result = False
    # Do stuff and store result
    if (result == False):
        result = step_B()
    return result

def step_B():
    ## Commands for Step B, in this case
    ## try to pull the repository
    result = False
    # Do stuff and store the result
    if (result == False):
        result = step_C()
    return result

def step_C():
    ## Commands for Step C, in this case
    ## try to clone the repository
    ## and set `result' to True or False
    result = False
    # Do stuff and set `result'
    return result

Upvotes: 0

Jace Browning
Jace Browning

Reputation: 12672

Based on Shao-Chuan Wang's answer, I think I might end up doing something like this:

any(all((a())),
    all((b(), a())),
    all((c(), b(), a())))

Upvotes: 0

Andrew Fuchs
Andrew Fuchs

Reputation: 306

This should work. Note that it if a fails, it will execute b,c,a. If b then fails, it will execute c,a,b -- that is, not in original order but should be good if order isn't of any particular preference.

ops = [a,b,c]

while op = ops.pop(0):
  if (op()): 
    continue
  ops.append(op)

Upvotes: 0

Shao-Chuan Wang
Shao-Chuan Wang

Reputation: 1030

Not sure if you feel 'better' about this one; here is an alternative. I believe some people like it and some people don't.

a() or (b(),a())[0] or (c(),b(),a())[0]

Here is validation test:

def a(ret):
    print 'run a, a succeeded?', ret
    return ret

def b(ret):
    print 'run b, b succeeded?', ret
    return ret

def c(ret):
    print 'run c, c succeeded?', ret
    return ret

And

a(False) or (b(False),a(False))[0] or (c(True),b(False),a(False))[0]

gives

run a, a succeeded? False
run b, b succeeded? False
run a, a succeeded? False
run c, c succeeded? True
run b, b succeeded? False
run a, a succeeded? False

And

a(False) or (b(True),a(False))[0] or (c(True),b(True),a(False))[0]

gives

run a, a succeeded? False
run b, b succeeded? True
run a, a succeeded? False

Upvotes: 1

Nikita Borisov
Nikita Borisov

Reputation: 216

How about:

while not a():
    while not b():
        c()

This only works as long as c() is expected to eventually make b() succeed (likewise for b() and a()), but this is a relatively common pattern for me.

Upvotes: 1

Related Questions