Chapter 17. Descriptors with Class Decorators

In this section we combine descriptors with class decorators to create a powerful mechanism for creating validated attributes.

Up to now if we wanted to ensure that an attribute was set to only a valid value we have relied on properties (or used getter and setter methods). The disadvantage of such approaches is that we must add validating code for every attribute in every class that needs it. What would be much more convenient and easier to maintain, is if we could add attributes to classes with the necessary validation built in. Here is an example of the syntax we would like to use:

@valid_string("name", empty_allowed=False)
@valid_string("productid", empty_allowed=False,
              regex=re.compile(r"[A-Z]{3}d{4}"))
@valid_string("category", empty_allowed=False, acceptable=
        frozenset(["Consumables", "Hardware", "Software", "Media"]))
@valid_number("price", minimum=0, maximum=1e6)
@valid_number("quantity", minimum=1, maximum=1000)
class StockItem:

    def __init__(self, name, productid, category, price, quantity):
        self.name = name
        self.productid = productid
        self.category = category
        self.price = price
        self.quantity = quantity


The StockItem class’s attributes are all validated. For example, the productid attribute can be set only to a nonempty string that starts with three uppercase letters and ends with four digits, the category attribute can be set only to a nonempty string that is one of the specified values, and the quantity attribute can be set only to a number between 1 and 1 000 inclusive. If we try to set an invalid value an exception is raised.

The validation is achieved by combining class decorators with descriptors. As we noted earlier, class decorators can take only a single argument—the class they are to decorate. So here we have used the technique shown when we first discussed class decorators, and have the valid_string() and valid_number() functions take whatever arguments we want, and then return a decorator, which in turn takes the class and returns a modified version of the class.

Let’s now look at the valid_string() function:

def valid_string(attr_name, empty_allowed=True, regex=None,
                 acceptable=None):
    def decorator(cls):

    name = "__" + attr_name
    def getter(self):
        return getattr(self, name)
    def setter(self, value):
        assert isinstance(value, str), (attr_name +
                                        " must be a string")
        if not empty_allowed and not value:
            raise ValueError("{0} may not be empty".format(
                             attr_name))
        if ((acceptable is not None and value not in acceptable) or
            (regex is not None and not regex.match(value))):
            raise ValueError("{0} cannot be set to {1}".format(
                             attr_name, value))
        setattr(self, name, value)
    setattr(cls, attr_name, GenericDescriptor(getter, setter))
    return cls
return decorator


The function starts by creating a class decorator function which takes a class as its sole argument. The decorator adds two attributes to the class it decorates: a private data attribute and a descriptor. For example, when the valid_string() function is called with the name “productid”, the StockItem class gains the attribute __productid which holds the product ID’s value, and the descriptor productid attribute which is used to access the value. For example, if we create an item using item = StockItem("TV", "TVA4312", "Electrical", 500, 1), we can get the product ID using item.productid and set it using, for example, item.productid = "TVB2100".

The getter function created by the decorator simply uses the global getattr() function to return the value of the private data attribute. The setter function incorporates the validation, and at the end, uses setattr() to set the private data attribute to the new (and valid) value. In fact, the private data attribute is only created the first time it is set.

Once the getter and setter functions have been created we use setattr() once again, this time to create a new class attribute with the given name (e.g., productid), and with its value set to be a descriptor of type GenericDescriptor. At the end, the decorator function returns the modified class, and the valid_string() function returns the decorator function.

The valid_number() function is structurally identical to the valid_string() function, only differing in the arguments it accepts and in the validation code in the setter, so we won’t show it here. (The complete source code is in the Valid.py module.)

The last thing we need to cover is the GenericDescriptor, and that turns out to be the easiest part:

<a id="page_65/">class GenericDescriptor:

    def __init__(self, getter, setter):
        self.getter = getter
        self.setter = setter

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        return self.getter(instance)

    def __set__(self, instance, value):
        return self.setter(instance, value)


The descriptor is used to hold the getter and setter functions for each attribute and simply passes on the work of getting and setting to those functions.

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

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