6
\$\begingroup\$

I want a Python example that illustrates how object.__getattribute__ resolves instance attribute lookups.

I'm looking for feedback about the following code. Is it a "close-enough" approximation of the object.__getattribute__ workflow using Python?

I'm aware that under the hood C is accessing the type/object slots and that the workflow is different (here). The well-written Python docs that mention the object.__getattribute__ workflow leave a lot of ambiguity about how to visualize it in Python. This ambiguity is probably legitimate because we can only talk about the workflow in terms of the C API. But isn't there a way to illustrate the call precedence using __dict__ attributes on the class and instance?

The code below tests the simulation for an instance method lookup during multiple inheritance, the real use case I'm investigating.


Given this mixins.py file:

class Boom(object):
    def log(self):
        print "[ BOOMTOWN ]: %s" % (self.__repr__())

class Basic(object):
    def log(self):
        print self.__class__

class Uno(Basic):
    pass

class Dos(Basic):
    pass

Simulate object.__getattribute__:

from mixins import *

def object_getattribute(instance, klass, attrname, klass_mro=[]):
    '''
    NOTE: the resolution workflow 
    for Class.attrname lookups is different
    '''
    print "[ INSPECTING ]: %s" % klass

    if attrname in klass.__dict__.keys():
        print "yep, in Class.__dict__"
        if ( hasattr( klass.__dict__[attrname], '__get__' ) 
             and hasattr( klass.__dict__[attrname], '__set__' ) ):
            print "yep, DATA descriptor found"
            return klass.__dict__[attrname].__get__( instance, klass )
        else:
            print "nope, not a DATA descriptor"
    else:
        print "nope, not in Class.__dict__"

    if attrname in instance.__dict__.keys():
        print "yep, instance.__dict__"
        return instance.__dict__[attrname]
    else:
        print "nope, not in instance.__dict__"

    if attrname in klass.__dict__.keys():
        print "yep, in Class.__dict__"
        if hasattr( klass.__dict__[attrname], '__get__' ):
            print "yep, NON-DATA descriptor found"
            return klass.__dict__[attrname].__get__( instance, klass )
        else:
            print "return from Class.__dict__[ attrname ]"
            return klass.__dict__[attrname]
    else:
        print "nope, not in Class.__dict__"

    if hasattr( klass, '__getattr__' ):
        print "return from Class.__getattr__( attrname )"
        return klass.__getattr__( attrname )
    else:
        print "nope, no __getattr__ override"

    return object_getattribute( instance, 
    klass_mro.pop(0), attrname, klass_mro=klass_mro )

Use the simulation to inspect how instance methods are looked up in multiple inheritance situations:

class Foo(Uno,Dos): pass
func = object_getattribute( Foo(), Foo, 'log', klass_mro=Foo.mro()[1:] )
func()
print "\n"

class Foo(Boom,Uno,Dos): pass
func = object_getattribute( Foo(), Foo, 'log', klass_mro=Foo.mro()[1:] )
func()
print "\n"

class Foo(Uno,Boom,Dos): pass
func = object_getattribute( Foo(), Foo, 'log', klass_mro=Foo.mro()[1:] )
func()
print "\n"

class Foo(Uno,Dos,Boom): pass
func = object_getattribute( Foo(), Foo, 'log', klass_mro=Foo.mro()[1:] )
func()
print "\n"

The output:

[ INSPECTING ]: <class '__main__.Foo'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Uno'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Dos'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Basic'>
yep, in Class.__dict__
nope, not a DATA descriptor
nope, not in instance.__dict__
yep, in Class.__dict__
yep, NON-DATA descriptor found
<class '__main__.Foo'>


[ INSPECTING ]: <class '__main__.Foo'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Boom'>
yep, in Class.__dict__
nope, not a DATA descriptor
nope, not in instance.__dict__
yep, in Class.__dict__
yep, NON-DATA descriptor found
[ BOOMTOWN ]: <__main__.Foo object at 0x102b0f050>


[ INSPECTING ]: <class '__main__.Foo'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Uno'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Boom'>
yep, in Class.__dict__
nope, not a DATA descriptor
nope, not in instance.__dict__
yep, in Class.__dict__
yep, NON-DATA descriptor found
[ BOOMTOWN ]: <__main__.Foo object at 0x102b06fd0>


[ INSPECTING ]: <class '__main__.Foo'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Uno'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Dos'>
nope, not in Class.__dict__
nope, not in instance.__dict__
nope, not in Class.__dict__
nope, no __getattr__ override
[ INSPECTING ]: <class 'mixins.Basic'>
yep, in Class.__dict__
nope, not a DATA descriptor
nope, not in instance.__dict__
yep, in Class.__dict__
yep, NON-DATA descriptor found
<class '__main__.Foo'>
\$\endgroup\$
  • \$\begingroup\$ Does the code already work like you expect it to, or you asking about possible fixes to the current behaviour? \$\endgroup\$ – ferada Nov 30 '15 at 22:35
0
\$\begingroup\$

