Writing a Non-Data Descriptor
Asked Answered
G

1

6

I am learning about descriptors in python. I want to write a non-data descriptor but the class having the descriptor as its classmethod doesn't call the __get__ special method when I call the classmethod. This is my example (without the __set__):

class D(object):

    "The Descriptor"

    def __init__(self, x = 1395):
        self.x = x

    def __get__(self, instance, owner):
        print "getting", self.x
        return self.x


class C(object):

    d = D()

    def __init__(self, d):
        self.d = d

And here is how I call it:

>>> c = C(4)
>>> c.d
4

The __get__ of the descriptor class gets no call. But when I also set a __set__ the descriptor seems to get activated:

class D(object):

"The Descriptor"

    def __init__(self, x = 1395):
        self.x = x

    def __get__(self, instance, owner):
        print "getting", self.x
        return self.x

    def __set__(self, instance, value):
        print "setting", self.x
        self.x = value

class C(object):

    d = D()

    def __init__(self, d):
        self.d = d

Now I create a C instance:

>>> c=C(4)
setting 1395
>>> c.d
getting 4
4

and both of __get__, __set__ are present. It seems that I am missing some basic concepts about descriptors and how they can be used. Can anyone explain this behaviour of __get__, __set__?

Gregory answered 10/9, 2016 at 11:38 Comment(0)
C
13

You successfully created a proper non-data descriptor, but you then mask the d attribute by setting an instance attribute.

Because it is a non-data descriptor, the instance attribute wins in this case. When you add a __set__ method, you turn your descriptor into a data descriptor, and data descriptors are always applied even if there is an instance attribute. (*)

From the Descriptor Howto:

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with a.__dict__['x'], then type(a).__dict__['x'], and continuing through the base classes of type(a) excluding metaclasses. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.

and

If an object defines both __get__() and __set__(), it is considered a data descriptor. Descriptors that only define __get__() are called non-data descriptors (they are typically used for methods but other uses are possible).

Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence.

If you remove the d instance attribute (never set it or delete it from the instance), the descriptor object gets invoked:

>>> class D(object):
...     def __init__(self, x = 1395):
...         self.x = x
...     def __get__(self, instance, owner):
...         print "getting", self.x
...         return self.x
...
>>> class C(object):
...     d = D()
...
>>> c = C()
>>> c.d
getting 1395
1395

Add an instance attribute again and the descriptor is ignored because the instance attribute wins:

>>> c.d = 42  # setting an instance attribute
>>> c.d
42
>>> del c.d   # deleting it again
>>> c.d
getting 1395
1395

Also see the Invoking Descriptors documentation in the Python Datamodel reference.


(*) Provided the data descriptor implements the __get__ hook. Accessing such a descriptor via instance.attribute_name will return the descriptor object unless 'attribute_name' exists in instance.__dict__.

Competition answered 10/9, 2016 at 11:41 Comment(8)
What if only __set__ is defined. what is this?Gametangium
@BAKEZQ: Then you have a data descriptor, one that doesn't hook into read access to the attribute name. You'll always get the descriptor object itself in that case.Competition
@BAKEZQ: take into account that such an unusual descriptor implementation doesn't override attribute access on the instance, so if the descriptor is Class.set_descriptor and instance is an instance of Class, then it depends on instance.__dict__['set_descriptor'] existing if the descriptor or the instance attribute value is returned when accessing instance.set_descriptor.Competition
What about the assignment. It seems that the descriptor always override the attributes in __dict__(I mean it always calls __set__)Gametangium
@BAKEZQ: yes, a descriptor that implements __set__ is always called when trying to set the attribute name.Competition
"Data and non-data descriptors differ in how overrides are calculated with respect to entries in an instance’s dictionary. If an instance’s dictionary has an entry with the same name as a data descriptor, the data descriptor takes precedence. If an instance’s dictionary has an entry with the same name as a non-data descriptor, the dictionary entry takes precedence." In the docs, they did not provide any example for this statement and the question above is a perfect example!Whitley
"Where this occurs in the precedence chain depends on which descriptor methods were defined.". I am not able to undestand it.Universalize
@novice: that's explained in the second quote. If your descriptor only defines __get__ it has lower precedence (instance attributes are found first), if it defines at least one of the __set__ or __delete__ methods, then the descriptor has higher precedence, it will be used even if there is an instance attribute with the same name.Competition

© 2022 - 2024 — McMap. All rights reserved.