Short circuit list of functions python

Viewed 100

I have a class with some method like this

class Validator:
    def _is_valid_code(self):
        return bool(#some logic here)

    def _is_valid_role(self):
        return bool(#some logic here)

    def _is_valid_age(self):
        return bool(#some logic here)
    .
    .
    .

if all of them are True I want to continue, else return a string

if not _is_valid_code():
    #short circuit and return a string
    return "not valid code"
if not _is_valid_role():
    #short circuit and return a string
    return "not valid role"
if not _is_valid_age():
    #short circuit and return a string with description
    return "not valid age"
#if we got here the request is valid so return a valid response
return valid_scenario

Now I do not have any problem with this because there are just three conditions, but for example, if I have more, let say like 10 conditions, I would end up with a lot of if scenarios, so I was thinking if something like this is possible, where I just add the condition logic and the condition to the list and that's it, the problem is to get the string value if the condition is false

conditions = [_is_valid_code(), _is_valid_role(), _is_valid_age()]
if all_conditions_true(conditions):
    return valid_scenario
else:
    return last_message_before_shortcut

Also want to know if this is valid and if this could be considered pythonic and recommended

3 Answers

You can use a dict to store the functions mapped to their respective strings.

checks = {
    _is_valid_code: "not valid code",
    _is_valid_role: "not valid role",
    _is_valid_age: "not valid age",
    }

for f in checks:
    if not f():
        return checks[f]
return valid_scenario

Note that dicts preserve insertion order in Python 3.7+. If you're using an older version or want to be stricter about the ordering, use an OrderedDict or list of tuples containing functions and strings.


This by itself is perfectly Pythonic, but I don't know the context so I can't say for sure in your case. For example, if valid_scenario isn't a string, I'd strongly recommend using exceptions instead of having a mixed return type. This question covers that: Why should functions always return the same type? And I'd recommend the same if the functions are checking for error conditions.

I would do something like this:

def one():
    """string from one"""
    return False 

def two():
    """string from two"""
    return False 

def three():
    """string from three"""
    return True

def four():
    """string from four"""
    return False

conditions=[one, two, three, four]

Then use next to find the first True or False as appropriate to read the associated doc string from the function:

>>> print( next(f.__doc__ for f in conditions if f()) )  
string from three  

You can use a default with next to indicate all are the same (True or False as the case may be):

next((f.__doc__ for f in conditions if f()), "All False")

Or, as you stated the desire:

def validator(conditions):
    return next((f.__doc__ for f in conditions if f()), None)

Advantages:

  1. If you use this approach in a class, the strings are automatically class in scope (not recreated each time);

  2. Easier to edit (for me) than [(one,"string from one"),(two,"string from two"),...];

  3. String and associated test code are right there together in the function;

  4. next with a conditional provides a short-circuit and stops at the first targeted boolean;

  5. The list of conditions [one,two,three,...] is easier for ME to see and understand vs [(one,"string from one"),(two,"string from two"),...]

  6. And one more thing! You can skip the __doc__ string entirely and just use descriptive function names with:

    next((f.__name__ for f in conditions if f()), default)

  7. If you want to do it old school you can use next with a dict of func_name:"associated string" as well.

I've written these things like:

for condition_is_ok, error_msg in [
    (_is_valid_code, "not valid code"),
    (_is_valid_role, "not valid role"),
    (_is_valid_age, "not valid age"),
]:
    if not condition_is_ok():
        return error_msg

Alternatively, change the way your validators work:

  • If they fail, return a string explaining why.
  • If they pass, return None.
class Validator:
    def _code_error(self):
        if ...:
            return "not valid code"
        return None

for checker in [_code_error, ...]:
    result = checker
    if result is not None:
        return result
Related