Steven D'Aprano wrote:
class X: def __getitem__(self, n): if n < 0: n += len(self) if not 0 <= n < len(self): raise IndexError ...
class Y: def __getitem__(self, n): self._validate(n) ... def _validate(self, n): if n < 0: n += len(self) if not 0 <= n < len(self): raise IndexError
Why should one of them be treated as "bah.__getitem__ raises itself" versus "bah.__getitem__ calls something which raises"?
They shouldn't be treated differently -- they're both legitimate ways for __getitem__ to signal that the item doesn't exist.
What *should* be treated differently is if an IndexError occurs incidentally from something else that __getitem__ does. In other words, Y's __getitem__ should be written something like
def __getitem__(self, n): self.validate(n) # If we get an IndexError from here on, it's a bug try: # work out the result and return it except IndexError as e: raise RuntimeError from e
I think we're over-generalizing this problem. There's two actual issues here, and we shouldn't conflate them as the same problem:
(1) People write buggy code based on invalid assumptions of what can and can't raise. E.g.:
(2) There's a *specific* problem with property where a bug in your getter or setter that raises AttributeError will be masked, appearing as if the property itself doesn't exist.
Case 1 can usually be handled by rewriting the code so as to make the scope of exception catching as narrow as possible.
Case 2 needs to be addressed within the method concerned on a case-by-case basis. If there's a general principle there, it's something like this: If you're writing a method that uses an exception as part of it's protocol, you should catch any incidental occurrences of the same exception and reraise it as a different exception.
I don't think there's anything more the Python language could do to help with either of those.
(Aside: I've been thinking for a long time that design by contract is a very useful feature to have. It should be possibly to set a contract that states that this function won't raise a certain exception, and if it does, treat it as a runtime error. But this is still at a very early point in my thinking.)
That sounds dangerously similar to Java's checked exceptions, which has turned out to be a huge nuisance and not very helpful.
Maybe we need a better way to assert that a certain function won't raise a particular exception:
try: item = bah without IndexError: foo(item) except IndexError: ... # assume baz failed
(But how is that different from try...except...else?)
It's no different, if I understand what it's supposed to mean correctly.
@property @bounce_exception(AttributeError, RuntimeError) def spam(self): ...
In the case of property getters, it seems to me you're almost always going to want that functionality, so maybe it should be incorporated into the property decorator itself.
The only cases where you wouldn't want it would be if your property dynamically figures out whether it exists or not, and in those rare cases you would just have to write your own descriptor class.