On Wed, Feb 20, 2019 at 9:09 PM Ben Rudiak-Gould <benrud...@gmail.com> wrote:
> That problem, of an inadvertently leaked implementation detail
> masquerading as a proper alternate return value, used to be a huge
> issue with StopIteration, causing bugs that were very hard to track
> down, until PEP 479 fixed it by translating StopIteration into
> RuntimeError when it crossed an abstraction boundary.

That's because a generator function conceptually has three ways to
provide data (yield, return, and raise), but mechanically, one of them
is implemented over the other ("return" is "raise StopIteration with a
value"). For other raised exceptions, this isn't a problem.

> I think converting exceptions to RuntimeErrors (keeping all original
> information, but bypassing catch blocks intended for specific
> exceptions) is the best option. (Well, second best after ML/Haskell.)
> But to make it work you probably need to support some sort of
> exception specification.

The trouble with that is that it makes refactoring very hard. You
can't create a helper function without knowing exactly what it might
be raising.

> I'm rambling. I suppose my points are:
>
> * Error handing is inherently hard, and all approaches do badly
> because it's hard and programmers hate it.

Well, yeah, no kidding. :)

> ... I was bitten several times by
> that StopIteration problem.
>

There's often a completely different approach that doesn't leak
StopIteration. One of my workmates started seeing RuntimeErrors, and
figured he'd need a try/except in this code:

   def _generator(self, left, right):
        while True:
            yield self.operator(next(left), next(right))

But instead of messing with try/except, it's much simpler to use
something else - in this case, zip.

Generally, it's better to keep things simple rather than to complicate
them with new boundaries. Unless there's a good reason to prevent
leakage, I would just let exception handling do whatever it wants to.
But if you want to specifically say "this function will not raise
anything other than these specific exceptions", that can be done with
a decorator:

def raises(*exc):
    """Declare and enforce what a function will raise

    The decorated function will not raise any Exception other than
    the specified ones, or RuntimeError.
    """
    def deco(func):
        @functools.wraps(func)
        def convert_exc(*a, **kw):
            try: return func(*a, **kw)
            except exc: raise
            except Exception as e: raise RuntimeError from e
        convert_exc.may_raise = exc # in case it's wanted
        return convert_exc
    return deco

This way, undecorated functions behave as normal, so refactoring isn't
impacted. But if you want the help of a declared list of exceptions,
you can have it.

@raises(ValueError, IndexError)
def frob(x):
    ...

ChrisA
_______________________________________________
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