Chapter 15. Metaclasses

A metaclass is to a class what a class is to an instance; that is, a metaclass is used to create classes, just as classes are used to create instances. And just as we can ask whether an instance belongs to a class by using isinstance(), we can ask whether a class object (such as dict, int, or SortedList) inherits another class using issubclass().

The simplest use of metaclasses is to make custom classes fit into Python’s standard ABC hierarchy. For example, to make SortedList a collections. Sequence, instead of inheriting the ABC (as we showed earlier), we can simply register the SortedList as a collections.Sequence:

class SortedList:
    ...
collections.Sequence.register(SortedList)


After the class is defined normally, we register it with the collections.Sequence ABC. Registering a class like this makes it a virtual subclass.[*] A virtual subclass reports that it is a subclass of the class or classes it is registered with (e.g., using isinstance() or issubclass()), but does not inherit any data or methods from any of the classes it is registered with.

Registering a class like this provides a promise that the class provides the API of the classes it is registered with, but does not provide any guarantee that it will honor its promise. One use of metaclasses is to provide both a promise and a guarantee about a class’s API. Another use is to modify a class in some way (like a class decorator does). And of course, metaclasses can be used for both purposes at the same time.

Suppose we want to create a group of classes that all provide load() and save() methods. We can do this by creating a class that when used as a metaclass, checks that these methods are present:

class LoadableSaveable(type):

    def __init__(cls, classname, bases, dictionary):
        super().__init__(classname, bases, dictionary)
        assert hasattr(cls, "load") and 
               isinstance(getattr(cls, "load"),
                          collections.Callable), ("class '" +
               classname + "' must provide a load() method")
        assert hasattr(cls, "save") and 
               isinstance(getattr(cls, "save"),
                          collections.Callable), ("class '" +
               classname + "' must provide a save() method")


Classes that are to serve as metaclasses must inherit from the ultimate metaclass base class, type, or one of its subclasses.

Note that this class is called when classes that use it are instantiated, in all probability not very often, so the runtime cost is extremely low. Notice also that we must perform the checks after the class has been created (using the super() call), since only then will the class’s attributes be available in the class itself. (The attributes are in the dictionary, but we prefer to work on the actual initialized class when doing checks.)

We could have checked that the load and save attributes are callable using hasattr() to check that they have the __call__ attribute, but we prefer to check whether they are instances of collections.Callable instead. The collections.Callable abstract base class provides the promise (but no guarantee) that instances of its subclasses (or virtual subclasses) are callable.

Once the class has been created (using type.__new__() or a reimplementation of __new__()), the metaclass is initialized by calling its __init__() method. The arguments given to __init__() are cls, the class that’s just been created; classname, the class’s name (also available from cls.__name__); bases, a list of the class’s base classes (excluding object, and therefore possibly empty); and dictionary that holds the attributes that became class attributes when the cls class was created, unless we intervened in a reimplementation of the meta-class’s __new__() method.

Here are a couple of interactive examples that show what happens when we create classes using the LoadableSaveable metaclass:

>>> class Bad(metaclass=Meta.LoadableSaveable):
...     def some_method(self): pass
Traceback (most recent call last):
...
AssertionError: class 'Bad' must provide a load() method


The metaclass specifies that classes using it must provide certain methods, and when they don’t, as in this case, an AssertionError exception is raised.

>>> class Good(metaclass=Meta.LoadableSaveable):
...    def load(self): pass
...    def save(self): pass
>>> g = Good()


The Good class honors the metaclass’s API requirements, even if it doesn’t meet our informal expectations of how it should behave.

We can also use metaclasses to change the classes that use them. If the change involves the name, base classes, or dictionary of the class being created (e.g., its slots), then we need to reimplement the metaclass’s __new__() method; but for other changes, such as adding methods or data attributes, reimplemeting __init__() is sufficient, although this can also be done in __new__(). We will now look at a metaclass that modifies the classes it is used with purely through its __new__() method.

