Home > Software engineering >  Why is Python data-descriptor not taking precedence over normal class variable?
Why is Python data-descriptor not taking precedence over normal class variable?

Time:09-28

In the following example:

class Desc:
    def __get__(self, object, type=None):
        print('Descriptor Desc read-mode activated.')
        return object._essence

    def __set__(self, object, value):
        print('Descriptor Desc write-mode activated.')
        object._essence = value


class C:
    essence = Desc()


class D(C):
    #essence = 'MAGNESIUM'

    def __init__(s):
        s.essence = 'STEAM'
        print('Class D object initialized.\n')


o = D()

print(o.__dict__)
print(o.essence)
o.essence = 'ALUMINUM'
print(o.essence)

The output is as follows (Sublime Text):

Descriptor Desc write-mode activated.    
Class D object initialized.

{'_essence': 'STEAM'}
Descriptor Desc read-mode activated.
STEAM
Descriptor Desc write-mode activated.
Descriptor Desc read-mode activated.
ALUMINUM
[Finished in 176ms]

, which is as I expect. But when the class variable in class D is enabled (by un-hashing that line), the descriptor is no longer reached and the class variable is looked-up instead. Then the output becomes:

Class D object initialized.

{'essence': 'STEAM'}
STEAM
ALUMINUM
[Finished in 178ms]

Shouldn't the data-descriptor in class C take precedence over all other, even if it's higher up the inheritance chain?

CodePudding user response:

Shouldn't the data-descriptor in class C take precedence over all other, even if it's higher up the inheritance chain?

No, since

class D(C):
   essence = 'MAGNESIUM'

creates a name D.essence (on the class, not on instances), and since that name is a thing, the superclass chain is not followed to find C.essence (which is a descriptor).

CodePudding user response:

You ask:

Shouldn't the data-descriptor in class C take precedence over all other, even if it's higher up the inheritance chain?

And maybe that's because you read the documentation and found:

Data descriptors always override instance dictionaries.

And also:

Instance lookup scans through a chain of namespaces giving data descriptors the highest priority, followed by instance variables, then non-data descriptors, then class variables, and lastly getattr() if it is provided.

However, your definition of D:

class D(C):
   essence = 'MAGNESIUM'

Overrides the definition of the class attribute defined on super-class C as essence = Desc() with essence as a simple class attribute with a string value. It no longer is a descriptor and assigning or reading won't reach your code.

Consider:

class Desc:
    def __get__(self, object, type=None):
        print('Descriptor Desc read-mode activated.')
        return object._essence

    def __set__(self, object, value):
        print('Descriptor Desc write-mode activated.')
        object._essence = value


class C:
    essence = Desc()


class D(C):
    # essence = 'MAGNESIUM'

    def __init__(s):
        s.essence = 'STEAM'
        print('Class D object initialized.\n')


d = D()
# show all the classes in the method resolution order that have `essence`
essence_found = [c for c in type(d).__mro__ if 'essence' in c.__dict__]
print([c.__name__ for c in essence_found])
# is `.essence` on the first one that was found a descriptor?
print('__get__' in type(essence_found[0].__dict__['essence']).__dict__)


class D(C):
    essence = 'MAGNESIUM'

    def __init__(s):
        s.essence = 'STEAM'
        print('Class D object initialized.\n')


d = D()
essence_found = [c for c in type(d).__mro__ if 'essence' in c.__dict__]
print([c.__name__ for c in essence_found])
print('__get__' in type(essence_found[0].__dict__['essence']).__dict__)
# here the second one is, but it doesn't matter:
print('__get__' in type(essence_found[1].__dict__['essence']).__dict__)

Output:

Descriptor Desc write-mode activated.
Class D object initialized.

['C']
True
Class D object initialized.

['D', 'C']
False
True

For the second definition of D, with the redefined class attributes, the method resolution order shows essence on D, so there's no need to continue to C. But the essence on that class is not a descriptor.

A descriptor is given the highest priority if it is found, but if the first class in the chain of namespaces has the required attribute. It is not a descriptor, but that doesn't matter. It doesn't say it will search all the way through the chain of namespaces for anything that defines the attribute as a descriptor, and then repeat the search for the next best thing. It says it will find it in the chain of namespaces, and prefer a descriptor over anything else on the first one it finds.

Edit: I ended up rewriting the test that shows what is going on, but am not sure whether it's clearer or not:

d = D()
# show all the classes in the method resolution order that have `essence`
# is `.essence` on the first one that was found a descriptor?
# for the second one it is, but it doesn't matter:
print([
    (c.__name__,  '__get__' in type(c.__dict__['essence']).__dict__)
    for c in type(d).__mro__ if 'essence' in c.__dict__
])

Output:

Descriptor Desc write-mode activated.
Class D object initialized.

[('C', True)]
Class D object initialized.

[('D', False), ('C', True)]
  • Related