There are many good uses for read-only—or immutable—property descriptors. In fact, there is a lot to back up the idea of having everything be effectively immutable. Unfortunately, due to Python’s inherent lack of being able to make anything actually immutable, interpreter optimization isn’t one of those possible benefits with Python. (PyPy may be able to make JIT optimizations because of it, but don’t take my word for it.)
There are plenty of other benefits to immutability, but those are beyond the scope of this book. The point of this chapter is to show how a descriptor can make instance-level properties be effectively immutable.
A first stab at making a read-only descriptor might be to not give it a __set__() method, but that works only if there’s a __delete__() method. If there’s no __delete__() method either, it becomes a non-data descriptor. If it’s a non-data descriptor and someone assigns to it, then it just creates an instance attribute that overrides the descriptor. This is clearly not what we want.
No, to truly keep users from assigning new values, __set__() is required, but it obviously can’t work as normal. So, what can it do? It can raise an exception. AttributeError is probably the best option of the built-in exceptions, but the functionality is almost unique enough to make a custom exception. It’s up to you, but the examples use AttributeError.
Now that the attribute can’t be changed, how does one supply it with its original value? Trying to send it through the descriptor’s constructor would simply end up with the same value for every instance, since the constructor is only called at class creation time. There needs to be some sort of back door. Three different techniques will be discussed: set-once, secret-set, and forced-set.
Set-Once Descriptors
A set-once descriptor is the most restrictive of the three read-only properties in that it most strongly restricts the number of assignments to once per instance under it.
Set-once descriptors work simply by checking whether a value is already set and acting accordingly. If it’s already assigned, it raises an exception; if it’s not, then it sets it.
First, it checks to see if there’s already a value set for the instance. If there is, it raises an AttributeError . Otherwise, it sets the value. Simple.
Of the three read-only descriptors, it’s also the simplest to use, since it’s set the same way descriptors are normally set: using simple assignment. The others each have a roundabout way of getting the value set. Also, because of it having a typical use for setting the value, it’s also the easiest to make versatile.
Secret-Set Descriptors
Secret-set descriptors use a “secret” method on the descriptor to initialize the value. The method uses the same parameters as __set__() and sets the value exactly the way __set__() would do with a normal descriptor. But with this technique, the __set__() method just raises an error.
The descriptor is accessed, then set()—the descriptor’s “secret” set method—is called to initialize the value for the instance. This is more verbose than self.val = value, but it works.
In the library, there are some helper functions (some of which are standardized within the library) that can be used. The one that is most guaranteed to work in every case (including instance attributes) is setattribute(instance, attr_name, value). There are also some optional parameters with default values that can be set for specifying the specific behavior, but the defaults will try everything (including techniques not shown here yet) until something works.
Forced-Set Descriptors
Now the __set__() method checks whether the forced parameter is set to True. If it’s not, then the method fails like any other read-only descriptor should. If it is True, though, then the method knows to let it pass and actually set the value.
If a descriptor is truly only meant to be written to during object creation, using the set-once descriptor is the best choice. It’s harder for users of the descriptor to thwart the read-only nature of the set-once descriptor than it is for the other two options. Choosing between either of the other two is a matter of preference. Some may find that altering the signature of a “magic” method doesn’t sit well with them, although some may enjoy the lack of a need for another method. Some may actually prefer the additional method, since they may already be using it, as shown in some examples in Chapter 11. For the most part, choosing between the secret-set and forced-set descriptor designs is just about preference.
Class Constants
Class constants are very much like read-only descriptors except that, when done properly, they don’t need to be set-once; instead, they’re set upon creation. This requires a little bit of tweaking, though.
First, you must realize that a descriptor for a class constant must be implemented as a metadescriptor (in case you forgot, that’s a descriptor on the metaclass) instead of a normal one. Second, each class that has constants will likely have its own set of constants, which means each of those classes will need a custom metaclass just for itself.
It’s an extremely simple descriptor, receiving a value in its constructor, returning it with a __get__() call, and raising an AttributeError if someone attempts to change or delete the value.
Now PI, e, and the GOLDEN_RATIO are constants of the Math class. The only way to mess with them is through the metaclass. A downside to using a metadescriptor for this is the fact the constants can no longer be accessed through instances of classes with the constant. This isn’t really a problem though, since many other languages never permitted that kind of access to begin with. There are also multiclassing issues that can pop up with classes that have different metaclasses, but that’s a pretty rare issue.
So, now that there’s a Constant metadescriptor and it’s understood how to use it, I will now channel my inner Raymond Hettinger by saying, “There must be a better way!” Nobody wants to make a metaclass just so they can make a normal class have constants.
There! Now, just by setting the resulting metaclass as Math’s metaclass, it has the constants provided by the keyword arguments given to withConstants(). There is one huge drawback to using this over the other way: autocompletion. You’d be hard pressed to find an editor that can autocomplete on something created completely dynamically like this.
Summary
This chapter has examined several different techniques to make descriptors for read-only attributes (or, at least, read-only-ish attributes). One thing to note in all of this is that none of the techniques actually make it impossible to change the values; they only make it difficult to do so, requiring extra steps in order to signify to the user that doing so is not what was intended. Such is the way of Python; after all, we’re all consenting adults here.