On 24/06/2017 11:03, Steven D'Aprano wrote:
On Sat, Jun 24, 2017 at 01:02:55PM +1200, Greg Ewing wrote:
In any case, this doesn't address the issue raised by the OP, which in this example is that if the implementation of bah.__getitem__ calls something else that raises an IndexError, there's no easy way to distinguish that from one raised by bah.__getitem__ itself. I'm not convinced that's a meaningful distinction to make in general. Consider the difference between these two classes:
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
The difference is a mere difference of refactoring. Why should one of them be treated as "bah.__getitem__ raises itself" versus "bah.__getitem__ calls something which raises"? That's just an implementation detail.
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.:
try: foo(baz) except IndexError: ... # assume baz failed (but maybe foo can raise too?)
(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.
In the case of (1), there's nothing Python the language can do to fix that. The solution is to write better code. Question your assumptions. Think carefully about your pre-conditions and post-conditions and invariants. Plan ahead. Read the documentation of foo before assuming it won't raise. In other words, be a better programmer.
If only it were that easy :-(
(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.)
Python libraries rarely give a definitive list of what exceptions functions can raise, so unless you wrote it yourself and know exactly what it can and cannot do, defensive coding suggests that you assume any function call might raise any exception at all:
try: item = baz except IndexError: ... # assume baz failed else: foo(item)
Can we fix that? Well, maybe we should re-consider the rejection of PEP 463 (exception-catching expressions).
https://www.python.org/dev/peps/pep-0463/ I'm all in favour of that :-) but I don't see how it helps in this example:
try: item = (baz except IndexError: SomeSentinelValue) if item == SomeSentinelValue: ... # assume baz failed else: foo(item)
is clunkier than the original version. Or am I missing something? Only if the normal and exceptional cases could be handled the same way would it help:
foo(baz except IndexError: 0)
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?)
In the case of (2), the masking of bugs inside property getters if they happen to raise AttributeError, I think the std lib can help with that. Perhaps a context manager or decorator (or both) that converts one exception to another?
@property @bounce_exception(AttributeError, RuntimeError) def spam(self): ...
Now spam.__get__ cannot raise AttributeError, if it does, it will be converted to RuntimeError. If you need finer control over the code that is guarded use the context manager form:
@property def spam(self): with bounce_exception(AttributeError, RuntimeError):
# guarded if condition: ... # not guarded raise AttributeError('property spam doesn't exist yet')