Condition checking vs. Exception handling [duplica

2019-02-08 04:46发布

问题:

This question already has an answer here:

  • Using try vs if in python 9 answers

When is exception handling more preferable than condition checking? There are many situations where I can choose using one or the other.

For example, this is a summing function which uses a custom exception:

# module mylibrary 
class WrongSummand(Exception):
    pass

def sum_(a, b):
    """ returns the sum of two summands of the same type """
    if type(a) != type(b):
        raise WrongSummand("given arguments are not of the same type")
    return a + b


# module application using mylibrary
from mylibrary import sum_, WrongSummand

try:
    print sum_("A", 5)
except WrongSummand:
    print "wrong arguments"

And this is the same function, which avoids using exceptions

# module mylibrary
def sum_(a, b):
    """ returns the sum of two summands if they are both of the same type """
    if type(a) == type(b):
        return a + b


# module application using mylibrary
from mylibrary import sum_

c = sum_("A", 5)
if c is not None:
    print c
else:
    print "wrong arguments"

I think that using conditions is always more readable and manageable. Or am I wrong? What are the proper cases for defining APIs which raise exceptions and why?

回答1:

Exceptions are much more manageable, because they define general families of things that can go wrong. In your example there is only one possible problem, so there is no advantage to using exceptions. But if you had another class that does division, then it needs to signal that you can't devide by zero. Simply returning None wouldn't work anymore.

On the other hand, exceptions can be subclassed and you can catch specific exceptions, depending on how much you care about the underlying problem. For example, you could have a DoesntCompute base exception and subclasses like InvalidType and InvalidArgument. If you just want a result, you can wrap all computations in a block that catches DoesntCompute, but you can still do very specific error handling just as easy.



回答2:

Generally, you want to use condition checking for situations which are understandable, expected, and able to be handled. You would use exceptions for cases that are incoherent or unhandleable.

So, if you think of your "add" function. It should NEVER return null. That is not a coherent result for adding two things. In that case, there is an error in the arguments that were passed in and the function should not attempt to pretend that everything is okay. This is a perfect case to throw an exception.

You would want to use condition checking and return null if you are in a regular or normal execution case. For instance, IsEqual could be a good case to use conditions, and return false if one of your conditions fails. I.E.

function bool IsEqual(obj a, obj b)
{ 
   if(a is null) return false;
   if(b is null) return false;
   if(a.Type != b.Type) return false;

   bool result = false;
   //Do custom IsEqual comparison code
   return result;
}

In that scenario, you are returning false both for the exception cases AND the "objects are not equal case". This means that the consumer (calling party) cannot tell whether the comparison failed or the objects were simply not equal. If those cases need to be distinguished, then you should use exceptions instead of conditions.

Ultimately, you want to ask yourself whether the consumer will be able to specifically handle the failure case that you encountered. If your method/function cannot do what it needs to do then you probably want to throw an exception.



回答3:

If you're asking, you should probably be using exceptions. Exceptions are used to indicate exceptional circumstances, a specific case where things work differently from other cases. This is the case for prettymuch all errors and for many other things as well.

In your second implementation of sum_, the user has to check every single time what the value was. This is reminiscent of the C/Fortran/other-languages boilerplate (and frequent source of errors) where error codes go unchecked that we avoid. You have to write code like this at all levels to be able to propagate errors. It gets messy and is especially avoided in Python.

A couple other notes:

  • You often don't need to make your own exceptions. For many cases, the builtin exceptions like ValueError and TypeError are appropriate.
  • When I do create a new exception, which is pretty useful, I often try to subclass something more specific than Exception. The built-in exception hierarchy is here.
  • I would never implement a function like sum_, since typechecking makes your code less flexible, maintainable, and idiomatic.

    I would simply write the function

    def sum_(a, b):
        return a + b
    

    which would work if the objects were compatible and if not it would already throw an exception, the TypeError that everyone is used to seeing. Consider how my implementation works

    >>> sum_(1, 4)
    5
    >>> sum_(4.5, 5.0)
    9.5
    >>> sum_([1, 2], [3, 4])
    [1, 2, 3, 4]
    >>> sum_(3.5, 5) # This operation makes perfect sense, but would fail for you
    8.5
    >>> sum_("cat", 7) # This makes no sense and already is an error.
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in sum_
    TypeError: cannot concatenate 'str' and 'int' objects
    

    My code was shorter and simpler yet is more robust and flexible than yours. This is why we avoid typechecking in Python.



回答4:

My main reason for preferring exceptions to status returns has to do with considering what happens if the programmer forgets to do his job. With exceptions, you might overlook catching an exception. In that case, your system will visibly fail, and you'll have a chance to consider where to add a catch. With status returns, if you forget to check the return, it will be silently ignore, and your code will continue on, possibly failing later in a mysterious way. I prefer the visible failure to the invisible one.

There are other reasons, which I've explained here: Exceptions vs. Status Returns.



回答5:

Actually, the problem of using exceptions lies in the business logic. If the situation is exception (i.e. should not happen at all), an exception can be used. However, if the situation is possible from the business logic point of view, when it should be handled by conditional checking, even if this condition looks much more complicated.

For example, here is the code that I met in the prepared statement, when the developer is setting parameter values (Java, not Python):

// Variant A
try {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId());
} catch (Exception e) {
  ps.setNull(1, Types.INTEGER);
}

