[Tutor] custom error classes: how many of them does one need?
Albert-Jan Roskam
fomcl at yahoo.com
Sun Jul 7 20:37:13 CEST 2013
----- Original Message -----
> From: Steven D'Aprano <steve at pearwood.info>
> To: tutor at python.org
> Cc:
> Sent: Friday, July 5, 2013 4:20 AM
> Subject: Re: [Tutor] custom error classes: how many of them does one need?
>
> On 05/07/13 05:40, Albert-Jan Roskam wrote:
>> Hello,
>>
>> I created a custom error class like under [A] below (see
> http://pastebin.com/JLwyPsRL if you want to see highlighted code). Now I am
> thinking: is this specific enough? "When
>> creating a module that can raise several distinct errors, a common practice
> is
>> to create a base class for exceptions defined by that module, and subclass
> that
>> to create specific exception classes for different error conditions"
> (http://docs.python.org/2/tutorial/errors.html)
>> Given that I have many retcodes, is it a better approach to define a class
> factory that generates a custom exception for each retcode on-the-fly? (under
> [B]). Using a class factory seems a little unusual, or isn it? More generally:
> what criteria should one follow when distinguishing error classes?
>
> Return codes are not exceptions, and generally speaking using return codes for
> errors is *strongly* discouraged unless you have a language without exceptions.
> (Although the designers of Go language disagree. I think they are wrong to do
> so.) Although looking forward, I see that even though you call them
> "retcodes", you don't actually use them as return codes.
Hi Steven, Alan, Eryksun,
In my app, the return codes are from C functions. < 0 means warning, 0 means OK, > 0 means error.
You're right that I was sloppy as I used the terms retcodes and exceptions interchangably.
>
>> global retcodes
>> retcodes = {0: "OK", 1: "Error_X", 2:
> "Error_Y"}
>
> You don't need to declare a global variable global in the global part of
> your file. Although Python allows it, it is redundant and rather silly. Delete
> the line "global retcodes".
Oops, despite of an earlier thread I still did this wrong. I used to think that "global" made The Unforgiveable Thing Of All Times (using a global variable) less bad because at least it was explicitly "declared". Silly me.
>> #############################
>> # [A] one custom exception for all purposes, but with a descriptive message
>> #############################
>>
>> class single_custom_exception(Exception):
>> def __init__(self, retcode, message):
>> self.retcode = retcode
>> message += " [%s]" % retcodes.get(retcode)
>> Exception.__init__(self, message)
>
> Exceptions are classes, and like all classes, the convention is to name them in
> CamelCase.
>
>
>> #############################
>> # [B] generating custom exceptions with a class factory
>> #############################
>>
>> class BaseError(Exception): pass
>>
>> def throw(retcode, message):
>> """class factory that generates an error class for
>> each return code label"""
>> class custom_exception(BaseError):
>> def __init__(self):
>> BaseError.__init__(self, message)
>> exception = retcodes.get(retcode)
>> custom_exception.__name__ = exception
>> return custom_exception
>
> This is overkill. No need to create a custom class *on the fly* for each return
> code. Doing so is pointless: the caller *cannot* catch them individually,
> because they are created on the fly. So they can only catch BaseError, then
> inspect the exception's details. If you have to do that, then having
> individual classes is a waste of time. You might as well just use a single class
> and be done with it.
>
> What you would like to do, but cannot, since neither Error_X nor Error_Y are
> defined:
>
> try:
> code_that_might_fail()
> except Error_X:
> handle_error_X
> except Error_Y:
> handle_error_Y
>
>
>
> What you are forced to do:
>
> try:
> code_that_might_fail()
> except BaseError as err:
> if err.retcode == 0:
> print ("Error: no error occurred!")
> elif err.retcode == 1:
> handle_error_X
> elif err.retcode == 1:
> handle_error_Y
> else:
> print ("Error: an unexpected error occurred!")
>
>
> which is ugly enough, but it makes the class factory redundant.
It might be ugly, but I think I can live with it. For now, the single custom error works. I was just trying to be critical towards my own code. And it was fun to write a factory class, of course. ;-)
> So, how should you handle this scenario?
>
> Use a separate exception class for each high-level *category* of errors. E.g.
> Python has:
>
> TypeError
> ValueError
> ZeroDivisionError
> UnicodeEncodeError
>
> etc. Within your app, can you divide the return codes into application-specific
> groups? If not, then you just need a single exception class:
I tried doing that. Semantically (I mean just by the looks of the messages), I can categorize them. Then I end up with about 15 exceptions. After having done that, I started to be convinced not to follow this route. It might be nice to be able to choose whether warnings (retcodes < 0) should be treated as errors or not (I believe in options() of CRAN R this is called warningsAsErrors). Would the code below be a good implementation (I also experimented with cmp)?
# this is a global variable (perhaps in __init__.py)
# if warningsAsErrors > 0, even warnings will raise an error.
import operator
warningsAsErrors = bool(os.getenv("warningsAsErrors"))
warningsAsErrors = operator.ne if warningsAsErrors else operator.gt
# this comes after each C function call
retcode = someCfunc(... blah...)
if retcode and oper(retcode, 0):
raise MyCustomError
> MyApplicationError
>
>
> If you do have a bunch of application-specific groups, then you might develop a
> hierarchical family of exceptions:
>
>
> WidgetError
> +-- WrongWidgetTypeError
> +-- InvalidWidgetError
> +-- WidgetNotInitialisedError
>
>
> WidgetGroupError
> +-- MissingWidgetError
> | +-- MissingCriticalWidgetError
> +-- TooManyWidgetsError
>
>
> SpoonError
> +-- TeaspoonError
> +-- SoupSpoonError
> +-- DesertSpoonError
>
DesertSpoonError? LOL! ;-)
> The parent exceptions, WidgetError, WidgetGroupError and SpoonError do not need
> to share a common base class.
>
> This hierarchy might completely obliterate the need for return codes.
> Python's build-in exceptions generally do not. However, maybe you do still
> need them, similar to the way Python's OSError and IOError have error codes.
> Assuming you do:
>
>
> def someFunc():
> perform_calculations()
> if there are no spoons:
> raise TeaspoonError("cannot stir my tea")
> elif some other error:
> raise InvalidWidgetError(retcode=99, message="Can't use red
> widget")
> else:
> return some value
>
>
>
> --
> Steven
> _______________________________________________
> Tutor maillist - Tutor at python.org
> To unsubscribe or change subscription options:
> http://mail.python.org/mailman/listinfo/tutor
>
More information about the Tutor
mailing list