1

Consider a case in Python where one uses getattr to either dynamically fetch a method or a value.

def message(obj, msg, *args, **kwargs):
    result = getattr(obj, msg, None)
    if result is not None:
        try:
            return result(*args, **kwargs)
        except TypeError:
            return result

...only, hang on -- that's not really very good behavior. Even if this is a bad getattr call, None is implicitly returned anyway -- that's not necessarily great behavior for this kind of function.

In the interest of determining a solid "believe me, ya don't want to return this" value in the face of no good and decent sentinels (that I knew of anyway), I considered setting the default for getattr to a function that raises an exception. In this way a bad search should always be obvious and caught, unless the 'other guy' decides to be cute and make this useless sentinel an attribute.

class _BadMessageException(Exception):
    pass

def _did_not_find(*args, **kwargs):
    raise BadMessageException

def _raise_right_exception(msg, obj):
    if not isinstance(msg, basestring):
        raise TypeError("Message '{}' was not a string".format(msg))
    else:
        raise AttributeError("Bad message '{}' sent to object '{}'".format(msg, obj))

In this way, the message is always at least on the up-and-up when it returns None, because it found a None where you asked it to look. It also then raises the exception you'd expect: AttributeError for an object with no such method/ivar, TypeError for too many args passed, etc etc. EDIT: Naturally, I post the wrong code snippet the first time around. Here's the corrected function.

def message(obj, msg, *args, **kwargs):
    result = getattr(obj, msg, _did_not_find)
    try:
        return result(*args, **kwargs)
    except TypeError:
        if not args or kwargs:
            return result
        else:
            _raise_right_exception(msg, obj)
    except _BadMessageException:
        _raise_right_exception(msg, obj)

It feels like a lot of extra code just to make sure this fails in the right way. A function that raises an exception which is just a McGuffin to raise the preferred exception, just to appease the eafp demigods... Hrrm.

Is there a simpler syntax for declaring a valid 'fail' sentinel, either in this or other situations where the return value is not known or guaranteed?

1 Answers1

2

Just don't give getattr() a default at all; have it raise AttributeError or even TypeError for a bad msg value, instead:

>>> getattr(object(), 'foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'foo'
>>> getattr(object(), 42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: getattr(): attribute name must be string

which makes your method simply:

def message(obj, msg, *args, **kwargs):
    result = getattr(obj, msg)
    try:
        return result(*args, **kwargs)
    except TypeError:
        return result

or, for your updated version, much the same with a re-raise if you had arguments passed in:

def message(obj, msg, *args, **kwargs):
    result = getattr(obj, msg)
    try:
        return result(*args, **kwargs)
    except TypeError:
        if not args and not kwargs:
            # assume attribute access was desired
            return result
        raise
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Well, if `Stick.foo()` takes no arguments, this just returns `` if I goof up and pass too many args. That's a behavior I'd like to avoid. **EDIT:** The goal is to rip off Obj-C's message passing, only in this way I can also just get attributes back if they aren't callable, without explicitly testing for callable (since the nature of the error kind of does that for you, heh). So the message itself should fail on its own merit -- at that point, if the actual *method call* fails, it'll be because the method was poorly called and can throw its own errors –  May 19 '14 at 19:33
  • @Stick: But your code didn't prevent that *either*. That has **nothing** to do with `getattr()`. Use `callable()` then to test if `result` is callable instead of catching `TypeError`. – Martijn Pieters May 19 '14 at 19:35
  • @Stick: that's orthogonal to how you access the attribute in the first place. – Martijn Pieters May 19 '14 at 19:36
  • I must not have provided the right method as source - I did have it all worked out, and it involved using the method as sentinel –  May 19 '14 at 19:37
  • @Stick: You cannot use the method as a sentinel to test if it is callable or not and distinguish it from throwing a `TypeError` directly or as a result of using an incorrect number of arguments. – Martijn Pieters May 19 '14 at 19:39
  • Updated with the more recent function. This should perform correctly; try to call the method, if it bails, and there were no args or kwargs, assume it is tantamount to a 'get' call and return the value, otherwise raise the error. Ultimately I think confusing the issue with attribute returning is going to cause me to just change my expectations on what this 'should' do, though –  May 19 '14 at 19:51
  • That *still* has nothing to do with `getattr()` and a sentinel; you are talking about a different problem altogether. – Martijn Pieters May 19 '14 at 20:10
  • I don't understand what part of this I'm not grasping. O_o What am I not getting? Are you suggesting it's pedantic to try and make sure there's a difference between using this function to access attributes/call methods, as opposed to doing it directly? So a sentinel is always just wrong here, whether I want to report errors or ensure good getattr calls or what –  May 19 '14 at 20:32
  • A sentinel is no use here because you *always* raise in that case. Your version just does more work to raise the same exceptions as the no-default case would. – Martijn Pieters May 19 '14 at 20:37