Two Kinds of Descriptors

In the previous section we used a descriptor with both __get__() and __set__() methods. Such descriptors, by the way, are called data descriptors. Descriptors with only the __get__() method are somewhat weaker than their cousins, and called non-data descriptors.

Repeating our experiment, but this time with non-data descriptors, we get:

Example 1.5. Non-data descriptors

class GetonlyDesc(object):
    "Another useless descriptor"
    
    def __get__(self, obj, typ=None):
        pass

class C(object):
    "A class with a single descriptor"
    d = GetonlyDesc()
    
cobj = C()

x = cobj.d 1
cobj.d = "setting a value" 2
x = cobj.d 3
del cobj.d 4

x = C.d 5
C.d = "setting a value on class" 6


1

Calls d.__get__(cobj, C) (just like before).

2

Puts "setting a value" in the instance itself (in cobj.__dict__ to be precise).

3

Surprise! This now returns "setting a value", that is picked up from cobj.__dict__. Recall that for a data descriptor, the instance's __dict__ is bypassed.

4

Deletes the attribute d from the instance (from cobj.__dict__ to be precise).

5 6

These function identical to a data descriptor.

Interestingly, not having a __set__() affects not just attribute setting, but also retrieval. What is Python thinking? If on setting, the descriptor gets fired and puts the data somewhere, then it follows that the descriptor only knows how to get it back. Why even bother with the instance's __dict__?

Data descriptors are useful for providing full control over an attribute. This is what one usually wants for attributes used to store some piece of data. For example an attribute that gets transformed and saved somewhere on setting, would usually be reverse-transformed and returned when read. When you have a data descriptor, it controls all access (both read and write) to the attribute on an instance. Of course, you could still directly go to the class and replace the descriptor, but you can't do that from an instance of the class.

Non-data descriptors, in contrast, only provide a value when an instance itself does not have a value. So setting the attribute on an instance hides the descriptor. This is particularly useful in the case of functions (which are non-data descriptors) as it allows one to hide a function defined in the class by attaching one to an instance.

Example 1.6. Hiding a method

class C(object):
    def f(self):
        return "f defined in class"

cobj = C()

cobj.f() 1

def another_f():
    return "another f"

cobj.f = another_f

cobj.f() 2


1

Calls the bound method returned by f.__get__(cobj, C). Essentially ends up calling C.__dict__['f'](cobj).

2

Calls another_f(). The function f() defined in C has been hidden.