Steven, Thanks for the responses. In response to your responses....: Steven D'Aprano wrote: > Hi jmward01, > I'm not really sure what you mean by "boilerplate heavy objects". > Boilerplate generally applies to the amount of source code you have to > write. I don't think that a two line class (perhaps a couple of extra > lines if you give it a docstring) justifies the name "boilerplate": > class MySpecialException(Exception): > pass > > In my experience, that covers the majority of custom exceptions I've > needed. (More on this below.) > As for "heavy objects", it's true that classes are moderately > heavyweight: > py> sys.getsizeof(Exception) > 400 > > but the instances are relatively lightweight: > py> sys.getsizeof(Exception('spam eggs')) > 96 > > and if you're stressed out by that, you might be using the wrong > language :-) > So I'm not quite sure what specific part of this you are worried about: > memory consumption or lines of code. I am not particularly concerned with performance here. I am more worried about code clarity, boilerplate that de-incentivizes a feature and hidden values. The current exception mechanism encourages passing, effectively, un-named lists of parameters or building boilerplate to create the equivalent of named tuples (e.args[0] or building an __init__ that defines members). If the only value is the name, why not just define it where you use it instead of creating an intermediary class definition? I use string literals for state names all the time instead of creating objects and importing them all over my code because most of the time the mental overhead of imports and the like isn't worth the ease and clarity of just passing a string. This is the same argument I am making for most exceptions by suggesting this syntax.
> More below. > On Mon, Oct 12, 2020 at 09:16:37PM -0000, jmwar...@gmail.com wrote: > > I generally find exception objects are really just > > boilerplate heavy > > objects masking what I really want them to be, function calls: > > class MySpecialException(Exception): > > def __init__(self, some, variables, i, am, tracking): > > self.some = some > > ... > > To me, that looks like something of a code smell. > https://en.wikipedia.org/wiki/Code_smell > I am suspicious of exceptions that have lots of values thrown into them. > With very few exceptions (pun intended :-) the model of exceptions I > expect to see is like the bulk of built-in exceptions. They typically > apply to a single value, for a single reason. > py> ord(1) > --> TypeError: ord() expected string of length 1, but int found > > So consider that, maybe, your exceptions are doing too much. > In your code snippet, the try...except block seems to be literally > useless. You are literally raising an exception, only to immediately > catch it again, then do a test, and depending on that True/False result > of that test, either suppress the exception or re-raise it. Yeah, not the best example I agree. Very contrived. I'll think of a better one that shows more clarity. But not right now. I haven't had my coffee yet. As for variables, the correct amount is the correct amount. I mentally do think of this as just a convoluted function call passing control. When you make that mentality switch then the current mechanism starts looking either obfuscated or bloated for the reasons I said above (passing anonymous lists of parameters, e.args, or adding extra code and defining an object just to pass named parameters). I guess what I am _really_ suggesting here is more of a named return that passes to the first block capable of handling it. That's really what exceptions are conceptually doing, just make it more explicit. > > ... > > try: > > if some_test_that_fails(variables): > > raise MySpecialException(a, b, c, d, e, f) > > except MySpecialException as e: > > logger.warning(f"BAD THING {e.a} HAPPENED!") > > if not handle_it(e.a, e.b, e.c, e.f): > > raise > > ... > > I think this is more naturally written without the redundant raise > followed by catch: > if some_test_that_fails(variables): > # presumably "variables" include a, b, c, d, e, f > logger.warning(f"BAD THING {a} HAPPENED!") > if not handle_it(a, b, c, d, e, f): > raise MySpecialException(a, b, c, d, e, f) > > But even better would be for handle_it to be more discriminating. > Rather than have it return a flag: > True <-- success > False <-- "an error occurred" > > which you handle by raising a catch-all exception that lists no fewer > than six things that may have caused the error (a through f), I think > that a better design would be for handle_it to directly raise a more > specific exception: > if some_test_that_fails(variables): > # presumably "variables" include a, b, c, d, e, f > logger.warning(f"BAD THING {a} HAPPENED!") > handle_it(a, b, c, d, e, f) > > Exceptions raised by handle_it probably need only refer to one > specific value, not a through f, with a specific exception cause, such > as TypeError, ValueError, etc. > > Instead of needing a whole new class definition, > > wouldn't it be nice to just have something like: > > .... > > # notice there isn't a boilerplate custom class created! > > try: > > if some_test_that_fails(variables): > > #I still have a base exception to fall back on for handlers that don't know > > my > > special exception > > raise Exception.my_special_exception(a, b, c, d, e, f) > > > > How does Exception have a my_special_exception method? What does it > > return? It has to return either an exception class or an exception > instance, but which class? > How does it know that the arguments a, b, .... f (which are anonymous > expressions) should be bound to names "a", "b", "c" etc? > What if I don't want names "a", etc but something more meaningful? If I > call: > raise Exception.apeiron_error(None, x or y, obj.widget.status()) > > what happens? How does Exception know about my apeiron_error method, and > what does it do with the arguments passed? > > except Exception.my_special_excpetion(a:int, b:str, > > d, e, f): > > How does this know to catch only the exception raised by the > my_special_exception method? > My guess is that you are proposing that some form of structural pattern > matching, but precisely what form? I can think of a few ways, but one way is to just think of this as a single named parameter 'subType' to Exception's constructor with some creative code to turn the calls into fancy wrappers around the constructor. Then the exception block is just looking at the exception and the sub type to determine the first handler capable of handling it. Normal function calling conventions apply after that with accompanying exceptions if the function signature doesn't match. You can even define these functions on an exception to make them explicit if you want to and to. The recurring theme to me here though is that functions require definition in two places and are simple: Their def and where they are called. Both sides have to understand the calling arguments and it works well. Exceptions however require definition in three places, the class, the call and the handler and the mechanism to pass arguments is a bit convoluted (either un-named lists or manual parameter assignment). Why not open up the simpler function like mechanism to exception handling? Great discussion. I appreciate the input/responses all! _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-le...@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/6SK5C5RZAYARAVQXJQWBZRQWRQ2BP5A2/ Code of Conduct: http://python.org/psf/codeofconduct/