© Jacob Zimmerman 2018
Jacob ZimmermanPython Descriptorshttps://doi.org/10.1007/978-1-4842-3727-4_8

8. Read-Only Descriptors

Jacob Zimmerman1 
(1)
New York, USA
 

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.

For example, this is what the basic __set__() method would look like if the descriptor was using on-descriptor storage in the instance attribute, storage.
def __set__(self, instance, value):
    if instance in self.storage:
        raise AttributeError("Cannot set new value on read-only property")
    self.storage[instance] = value

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.

To have access to the secret method, access to the actual descriptor object is needed. With the current general standard of returning self in the __get__() method when no instance is provided, getting the descriptor from the instance is as easy as type(a).x (you could change it to directly use the class name, but that ignores inheritance and makes a little more if you ever refactor the name). Even with returning unbound attributes, this is possible, although it requires an extra step. You may recall that UnboundAttribute has a descriptor attribute of its own. So, the lookup becomes just a little longer. Instead of just type(a).x, it becomes type(a).x.descriptor. Once you have access to the descriptor object, all that needs to be done is call the “secret” set method. Here’s an example of a class using a secret-set descriptor called ROValue in the __init__() method.
class A:
    val = ROValue()
    def __init__(self, value):
        type(self).val.set(self, value)

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

The way that forced-set descriptors work is, instead of using an entirely new method as a back door, it still uses __set__(), but with a twist. Instead of just the typical three parameters (self, instance, and value), it has a fourth with a default value. This parameter is something like forced=False. This makes it so that the built-in way of calling __set__() will not cause the value to be set. Rather, the descriptor object needs to be accessed and have __set__() called explicitly with the additional forced=True argument. So, if ROValue was a forced-set descriptor instead the previous secret-set one, the basic __set__() method would look like this:
def __set__(self, instance, value, forced=False):
    if not forced:
        raise AttributeError("Cannot set new value on read-only property")
    # setter implementation here

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.

To begin, here’s the actual descriptor that will be used.
class Constant:
   def __init__(self, value):
       self.value = value
   def __get__(self, instance, owner):
       return self.value
   def __set__(self, instance, value):
       raise AttributeError("Cannot change a constant")
   def __delete__(self, instance):
       raise AttributeError("Cannot delete a constant")

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.

To use this descriptor, though, it must be placed in a metaclass, which must then have a class to derive from it. For an example, here is an instance of a metaclass and class holding several mathematical constants.
class MathMeta(type):
    PI = Constant(3.14159)
    e = Constant(2.71828)
    GOLDEN_RATIO = Constant(1.61803)
class Math(metaclass=MathMeta):
    pass

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 is a better way. Python allows for dynamically defining classes and metaclasses, and if they’re created within a function, that definition can be reused dynamically over and over again. Here’s how.
def withConstants(**kwargs):
    class MetaForConstants(type):
        pass
    for k, v in kwargs.items():
        MetaForConstants.__dict__[k] = Constant(v)
    return MetaForConstants
This function creates a metaclass using each given keyword argument as a new Constant and returns the metaclass. Here’s what the new Math class definition would look like with this function instead of the fully written metaclass.
class Math(metaclass=withConstants(PI=3.14159, e=2.71828, GOLDEN_RATIO=1.61803)):
    pass

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.71.106