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?

1

There are 1 best solutions below

0
jsbueno On

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-tutorial you will see that fetching the name in the class, to check if it is a descriptor, does not use type(obj).__getattribute__ - instead, they wrote a helper function find_name_in_mro which 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 to object.__getattribute__, and what it does is:

  1. try to retrieve the object in the class namespace or in any super-class. This search does not use __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".
    1. If the attribute is found in the class, it is saved for later, and immediatelly checked if it is a "data descriptor": i.e., an object which features either __set__ or __del__ in its own class. This lookup is non-recursive and checks directly the (retrieved object's) class slots - 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.
    2. If there is a data-descriptor present, __get__ is called on the descriptor instance, and its return value is used as the result of __getattribute__
  2. If the value is not on the class-namespace or super-classes, then Python searches the class's __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.
  3. If there is an attribute in the instance __dict__ that is returned
  4. Otherwise, if there is a non-data-descriptor present, __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).
  5. As a last resource, __getattr__ in the same instance is called - if defined - its return value, or AttributeError exception are used as result of __getattribute__
  6. AttributeError is raised.