7
\$\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'>
asked Jun 2, 2015 at 19:53
\$\endgroup\$
1
  • \$\begingroup\$ Does the code already work like you expect it to, or you asking about possible fixes to the current behaviour? \$\endgroup\$ Commented Nov 30, 2015 at 22:35

1 Answer 1

1
\$\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)
answered Aug 1, 2020 at 16:57
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.