Your code doesn't have complete test coverage. Nor does it test __getattr__ at all.

  1. You call __getattr__ without providing the instance.

  2. You call __getattr__ before walking the entire __mro__ to verify if the object exists. This causes any object with only __getattr__ defined on the leaf class to break all, non-instance, attribute lookup.

  3. Due to your recursive approach you return changes to an instances __dict__ even if the attribute is a descriptor.

  4. You pass the incorrect class to descriptors. They expect the type of the instance, not the type of the object the descriptor is defined on.

    My partially fixed version of your code only ever displays Foo. However my version and getattr display Foo, Bar, Spam and Ham.

Your code would be much simpler if you just removed any recursion:

def _getattr(objs, attr):
    SENTINEL = object()
    for obj in objs:
        value = obj.__dict__.get(attr, SENTINEL)
        if value is not SENTINEL:
            return True, value
    return False, None


def getattribute_peilonrayz(instance, attr):
    has_cls, value_cls = _getattr(type(instance).__mro__, attr)
    if has_cls and hasattr(value_cls, '__get__'):
        return value_cls.__get__(instance, type(instance))
    has_inst, value_inst = _getattr([instance], attr)
    if has_inst:
        return value_inst
    if has_cls:
        return value_cls
    has_attr, value_attr = _getattr(type(instance).__mro__, '__getattr__')
    if has_attr:
        return value_attr(instance, attr)
    raise AttributeError("No attribute {}".format(attr))

You can test this along with a modified version of yours with the following.

def disp(value):
    return
    print value


def object_getattribute_1(instance, klass, attrname, klass_mro=[]):
    """This does not fix 3 or 4."""
    disp("[ INSPECTING ]: %s" % klass)
    if attrname in klass.__dict__.keys():
        disp("yep, in Class.__dict__")
        if ( hasattr( klass.__dict__[attrname], '__get__' ) 
             and hasattr( klass.__dict__[attrname], '__set__' ) ):
            disp("yep, DATA descriptor found")
            return True, klass.__dict__[attrname].__get__( instance, klass )
        else:
            disp("nope, not a DATA descriptor")
            pass
    else:
        disp("nope, not in Class.__dict__")
        pass
    if attrname in instance.__dict__.keys():
        disp("yep, instance.__dict__")
        return True, instance.__dict__[attrname]
    else:
        disp("nope, not in instance.__dict__")
        pass
    if attrname in klass.__dict__.keys():
        disp("yep, in Class.__dict__")
        if hasattr( klass.__dict__[attrname], '__get__' ):
            disp("yep, NON-DATA descriptor found")
            return True, klass.__dict__[attrname].__get__(instance, klass)
        else:
            disp("return from Class.__dict__[ attrname ]")
            return True, klass.__dict__[attrname]
    else:
        disp("nope, not in Class.__dict__")
        pass
    try:
        return object_getattribute_1( instance, 
        klass_mro.pop(0), attrname, klass_mro=klass_mro )
    except IndexError:
        return False, None


def object_getattribute_2(instance, klass, attrname, klass_mro=[]):
    if hasattr( klass, '__getattr__' ):
        disp("return from Class.__getattr__( attrname )")
        return klass.__getattr__(instance, attrname)
    else:
        disp("nope, no __getattr__ override")
        pass
    return object_getattribute_2( instance, 
    klass_mro.pop(0), attrname, klass_mro=klass_mro )


def getattribute(instance, attribute):
    cls = type(instance)
    has, value = object_getattribute_1(instance, cls, attribute, list(cls.__mro__))
    if has:
        return value
    return object_getattribute_2(instance, cls, attribute, list(cls.__mro__))


def assert_eq(a, b):
    print('assert {!r} == {!r}'.format(a, b))
    assert a == b


def _tests(f, fn):
    assert_eq('foo', fn(f, 'foo'))
    f.foo = 'foo changed'
    assert_eq('foo changed', fn(f, 'foo'))
    assert_eq('bar get', fn(f, 'bar'))
    # Commented out because of 3
    # f.__dict__['bar'] = 'bar changed'
    # assert_eq('bar get', fn(f, 'bar'))
    f.bar = 'bar changed'
    assert_eq('bar changed set get', fn(f, 'bar'))
    assert_eq('baz __getattr__', fn(f, 'baz'))
    f.baz = 'baz changed'
    assert_eq('baz changed', fn(f, 'baz'))


class Descriptor(object):
    def __init__(self, value):
        self.value = value
        self.values = {}
    def __get__(self, obj, objtype):
        print(obj, objtype)
        return self.values.get(obj, self.value) + ' get'
    def __set__(self, obj, value):
        self.values[obj] = value + ' set'


class Foo(object):
    foo = 'foo'
    bar = Descriptor('bar')
    def __getattr__(self, value):
        return value + ' __getattr__'


class Bar(Foo):
    pass


class Baz(Foo):
    pass


class Spam(Bar, Baz):
    pass


class Ham(Baz, Bar):
    pass


def test_basic(fn):
    _tests(Foo(), fn)


def test_line(fn):
    _tests(Bar(), fn)


def test_diamond(fn):
    _tests(Spam(), fn)
    _tests(Ham(), fn)


test_basic(getattr)
test_line(getattr)
test_diamond(getattr)

test_basic(getattribute_peilonrayz)
test_line(getattribute_peilonrayz)
test_diamond(getattribute_peilonrayz)

test_basic(getattribute)
test_line(getattribute)
test_diamond(getattribute)

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.