You are partially correct, let's start by what you got wrong.
Should I think of inheritance as simply creating pointers to the base class objects
That would be incorrect. When you access an attribute, either with getattr(obj, attr) or by doing obj.attr, Python first recovers the attribute through the method resolution order. In particular this means that this does not happen at class creation, but at attribute lookup instead.
It looks as if Python creates an attribute foo which is bound to A.foo
Here you are correct. Binding a class or instance method created a new object in the process.
After having recovered the attribute, Python may need to bind it. It first checks if it is a descriptor, that is an object with a __get__ method to allow binding it to a certain instance.
Here is a Python implementation of the getattr to help visualize what happens when you retrieve an attribute.
First this is how Python resolves a class attribute using the mro.
def _resolve_cls_attr(cls, attr):
if attr in cls.__dict__:
return cls.__dict__[attr]
elif len(cls.__mro__) > 1:
return _resolve_cls_attr(cls.__mro__[1], attr)
else:
raise AttributeError('class has no attribute {}'.format(attr))
Then, this is how getattr would be implemented if it were in Python.
def getattr(instance, attr):
cls = instance if isinstance(instance, type) else type(instance)
# Recover instance attribute
if hasattr(instance, '__dict__') and attr in instance.__dict__:
attr_value = instance.__dict__[attr]
# Recover the attribute from the class
else:
attr_value = _resolve_cls_attr(cls, attr)
# We recovered a simple class attribute
if not hasattr(attr_value, '__get__'):
return attr_value
# We recovered an instance method from a class or a staticmethod descriptor
if instance is cls or isinstance(attr_value, staticmethod):
return attr_value.__get__(None, cls)
# We recovered an instance method or a custom descriptor
return attr_value.__get__(instance, cls)
Keep in mind that the above omits a few steps for the sake of sticking to your question. By example, it will not rely on __getattr__ and __getattribute__ as the builtin getattr would.
b.foo, Python will first look in the__dict__ofbwhere it won’t find afoo. Then it will check themro(), i.e. all the types in the type hierachy in order, to check where it could take afoofrom. In your case, it would first checkB.fooand thenA.foo.B(A),fooisn't added toB's namespace, rather__dict__contains all the attributes that were created specifically withinBand not inherited from elsewhere, yeh?__dict__will only contain instance members, i.e. things where you didself.something = value. Methods will usually live on the class, not the instance.fooisn't added toB.__dict__when you doB(A), rather when you callB.foo, theB.__dict__is checked and then further up the MRO since it wasn't found?