6

I get the following example from Effective Python item 31:

from weakref import WeakKeyDictionary
class Grade(object):
    def __init__(self):
        self._values = WeakKeyDictionary()
    def __get__(self, instance, instance_type):
        if instance is None: return self
        return self._values.get(instance, 0)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self._values[instance] = value


# Example 16
class Exam(object):
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

first_exam = Exam()
first_exam.writing_grade = 82
second_exam = Exam()
second_exam.writing_grade = 75
print('First ', first_exam.writing_grade, 'is right')
print('Second', second_exam.writing_grade, 'is right')

I can't think of any reason to have if instance is None: return self in __get__. How can an Exam (or other potential classes using Grade) instance be None?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
RNA
  • 146,987
  • 15
  • 52
  • 70

1 Answers1

7

Python will pass in None for the instance when accessing the descriptor on the class.

By returning self in that case you can access the descriptor object on the class without having to bypass the protocol (by accessing ClassObj.__dict__['name_of_descriptor']).

>>> class DemoDescriptor:
...     def __get__(self, instance, type_):
...         if instance is None:
...             print('Accessing descriptor on the class')
...             return self
...         print('Accessing descriptor on the instance')
...         return 'Descriptor value for instance {!r}'.format(instance)
... 
>>> class DemoClass(object):
...     foo = DemoDescriptor()
... 
>>> DemoClass.foo  # on the class
Accessing descriptor on the class
<__main__.DemoDescriptor object at 0x1041d3c50>
>>> DemoClass.__dict__['foo']  # bypassing the descriptor protocol
<__main__.DemoDescriptor object at 0x1041d3c50>
>>> DemoClass().foo  # on the instance
Accessing descriptor on the instance
'Descriptor value for instance <__main__.DemoClass object at 0x1041d3438>'

This is how the __get__ method implementations for function and property objects work too.

For your specific case, each of Exam.math_grade, Exam.writing_grade or Exam.science_grade will call Grade.__get__, passing in None for the instance, and Exam for the instance_type.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • what about `__set__`? will `Exam.math_grade=3` pass `None` to `instance` argument too? Is there any use case of accessing the descriptor object by returning `self`? – RNA Apr 03 '15 at 17:34
  • @RNA: descriptors only support *setting* on instances. Assigning to `Exam.math_grade` would replace the descriptor object with whatever the right-hand-side produces. – Martijn Pieters Apr 03 '15 at 17:41
  • that seems not true (?). let me update the question. – RNA Apr 03 '15 at 17:42
  • @RNA: The `@property` descriptor object heavily relies on `self` being returned, as otherwise you couldn't use the object in sub-classes to override a property setter or getter or deleter. – Martijn Pieters Apr 03 '15 at 17:42
  • @RNA: Perhaps you made a mistake in your test; it very much is true, see https://gist.github.com/mjpieters/531cb4d657d0b38b8684 – Martijn Pieters Apr 03 '15 at 17:44
  • I got what you mean by "replacing the descriptor object". but still have problem understanding "The `@property` descriptor object heavily relies on self being returned"... could you give an example why it is good to return `self`? – RNA Apr 03 '15 at 17:57
  • 1
    @RNA: `ClassObject.descriptorname` returns whatever `ClassObject.__dict__['descriptorname'].__get__(None, ClassObject)` returns. Returning something other than `self` would give you whatever you returned instead. That's inconvenient if you need to use the descriptor object itself repeatedly. – Martijn Pieters Apr 03 '15 at 18:00
  • @RNA: see [Python overriding getter without setter](https://stackoverflow.com/a/15786149) for an example where a `property` descriptor is accessed; `human.name` is a property descriptor object, and that invokes `human.__dict__['name'].__get__(None, human)`. – Martijn Pieters Apr 03 '15 at 18:01