Search code examples
pythonexceptionpython-descriptors

What does "exception raising placeholder" mean in the Descriptor HowTo Guide?


In the Python's Descriptor HowTo Guide there's this paragraph (bold added):

Descriptor HowTo Guide

Descriptor protocol

(...)

To make a read-only data descriptor, define both __get__() and __set__() with the __set__() raising an AttributeError when called. Defining the __set__() method with an exception raising placeholder is enough to make it a data descriptor.

I'm not sure what the expression: "exception raising placeholder" here means exactly (googling "exception placeholder" also doesn't help clarify the expression) since the only results on Google are quotes from the "Descriptor HowTo Guide" itself.

Does it mean to raise any exception in the descriptor's __set__()? Does the exception have to be an AttributeError? Or does it mean something else?


Solution

  • It means that, if you want a data-descriptor, all you need is that it has a __set__ method - and even if that __set__ method will always raise an exception when called, it will still be a data-descriptor.

    This "method that always raise an exception" is what is meant by "exception raising placeholder" in the docs.

    A class without a __set__ method is a "non-data descriptor" - the main change is that for non-data descriptors, the instance is always checked when retrieving an attribute - even if there is a (non-data) descriptor in that class, with the same name, the instance attribute is retrieved. Because of that it is possible to override or "remove" a method from a specific instance from a class: you just assign some other value to the method name in the instance, and that value will be used in place of the original method.

    Now, if the descriptor class does have a __set__ method, trying to set the value on the instance will always go through it, and the instance value is not set by ordinary means. If that method raises an exception, this makes the descriptor value immutable: one simply can't assign a new value to it in any instance. (Assigning to its name on the class will effectively remove the whole descriptor, of course.)

    It is interesting to note that Python propertyes are always data descriptors: the property object itself does have a __set__ method even if one does not configure a property "setter". And it is the exact case that when trying to set a value in a such a property, an exception will be raised: a property without a setter is an example of a descriptor containing a exception raising method.

    Some snippets to demonstrate the points:

    In [7]: class NonDataDescriptor:
       ...:     def __get__(self, inst, owner):
       ...:         return 23
       ...: 
    
    In [8]: class A:
       ...:     b = NonDataDescriptor()
       ...: 
    
    In [9]: c = A()
    
    In [10]: c.b
    Out[10]: 23
    
    In [11]: c.__dict__
    Out[11]: {}
    
    In [12]: c.b = 42
    
    In [13]: c.__dict__
    Out[13]: {'b': 42}
    
    In [14]: c.b
    Out[14]: 42
    
    In [15]: # descriptor is superseeded by value attributed to the instance
    
    In [16]: A().b   # in a new instance, the descriptor works as new
    Out[16]: 23
    
    In [19]: class ImutableDataDescriptor:
        ...:     def __get__(self, inst, owner):
        ...:         return 55
        ...:     def __set__(self, inst, value):
        ...:         # Look! An exception raising placeholder!!
        ...:         raise NotImplementedError()  # or whatever exception
        ...: 
    
    In [20]: class D:
        ...:     e = ImutableDataDescriptor()
        ...: 
    
    In [21]: f = D()
    
    In [22]: f.e
    Out[22]: 55
    
    In [23]: f.e = 3125
    ---------------------------------------------------------------------------
    NotImplementedError                       Traceback (most recent call last)
    Cell In [23], line 1
    ----> 1 f.e = 3125
    
    Cell In [19], line 6, in ImutableDataDescriptor.__set__(self, inst, value)
          4 def __set__(self, inst, value):
          5     # Look! An exception raising placeholder!!
    ----> 6     raise NotImplementedError()
    
    NotImplementedError: 
    
    In [24]: f.__dict__
    Out[24]: {}
    
    In [26]: class G:  
        ...:     # ordinary properties with no setters work as data-descriptors
        ...:     @property
        ...:     def h(self):
        ...:         return 17
        ...: 
    
    In [27]: i = G()
    
    In [28]: i.h
    Out[28]: 17
    
    In [29]: i.h = 23
    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    Cell In [29], line 1
    ----> 1 i.h = 23
    
    AttributeError: property 'h' of 'G' object has no setter