As an alternative to using the @property and @name.setter decorators, we could create classes where we use a simple naming convention to identify properties. For example, if a class has methods of the form get_name() and set_name(), we would expect the class to have a private __name property accessed using instance.name for getting and setting. This can all be done using a metaclass. Here is an example of a class that uses this convention:

class Product(metaclass=AutoSlotProperties):

    def __init__(self, barcode, description):
        self.__barcode = barcode
        self.description = description

    def get_barcode(self):
        return self.__barcode

    def get_description(self):
        return self.__description

    def set_description(self, description):
        if description is None or len(description) < 3:
            self.__description = "<Invalid Description>"
        else:
            self.__description = description


We must assign to the private __barcode property in the initializer since there is no setter for it; another consequence of this is that barcode is a read-only property. On the other hand, description is a readable/writable property. Here are some examples of interactive use:

>>> product = Product("101110110", "8mm Stapler")
>>> product.barcode, product.description
('101110110', '8mm Stapler')
>>> product.description = "8mm Stapler (long)"
>>> product.barcode, product.description
('101110110', '8mm Stapler (long)')


If we attempt to assign to the bar code an AttributeError exception is raised with the error text “can’t set attribute”.

If we look at the Product class’s attributes (e.g., using dir()), the only public ones to be found are barcode and description. The get_name() and set_name() methods are no longer there—they have been replaced with the name property. And the variables holding the bar code and description are also private (__bar-code and __description), and have been added as slots to minimize the class’s memory use. This is all done by the AutoSlotProperties metaclass which is implemented in a single method:

class AutoSlotProperties(type):

    def __new__(mcl, classname, bases, dictionary):
        slots = list(dictionary.get("__slots__", []))
        for getter_name in [key for key in dictionary
                            if key.startswith("get_")]:

        if isinstance(dictionary[getter_name],
                      collections.Callable):
            name = getter_name[4:]
            slots.append("__" + name)
            getter = dictionary.pop(getter_name)
            setter_name = "set_" + name
            setter = dictionary.get(setter_name, None)
            if (setter is not None and
                isinstance(setter, collections.Callable)):
                del dictionary[setter_name]
            dictionary[name] = property(getter, setter)
    dictionary["__slots__"] = tuple(slots)
    return super().__new__(mcl, classname, bases, dictionary)


A metaclass’s __new__() class method is called with the metaclass, and the class name, base classes, and dictionary of the class that is to be created. We must use a reimplementation of __new__() rather than __init__() because we want to change the dictionary before the class is created.

We begin by copying the __slots__ collection, creating an empty one if none is present, and making sure we have a list rather than a tuple so that we can modify it. For every attribute in the dictionary we pick out those that begin with "get_" and that are callable, that is, those that are getter methods. For each getter we add a private name to the slots to store the corresponding data; for example, given getter get_name() we add __name to the slots. We then take a reference to the getter and delete it from the dictionary under its original name (this is done in one go using dict.pop()). We do the same for the setter if one is present, and then we create a new dictionary item with the desired property name as its key; for example, if the getter is get_name() the property name is name. We set the item’s value to be a property with the getter and setter (which might be None) that we have found and removed from the dictionary.

At the end we replace the original slots with the modified slots list which has a private slot for each property that was added, and call on the base class to actually create the class, but using our modified dictionary. Note that in this case we must pass the metaclass explicitly in the super() call; this is always the case for calls to __new__() because it is a class method and not an instance method.

For this example we didn’t need to write an __init__() method because we have done all the work in __new__(), but it is perfectly possible to reimplement both __new__() and __init__() doing different work in each.

If we consider hand-cranked drills to be analogous to aggregation and inheritance and electric drills the analog of decorators and descriptors, then meta-classes are at the laser beam end of the scale when it comes to power and versatility. Metaclasses are the last tool to reach for rather than the first, except perhaps for application framework developers who need to provide powerful facilities to their users without making the users go through hoops to realize the benefits on offer.



[*] In Python terminology, virtual does not mean the same thing as it does in C++ terminology.

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

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