[Python-ideas] the error that raises an AttributeError should be passed to __getattr__

Jason Maldonis jjmaldonis at gmail.com
Mon Jun 19 23:26:21 EDT 2017


First, I apologize for the poor post. Your corrections were exactly
correct: This is only relevant in the context of properties/descriptors,
and the property swallows the error message and it isn't printed to
screen.  I should not be typing without testing.

> So... what precisely should be passed to __getattr__, and what exactly are
you going to do with it?
I'd say the error instance that caused __getattr__ to be raised should be
passed into __getattr__, but I don't have a strong opinion on that. I'll
assume that's true for the rest of this post, however.

To clarify my mistakes in my first post, your example illustrates what I
wanted to show:
class A(object):
    eggs = "text"
    def __getattr__(self, name):
        if name == 'cheese':
            return "cheddar"
        raise AttributeError('%s missing' % name)
    @property
    def spam(self):
        return self.eggs.uper() # Oops.
a = A()
a.spam

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __getattr__
AttributeError: spam missing

This swallows the AttributeError from `eggs.uper()` and it isn't available.

Even if it were available, I see your point that it may not be especially
useful. In one iteration of my code I was looking through the stack trace
using the traceback module to find the error I wanted, but I quickly
decided that was a bad idea because I couldn't reliably find the error.
However, with the full error, it would (I think) be trivial to find the
relevant error in the stack trace. With the full stack trace, I would hope
that you could properly do any error handling you wanted.

However, if the error was available in __getattr__, we could at least
`raise from` so that the error isn't completely swallowed. I.e. your
example would be slightly modified like this:
class A(object):
    eggs = "text"
    def __getattr__(self, name, error):
        if name == 'cheese':
            return "cheddar"
        raise AttributeError('%s missing' % name) from error
...

which I think is useful.


> That's a fairly long and heavy process, and will be quite annoying to
> those writing cross-version code using __getattr__, but it can be done.
This is another thing I hadn't considered, and I have no problem saying
this non-backwards-compatible-change just isn't worth it. Maybe if there
are multiple small updates to error handling it could be worth it (which I
believe I read was something the devs care quite a bit about atm), but I
don't think that this change is a huge deal.


> One good solution to this is a "guard point" around your property
functions.
I am using a very similar decorator (modified from the one you gave me a
month or so ago) in my code now, and functionally it works great.  There is
something that feels a bit off / like a hack to me though, and maybe it's
that I am simply "renaming" the error from one I can't handle (due to name
conflicts) to one I can. But the modification is just name change -- if I
can handle the RuntimeError correctly, I feel like I should have just been
able to handle the original AttributeError correctly (because in practice
they should be raising an error in response to the exact same problem).
That said, your decorator works great and gives me the functionality I
needed.


> > It's a feature.  It's why Enum classes can have members named 'value'
and
> > 'name'.
> Can you explain further? What's special about value and name?
I'm also very curious about this. I've read the Enum code a few times but
it hasn't clicked yet.


> > Perhaps a better approach is to prevent descriptors from leaking
> > AttributeError in the first place? Change the protocol so that if
> > descriptor.__get__ raises AttributeError, it is caught and re-raised as
> > RuntimeError, similar to StopIteration and generators.
> This can't be done globally, because that's how a descriptor can be
> made conditional (it raises AttributeError to say "this attribute does
> not, in fact, exist"). But it's easy enough - and safe enough - to do
> it just for your own module, where you know in advance that any
> AttributeError is a leak.
I have a pretty large codebase with a few different functionalities, so I'm
hesitant to say "any AttributeError is a leak" in an object that might
affect / be affected by other packages/functionalities.  One other thing I
really like about the `noleak` decorator is that I can change the
AttributeError to a custom MyCustomError, which allows me to handle
MyCustomError precisely where it should be handled.  Also, maybe I'm just
not fully understanding the locality of re-raising another error from
descriptor.__get__ because I haven't completely wrapped my head around it
yet.

On Mon, Jun 19, 2017 at 10:10 PM, Chris Angelico <rosuav at gmail.com> wrote:

> On Tue, Jun 20, 2017 at 12:26 PM, Steven D'Aprano <steve at pearwood.info>
> wrote:
> > On Tue, Jun 20, 2017 at 11:31:34AM +1000, Chris Angelico wrote:
> >
> >> Why not just write cross-version-compatible code as
> >>
> >> def __getattr__(self, name, error=None):
> >>
> >> ? Is there something special about getattr?
> >
> > You've still got to write it in the first place. That's a pain,
> > especially since (1) it doesn't do you any good before 3.7 if not later,
> > and (2) even if this error parameter is useful (which is yet to be
> > established), it's a pretty specialised use. Most of the time, you
> > already know the name that failed (its the one being looked up).
>
> Gotcha, yep. I was just confused by your two-parter that made it look
> like it would be hard (or impossible) to write code that would work on
> both 3.6 and the new protocol.
>
> > Perhaps a better approach is to prevent descriptors from leaking
> > AttributeError in the first place? Change the protocol so that if
> > descriptor.__get__ raises AttributeError, it is caught and re-raised as
> > RuntimeError, similar to StopIteration and generators.
>
> This can't be done globally, because that's how a descriptor can be
> made conditional (it raises AttributeError to say "this attribute does
> not, in fact, exist"). But it's easy enough - and safe enough - to do
> it just for your own module, where you know in advance that any
> AttributeError is a leak. The way generators and StopIteration
> interact was more easily fixed, because generators have two legitimate
> ways to emit data (yield and return), but there's no easy way for a
> magic method to say "I don't have anything to return" other than an
> exception.
>
> Well, that's not strictly true. In JavaScript, they don't raise
> StopIteration from iterators - they always return a pair of values
> ("done" and the actual value, where "done" is either false for a yield
> or true for a StopIteration). That complicates the normal case but it
> does make the unusual case a bit easier. Also, it's utterly and
> fundamentally incompatible with the current system, so it'd have to be
> a brand new competing protocol.
>
> ChrisA
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20170619/cf13de82/attachment-0001.html>


More information about the Python-ideas mailing list