4

Assume that I wish to perform an action on a string (e.g., print the string) if, and only if, the string first passes several tests. The tests are diverse (and may even be complex functions themselves), and "passing" a test may mean different things depending on the specific test. For example, some tests can be discretized to result in True/False. Other tests are considered "passed" if the underlying function doesn't raise an exception.

Moreover, assume that I wish to have all tests (there may be dozens of them!) combined in a single custom function (e.g., validate_string).

My objective is to write code (i.e., a combination of validate_string() and main()) that works off test after test and only prints the string if none of the tests in validate_string() fails.

One could naively imagine utilizing a custom function that employs exception handling to simply break the script if any of the tests in function validate_string() fails. That way the code exits before the print command in main() is executed. However, to me such a design misuses exception handling and I would prefer a different design.

def validate_string(my_string, required_end):
    try:
        if not my_string[-1] == required_end:
            raise ValueError
    except ValueError:
        exit('ERROR: String `%s` does not end with `%s`.' % (my_string, required_end))
    try:
        complex_function(my_string)
    except:
        exit('ERROR: String `%s` could not be processed.' % (my_string))

def main(my_string):
    validate_string(my_string, "o")
    print my_string

> main("foo bar")
ERROR: String `foo bar` does not end with `o`.

Can anybody recommend a better overall design for this type of problem?

  • 1
    Not sure why I received a downvote. This seems like a reasonable software design question to me. – Michael Gruenstaeudl Jul 14 '17 at 11:30
  • 1
    "Pythonic" sounds like a marketing term rather than anything related to programming. This makes it impossible to understand what do you mean by "such a design strikes me as very non-pythonic." Thus, the question would be probably closed as unclear. Can you rewrite your question using common software engineering terms instead of "pythonic"? Try to explain the exact problem you're having. Right now the question is basically, "This doesn't look right," which may be just the way you perceive things. – scriptin Jul 14 '17 at 13:01
  • 1
    Also, your code sample looks like an unnecessarily complicated way of doing "else" clause by throwing and immediately catching an exception. – scriptin Jul 14 '17 at 13:02
  • @scriptin Thank you for your feedback. I have rewritten the question using less ambiguous terminology. I believe that it clearly defines the problem now. Would you please take a look at it and attempt an answer? – Michael Gruenstaeudl Jul 14 '17 at 15:16
  • @scriptin Yes, I am aware that my code example misuses exception handling for the purpose of having a general-use switch to exit main(). In fact, I am looking for an alternative. What alternative would you recommend me? – Michael Gruenstaeudl Jul 14 '17 at 15:20
2
+50

Regardless of how this is implemented, you haven't been very clear about the behavior you want.

My objective is to write code (i.e., a combination of validate_string() and main()) that works off test after test and only prints the string if none of the tests in validate_string() fails.

This is simple enough to achive. But what should happen when a test fails? Do you want to know why it didn't print? If not this isn't validation. This is filtering. 80% of the work here isn't when this passes. It's when it doesn't.

One could naively imagine utilizing a custom function that employs exception handling to simply break the script if any of the tests in function validate_string() fails. That way the code exits before the print command in main() is executed. However, to me such a design misuses exception handling and I would prefer a different design.

No, that's not a misue of exception handling. That's not handling exceptions.

Exeception handling is when you catch an exception and recover from it. You close any opened resources and put the system back into a useable state. Likely you'll want to log the exception so people can debug the system. You may also need to notify the user of what happened so they stop waiting for something to happen.

Not handling an exception means the program is going to terminate. That also may be what you want. Terminating when an exception happens ensures the system doesn't go into a weird state and start destroying things you care about.

You need to decide what should happen when the string is invalid.

By calling your method validate_string() you make it seem like something bad has happened when it fails. If all you want is for the system to queitly not print the "invalid" string and then go about the rest of it's business as if nothing was ever wrong then your string isn't "invalid", it's filtered. filter_string() would be a better name.

The tests are diverse (and may even be complex functions themselves), and "passing" a test may mean different things depending on the specific test. For example, some tests can be discretized to result in True/False. Other tests are considered "passed" if the underlying function doesn't raise an exception.

The tests may be diverse but we need to make them consistent one way or another to work with them. You can do that by either rewriting the tests or wrapping the tests that need it in something that makes them consistent.

If we make them all return a bool when the execption is something we want to recover from this works:

def printValidString(str, predicate_list):
  if all(f(str) for f in predicate_list):
    print str

This will only print if all checks pass. Any unhandled exection will blow right out of that and cause us to terminate. Any handled exception can be turned into a false and supress the printing.

def isSunny(mayThrow, args):
  try:
    mayThrow(args)
    return True
  except ExceptionTypeWeHandle:
    return False

Why is this an ok design? Because the prohibition against using exceptions to branch is about if else being better. If else doesn't work when moving through contexts.

Is this an awesome design? No. It would be much better to have consistant checks from the start and use them, well, consistantly. But presuming you're stuck with inconsistant checks, wrapping them to make them consistant works.

Under this design there are three posible outcomes.

  1. Every check passes, the string is printed, and execution continues
  2. A check fails in an expected way, the string is not printed, and execution continues
    1. a check returned false on it's own
    2. isSunny() ran a check, handled an expected exception, and returned false
  3. A check fails in an unexpected way, the string is not printed, and execution is halted after the unexpected exception bubbles up the call stack

Is something like that what you had in mind?

  • Thanks for sharing your thoughts and insights. That is excellent feedback. – Michael Gruenstaeudl Jul 23 '17 at 19:45
2

Have you tried appling a modified version of builder pattern?

I don't have really experience in Python but I think it could be very reusable if used as:

assert StringValidator.target(myString).notEndsWith('hello').isLowercase().customTest(lambda x: x.hasString('a')).test() is 'passed'

With the possibility to change the order to anything you like to obtain the same end result

assert StringValidator.target(myString).customTest(lambda x: x.isUpperCase()).notEndsWith('hello').test() is 'passed'

And your implementation could be something like this (but properly done):

class StringValidator:->

    builder:StringValidationBuilder = new StringValidationBuilder


    static target(myString:String):->
      return builder.setTarget(myString)

    private static class StringValidationBuilder:->
        constructor()
        setTarget(target:String):->
           this.target=target
           return this
        test(builder:StringValidationBuilder):->
            return "passed" if target is not null and (if isEmptyTestedFlag and isEmpty or not isEmtyTested) .... otherwise "not passed because of #{failureReason}".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.