Photo by Kai Pilger on Unsplash

Write-count-limited attributes in Python

In this post I demonstrate a solution to generalize the creation of class attributes for various write-count-limited scenarios, such as:

  1. Class-level constant attributes (read-only)
  2. Write-once attributes, i.e. can be written once and then become read-only
  3. Object-level constant attributes (read-only)
  4. Attributes which can be written a number of times up to a specified limit before becoming read-only

Generalizing

Upon inspection, it should become clear that

all of the above are actually cases of a general write-count limit in the range 0-N.

Specifically, for each case above, the write-count-limit is as follows:

  1. write-count-limit = 0 (but the constant must be supplied at the class-level)
  2. write-count-limit = 1 (if not initialized) or 2 (if initialized)
  3. write-count-limit = 1 and initialized (i.e. the initializer consumes the single write-count with the “constant”)
  4. write-count-limit = N (if not initialized) or N+1 (if initialized)

Note that we distinguished cases where an attribute was or wasn’t initialized (i.e. written with an initial value in a client object’s __init__() method).

What should happen when we attempt exceed the write-count-limit?

We should raise an appropriate exception so that application code can decide how to handle it. Disabling exceptions should be possible on a per-attribute basis.

Also, wouldn’t it be great to be able to get state information for a particular attribute of a specific object as to whether it is now read-only, how many writes have been performed or the specified limit?

Solution

If you want to jump straight to the code, go here:

https://github.com/sebastian-ahmed/python-etc/tree/main/write_limited_attributes

The class which implements the general write-count-limited attribute is defined in WriteLimited.py and examples of each case is simulated in test.py

What about properties? Can’t we just use these? The short answer is yes, but the solution would not be generalized enough, i.e., each case would require maintaining a shadow raw-attribute as well as a count attribute. That’s pretty heavy if you need this sort of behavior on a number of attributes.

Also, there is no way to enforce (or catch) the user of a class to modify the underlying raw-attributes. Working around the descriptor interface is more involved and thus a less likely mechanism for inadvertent or “lazy” circumventing.

Instead of using properties, we leverage the descriptor protocol and a custom exception

The descriptor protocol allows us to create a descriptor class that encapsulates and abstracts the behaviors at the attribute-level.

Along with such a class, a custom exception is defined which can thus be finely controlled (as opposed to raising a more generic AttributeError or other exception). For this purpose, the descriptor raises the WriteLimitError custom exception. Had we used properties, the exception raising would need to be done in the client-class.

Usage Example

In order to better illustrate the concepts, below is a code snippet of a valid use of the WriteLimited descriptor class within a client class that implements the various write-limited scenarios:

It should be noted that the class-level constants do not have any initialization because the constant is passed directly to the descriptor object. It can also be noted that all but attribute_2 will raise an exception if written beyond their limit. Lastly, I will re-iterate that all the assignments in the __init__() method count as a single write.

If we instantiate an object of this class, print it and then attempt to write to attribute_5 11 times (one more than allowed), we get the following output:

Example usage output

The custom exception provides a useful message telling us the offending attribute name and pre-set max write-count.

Note that in the screen capture above, we were able to print out the object attribute write-count and is_read-only information. This is made possible by some special attribute utility methods provided by the class, specifically WriteLimited.getattr_wcount(instance,name) and WriteLimited.getattr_ro(instance,name). These static methods work with a similar interface to the data-model getattr methods in that they take an object reference (instance) and a name of the attribute. How do these work?

Shadow Attributes (gory details)

If you are familiar with descriptors, you’ll know that the interface only supports a few methods relating to setting/getting/deleting.

The solution to this problem (of providing some sort of per-attribute level state) is not immediately obvious. It involves creating and maintaining a set of shadow attributes at the client-object instance level. Essentially, each time we set an instance-level attribute, we shadow this operation by setting/updating shadow attributes that maintain the per-client-instance state.

In order to support the getattr_*() utility methods, there needs to be a way to make sure that the shadow attributes are up-to-date for the requested instance. To achieve this, both the __get__() and __set__() descriptor interface methods perform updates to the shadow attributes. The __get__() method is actually called by the utility methods as a priming operation which allows the shadow attributes to be updated since reads have no side-effects.

For complete details, see WriteLimited.py.

Summary

In this post I’ve provided a relatively straightforward and generalized interface for creating various write-limited attribute applications including exception support to catch limit violations. This approach is much lighter weight than using properties which natively only support instance-level read-only semantics (by omission of the setter method).

In regard to implementation details, I’ve shown that the descriptor protocol approach provides the necessary mechanics to generalize the solution and encapsulate the limit/access-violation checking with integrated exception mechanics. I’ve also shown how dynamically generated shadow attributes can provide a rich state visibility layer for attributes,

making attributes stateful beyond their mere assigned values

As with all my posts, working code is available and in a state ready for re-use:

https://github.com/sebastian-ahmed/python-etc/tree/main/write_limited_attributes

Technology Leader | Systems Architect | Programmer | Photographer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store