With conditional checking this would be written like this:

// Variant B
if (enterprise != null && enterprise.getSubRegion() != null
  && enterprise.getSubRegion().getRegion() != null
  && enterprise.getSubRegion().getRegion().getCountry() != null) {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId()); 
} else {
  ps.setNull(1, Types.INTEGER);
}

Variant B seems much more complicated from the first sight, however, it is the correct one, since this situation is possible from the business point of view (the country may not be specified). Using exceptions will cause performance issues, and will lead to misunderstanding of code, since it will not be clear, is it acceptable for the country to be empty.

Variant B can be improved by using auxiliary functions in EnterpriseBean that will return the region and country immediately:

public RegionBean getRegion() {
  if (getSubRegion() != null) {  
    return getSubRegion().getRegion();
  } else {
    return null;
  }
}

public CountryBean getCountry() {
  if (getRegion() != null) {
    return getRegion().getCountry();
  } else {
    return null;
  }
}

This code uses something like chaining, and each get method seems simple enough and is using only one predecessor. Therefore variant B can be rewritten as follows:

// Variant C
if (enterprise != null && enterprise.getCountry() != null) {
  ps.setInt(1, enterprise.getCountry().getId());
} else {
  ps.setNull(1, Types.INTEGER);
}

Also, read this Joel article about why exceptions should not be overused. And an essay by Raymon Chen.



回答6:

You should throw exception when the parameter contains an unexpected value.

With your examples, I would recommend to throw exception when the two parameters are of different types.

To throw an exception is an elegant way to abort a service without cluttering up your code.



回答7:

Maybe sum_ looks fine alone. What if, you know, it actually is used?

#foo.py
def sum_(a, b):
    if type(a) == type(b):
        return a + b

#egg.py
from foo import sum_:
def egg(c = 5):
  return sum_(3, c)

#bar.py
from egg import egg
def bar():
  return len(egg("2"))
if __name__ == "__main__":
  print bar()

If you ran bar.py you would get:

Traceback (most recent call last):
  File "bar.py", line 6, in <module>
    print bar()
  File "bar.py", line 4, in bar
    return len(egg("2"))
TypeError: object of type 'NoneType' has no len()

See -- usually one calls a function with the intent to act on its output. If you simply "swallow" the exception and return a dummy value, who uses your code will have an hard time troubleshooting. First off, the traceback is completely useless. This alone should be enough reason.

Who wants to fix this bug would have to first doublecheck bar.py, then analize egg.py trying to figure out where exactly the None came from. After reading egg.py they'll have to read sum_.py and hopefully notice the implicit return of None; only then they understand the problem: they failed the type check because of the parameter egg.py put in for them.

Put a bit of actual complexity in this and thing get ugly really fast.

Python, unlike C, is written with the Easier to Ask Forgiveness than Permission principle in mind: if something goes wrong, I'll get an exception. If you pass me a None where I expect an actual value, things will break, the exception will happen far away from the line actually causing it and people will curse in your general direction in twenty different languages, then change the code to throw a suitable exception (TypeError("incompatible operand type")).