0

When defining a descriptor class, you can simply raise an AttributeError to signal that the attribute isn't available. For example:

from typing import Any, TypeVar, MutableMapping

V = TypeVar("V")

class Desc:
    def __init__(self, key: str) -> None:
        self.key = key
    def __get__(self, instance: Any, owner: type) -> V:
        try:
            return instance._dict[self.key]
        except KeyError:
            raise AttributeError()

class C:
    foo = Desc("d")
    def __init__(self, d: MutableMapping[str, V]) -> None:
        self._dict = d

Use like:

>>> d1 = dict(d=0, bar=0, baz=0)
>>> c = C(d1)
>>> c.foo 
0
>>> d1.update(d=1)
>>> c.foo
1
>>> hasattr(C(dict()), "foo")
False

Note that the AttributeError causes the hasattr function to "fail silently". This is as expected and described by the descriptor protocol.

However, in some cases you may want the AttributeError to "bubble up" to the top. For example:

class NewDesc:
    def __get__(self, instance, owner):
        do_complex_task(instance) # <-- AttributeError occurs here
        return instance._on_the_fly_private_attr

I have found that this is easily handled by wrapping the offending line of code in a try-except block that raises some other error different than AttributeError:

try:
    do_complex_task(instance) # <-- AttributeError occurs here
except AttributeError as e:
    raise MyError() from e

Is this the canonical way of handling this problem? What pitfalls am I leaving myself open to by doing it this way?

Rick
  • 43,029
  • 15
  • 76
  • 119
  • The error does bubble up - its just that `hasattr` catches it and returns `False`. So, this is question is just about raising alternate errors in the `hasattr` case? – tdelaney Jun 20 '18 at 15:01
  • @tdelaney it's not quite that simple- i've observed that other libraries use an if/else `hasattr` block instead of a try/except block pretty regularly. see for example the `geometry.asShape` method [in the `shapely` library](https://github.com/Toblerity/Shapely/blob/master/shapely/geometry/geo.py). – Rick Jun 20 '18 at 16:00
  • @tdelaney though, in that case, it really wouldn't make much of a meaningful difference to change to try/except. anyway that is the piece of code which bit me this morning and had me wondering about this (silent failure occurring in my `__geo_interface__` getter). – Rick Jun 20 '18 at 16:03
  • Its an interesting question. Since programs expect `AttributeError` and `hasattr` to be the same thing, I think you should honor that. Only raise a different exception for a truly exceptional condition. If the object is sane without the attribute, use `AttributeErrror` - if its time to pack up and go home, raise something else. – tdelaney Jun 20 '18 at 16:50
  • @tdelaney makes sense. i guess it wasn't a great SO question, but i was wondering if there was an "accepted way" floating around out there. – Rick Jun 20 '18 at 18:22
  • You use a descriptor because you want `foo` to look like an attribute. I'd say that the most canonical thing to do is to make it look like an attribute as much as possible. Code using `hasattr` generally isn't going to handle some other raised exception very gracefully and there is a good chance it will break if it receives an unexpected exception. It would be risky to raise something unless you really want to go down an error path that cleans things up. – tdelaney Jun 20 '18 at 20:57
  • @tdelaney yes that's my thought as well: only raise something else if you WANT it to stop. it would be nice if there were a way to raise an attribute error that signaled to the descriptor machinery "something bad happened" though, and differentiate it from a "regular" attribute error. not sure how one would make that work in the lanuage. – Rick Jun 20 '18 at 21:32
  • @tdelaney seems i am not the first person to recognize that this really is a problem: https://stackoverflow.com/a/1063055/2437514 – Rick Jun 21 '18 at 00:34

0 Answers0