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[5])
    except IndexError:
        ... # assume baz[5] 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[5]
    except IndexError:
        ... # assume baz[5] 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/


Maybe we need a better way to assert that a certain function won't raise 
a particular exception:

    try:
        item = bah[5]
        without IndexError:
            foo(item)
    except IndexError:
        ... # assume baz[5] 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')



-- 
Steve
_______________________________________________
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to