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 anAttributeError
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?
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 property
es 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