I've been intensively consulting the Python docs for descriptors, and I can't wrap my head around some points in it regarding descriptor invocation and the given Python equivalent of object.__getattribute__().
The pure Python equivalent given (see "object_getattribute" in the code below) supposedlly calls type's version of __getattribute__() by calling getattr() with the class object supplied as first argument, which results in a call to type's __getattribute__(). The problem is, having type's attribute lookup (implememted in type.__getattribute__()) involved in the lookup for instance invocation should mess up things as it would bridge into a different line of relation in which classes are objects and metaclasses are the classes. The lookup search carried by object.__getattribute__() should terminate when the MRO of the class is exhausted (at which point getattr() would then be utilized), but calling __getattribute__() of type would illogically cascade search up to cover the metaclass's dict and possibly get back a similarly-named attribute, which is wrong as metaclasses define classes, not objects.
Here is an example creating a metaclass named "mytype", a base class called "base" that uses the Python equivalent provided at: https://docs.python.org/3/howto/descriptor.html#technical-tutorial, and an instance class, "myclass", from "mytype":
class mytype(type):
cls_var = 11
class base:
def object_getattribute(obj, name):
null = object()
objtype = type(obj)
cls_var = getattr(objtype, name, null)
descr_get = getattr(type(cls_var), '__get__', null)
if descr_get is not null:
if (hasattr(type(cls_var), '__set__') or hasattr(type(cls_var), '__delete__')):
return descr_get(cls_var, obj, objtype)
if hasattr(obj, '__dict__') and name in vars(obj):
return vars(obj [name]
if descr_get is not null:
return descr_get(cls_var, obj, objtype)
if cls_var is not null:
return cls_var
raise AttributeError(name)
def __init__(s, v):
s.core = v
@property
def core(s):
return '-- ' + s._core + ' --'
@core.setter
def core(s, v):
s._core = v
myclass = mytype('myclass', (base, ), {'a':97})
o = myclass('IRON')
(Note: I don't know how to add a method definition to a class when creating it throw calling type's new(), so I came up with class "base" as means to provide the "object_getattribute()" method from the docs)
Now, if I run:
print(o.__getattribute__('cls_var'))
Or, more simply:
print(o.cls_var
I get:
...
AttributeError: 'myclass' object has no attribute 'cls_var'
[Program finished]
The result is what I would normally expect. The search doesn't look in the metaclass for evaluating names - that's different territory. However, if I use the pure Python equivalent from the docs as follows:
print(o.object_getattribute('cls_var'))
Then I get:
11
Which is on mytype.
Summary: Is the pure-Python version provided in the docs, wrong?
The example that used to be in the docs was incorrect.
If you look there now -
https://docs.python.org/3/howto/descriptor.html#technical-tutorialyou will see that fetching the name in the class, to check if it is a descriptor, does not usetype(obj).__getattribute__- instead, they wrote a helper functionfind_name_in_mrowhich accurately emulates the mechanism of retrieving an attribute.When one opens the same URL for an older Python version, say, https://docs.python.org/3.9/howto/descriptor.html#technical-tutorial, you will notice the same code as is pasted in the question - which is, in fact, incorrect.
So, putting it yet in other words so there is a chance it makes things clearer for some eventual reader: When retrieving an attribute in any instance in Python, that instance's
__getattribute__is called. Usually that will default toobject.__getattribute__, and what it does is:__getattribute__on the class - it is a custom search, that will search for the attribute in the class'__dict__, and then in the__dict__of all its linearized superclasses (the mro - "method resolution order") up to "object".__set__or__del__in its own class. This lookup is non-recursive and checks directly the (retrieved object's) classslots- there is this difference for "non data descriptors" because -descriptors featuring only__get__, as these can be shadowed by values set in the instance. Data descritors are never shadowed by values in the instance.__get__is called on the descriptor instance, and its return value is used as the result of__getattribute____dict__itself. Note that attributes in user defined classes can exist in classes whose instances do not have a__dict__, if__slots__is used in the class definition. However, attribute access in__slots__itself is performed via a specialized descriptor - so, Python will will use the case 1.1 above for those.__dict__that is returned__get__is called in the descriptor object.This is the case used to retrieve functions in the class which are wrapped into bound-methods (the.__get__()method in functions do that).__getattr__in the same instance is called - if defined - its return value, orAttributeErrorexception are used as result of__getattribute__AttributeErroris raised.