3

I realize that there have already been discussions on whether to use If/Else or Try/Except blocks. Such a question is located here: Better to 'try' something and catch the exception or test if its possible first to avoid an exception?

But I'd like to expand the discussion a little further to nested try/except and nested if/elif/else logic blocks. Here's the set up... I want to write a function that will allow users to provide either a string literal, an integer, or an iterable. This is a high level function that will provide some level of abstraction to other functions I have written. My code is like so:

def high_level_func(parameter = None):
    """
    :param parameter: Can accept the string 'All', a single integer, or an iterable such 
        as a range or a list or tuple of ints.
    """
    try:
        if parameter.lower() == 'all'
        # .lower because str input should be case-insensitive
            return str_all_function(parameter) # Only accepts the 
                    # string 'all' - case insensitive
    except AttributeError:
        # if parameter is an int or iter end up here because those types 
        # don't have .lower() methods
        try:
            for para in parameter:
                try:
                    print(int_input_function(parameter))
                except MyException:
                    raise MyException('An iter of something other than ints was '
                                      'provided and cause this error.')
        except TypeError:
            # parameter must be an int because ints aren't iterable and end up here
            return int_input_function(parameter)

In this case let's assume that I have no idea what type of input most users will prefer (i.e. it is equally likely that any given user will pass either an int, an iter, or the string 'all'. But we can safely assume that the user will most likely never pass a list of strings or tuple of strings -illegal iters)

Is this ok to do or would I be better off checking the type of the input and doing an if/elif/else (IEE) code block? Would an IEE code block be significantly easier to read in your opinion?

Alternative suggestion: what about using a combination of try/except and IEE? The try/except might try to lower the input if it is the string literal 'all', for example, and the IEE would be nested in the except block to check the alternative cases (integer or iter or illegal type)

And in more general, how can I tell which method is the fastest without writing the function three different times and testing each one?

An even further question is, if the Try/Except is faster on average than an If/Elif/Else test, but we think If/Elif/Else has better readability, how much faster should the Try/Except method be to warrant throwing readability out the window or does readability always prevail in the face of speed? Or is this up to the coder's/team's discretion?

Community
  • 1
  • 1
boymeetscode
  • 805
  • 1
  • 9
  • 26
  • Python recommends EAFP model (Easier to Ask for Forgiveness than Permission), so in this case using an try-except would be the pythonic way to go. Personally, I would go the non-pythonic route and use if isinstance(parameter, collections.Iterable) to divide the validations, to avoid a cascade of try-excepts. As for performance, I would advice that you write a simpler version and test with the testit module. – Fernando Coelho Apr 05 '17 at 14:49
  • For added information, I did run a simplified version of this comparison over 100,000 iterations and found the If/Elif/Else with isInstance to be faster actually than the try/except method. (I did not use the testit module though.) So it seems it is a readability and efficiency win-win to use the If/Else approach in this case. Just not sure why. Try/Except method: 1.34000015258789 seconds over 100,000 loop If/Elif/Else method: 1.18899989128112 seconds over 100,000 loop – boymeetscode Apr 05 '17 at 15:12

1 Answers1

3

I think your input functions should be responsible for validating the input, not the function that calls them. After that, you can let the high level function remain high level and keep trying to use one of them until it succeeds:

def high_level_function(parameter=None):
  try:
    return str_input_function(parameter)
  except ValueError:  # Raised by str_input_function on bad input.
    pass

  try:
    return iter_input_function(parameter)  # MyException now propagates itself.
  except ValueError:  # Raised by iter_input_function on bad input.
    pass

  try:
    return int_input_function(parameter)
  except ValueError:  # Raised by int_input_function on bad input.
    pass

  # This should never be hit:
  raise ValueError("parameter has unsupported type: " + type(parameter).__name__)
Brian Rodriguez
  • 4,250
  • 1
  • 16
  • 37
  • Thanks for this insight. It is simple, clean, readable and still follows the EAFP methodology of python. Kind of mad at myself for over-thinking this problem. I tend to do that with python. Grrrr. – boymeetscode Apr 05 '17 at 17:07
  • No problem! FWIW, I think you were on the right track. – Brian Rodriguez Apr 05 '17 at 17:11
  • When I reach a `return` statment, I should leave the high_level_function, right? I'm testing my code and it appears like I am going through all the try blocks despite each try block having a return statement and I expect it to succeed but it doesn't. Making sure what I expect and what python do are the same thing. – boymeetscode Apr 06 '17 at 20:11
  • If it passes everything it'd throw that final exception, are you seeing that happen? otherwise, it means it has returned early as expected. – Brian Rodriguez Apr 06 '17 at 20:33
  • Sorry, I figured it out. Was being caused by a missing int() function. So it was throwing an AttributeError like I wanted but in the wrong spot. :-/ – boymeetscode Apr 06 '17 at 20:38