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'>
-
\$\begingroup\$ Does the code already work like you expect it to, or you asking about possible fixes to the current behaviour? \$\endgroup\$ferada– ferada2015年11月30日 22:35:13 +00:00Commented Nov 30, 2015 at 22:35
1 Answer 1
Your code doesn't have complete test coverage. Nor does it test __getattr__
at all.
You call
__getattr__
without providing the instance.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.Due to your recursive approach you return changes to an instances
__dict__
even if the attribute is a descriptor.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 andgetattr
displayFoo
,Bar
,Spam
andHam
.
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)
Explore related questions
See similar questions with these tags.