On Fri, 1 Jan 2016 09:48 am, Chris Angelico wrote: > On Fri, Jan 1, 2016 at 7:18 AM, Ben Finney <ben+pyt...@benfinney.id.au> > wrote: [...] >> As best I can tell, Steven is advocating a way to obscure information >> from the traceback, on the assumption the writer of a library knows that >> I don't want to see it. >> >> Given how very often such decisions make my debugging tasks needlessly >> difficult, I'm not seeing how that's a desirable feature. > > What Steven's actually advocating is removing a difference between > Python code and native code. Compare:
Well, not quite. What I'm really doing is two-fold: (1) reminding people that the part of the code which determines the existence of an error need not be the part of the code which actually calls raise; and (2) suggesting a tiny change to the semantics of raise which would make this idiom easier to use. (Namely, have "raise None" be a no-op.) I'm saddened but not astonished at just how much opposition there is to point (1), even though it is something which Python has been capable of since the earliest 1.x days. Exceptions are first-class objects, and just because raising an exception immediately after a test is a common idiom: if condition: raise SomeError('whatever') doesn't mean it is *always* the best idiom. I have identified a common situation in my own code where I believe that there is a better idiom. From the reaction of others, one might think I've suggested getting rid of exceptions altogether and replacing them with GOTO :-) Let's step back a bit and consider what we might do if Python were a less capable language. We might be *forced* to perform error handling via status codes, passed from function to function as needed, until we reach the top level of code and either print the error code or the program's intended output. None of us want that, but maybe there are cases where a less extreme version of the same thing is useful. Just because I detect an error condition in one function doesn't necessarily mean I want to trigger an exception at that point. Sometimes it is useful to delay raising the exception. Suppose I write a validation function that returns a status code, perhaps an int, or a Enum: def _validate(arg): if condition(arg): return CONDITION_ERROR elif other_condition(arg): return OTHER_ERROR return SUCCESS def func(x): status = _validate(x) if status == CONDITION_ERROR: raise ConditionError("condition failed") elif status == OTHER_ERROR: raise OtherError("other condition failed") assert status == SUCCESS ... Don't worry about *why* I want to do this, I have my reasons. Maybe I want to queue up a whole bunch of exceptions before doing something with them, or conditionally decide whether or not raise, like unittest. Perhaps I can do different sorts of processing of different status codes, including recovery from some: def func(x): status = _validate(x) if status == CONDITION_ERROR: warnings.warn(msg) x = massage(x) status = SUCCESS elif status == OTHER_ERROR: raise SomeError("an error occurred") assert status == SUCCESS do_the_real_work(x) There's no reason to say that I *must* raise an exception the instant I see a problem. But why am I returning a status code? This is Python, not C or bash, and I have a much more powerful and richer set of values to work with. I can return an error type, and an error message: def _validate(arg): if condition(arg): return (ConditionError, "condition failed") elif other_condition(arg): return (OtherError, "something broke") return None But if we've come this far, why mess about with tuples when we have an object oriented language with first class exception objects? def _validate(arg): if condition(arg): return ConditionError("condition failed") elif other_condition(arg): return OtherError("something broke") return None The caller func still decides what to do with the status code, and can process it when needed. If the error is unrecoverable, it can raise. In that case, the source of the exception is func, not _validate. Just look at the source code, it tells you right there where the exception comes from: def func(x): exc = _validate(x) if exc is not None: raise exc # <<<< this is the line you see in the traceback do_the_real_work(x) This isn't "hiding information", but it might be *information hiding*, and it is certainly no worse than this: def spam(): if condition: some_long_message = "something ...".format( many, extra, arguments) exc = SomeError(some_long_message, data) raise exc # <<<< this is the line you see in the traceback If I have a lot of functions that use the same exception, I can refactor them so that building the exception object occurs elsewhere: def spam(): if condition: exc = _build_exception() raise exc # <<<< this is STILL the line you see in the traceback and likewise for actually checking the condition: def spam(): exc = _validate_and_build_exception() if exc is not None: raise exc # <<<<<<<<<<<< Fundamentally, _validate is an implementation detail. The semantics of func will remain unchanged whether it does error checking inside itself, or passes it off to another helper function. The very existence of the helper function is *irrelevant*. We have true separation of concerns: (1) _validate decides whether some condition (nominally an error condition) applies or not; (2) while the caller func decides whether it can recover from that error or needs to raise. (Aside: remember in my use-case I'm not talking about a single caller func. There might be dozens of them.) If func decides it needs to raise, the fact that _validate made the decision that the condition applies is irrelevant. The only time it is useful to see _validate in the traceback is if _validate fails and raises an exception itself. -- Steven -- https://mail.python.org/mailman/listinfo/python-list