Читать книгу Professional Python - Luke Sneeringer - Страница 9

Part I
Functions
Chapter 1
Decorators
Decorating Classes

Оглавление

Remember that a decorator is, fundamentally, a callable that accepts a callable and returns a callable. This means that decorators can be used to decorate classes as well as functions (classes are callable, after all).

Decorating classes can have a variety of uses. They can be particularly valuable because, like function decorators, class decorators can interact with the attributes of the decorated class. A class decorator can add or augment attributes, or it can alter the API of a class to provide a distinction between how a class is declared versus how its instances are used.

You might ask, "Isn't the appropriate way to add or augment attributes of a class through subclassing?" Usually, the answer is "yes." However, in some situations an alternative approach may be appropriate. Consider, for example, a generally applicable feature that may apply to many classes in your application that live in distinct places in your class hierarchies.

By way of example, consider a feature of a class such that each instance knows when it was instantiated, and instances are sorted by their creation times. This has general applicability across many different classes, and requires the addition of three attributes – the instantiation timestamp, and the __gt__ and __lt__ methods.

You have multiple ways to go about adding this. Here is how you can do it with a class decorator:


The first thing that is happening in this decorator is that you are saving a copy of the class's original __init__ method. You do not need to worry about whether the class has one. Because object has an __init__ method, that attribute's presence is guaranteed. Next, you create a new method that will be assigned to __init__, and this method first calls the original and then does one piece of extra work, saving the instantiation timestamp to self._created.

It is worth noting that this is a very similar pattern to the execution-time wrapping code from previous examples – making a function that wraps another function, whose primary responsibility is to run the wrapped function, but also adds a small piece of other functionality.

It is worth noting that if a class decorated with @sortable_by_creation_time defined its own __lt__ and __gt__ methods, then this decorator would override them.

The _created value by itself does little good if the class does not recognize that it is to be used for sorting. Therefore, the decorator also adds __lt__ and __gt__ magic methods. These cause the < and > operators to return True or False based on the result of those methods. This also affects the behavior of sorted and other similar functions.

This is all that is necessary to make an arbitrary class's instances sortable by their instantiation time. This decorator can be applied to any class, including many classes with unrelated ancestry.

Here is an example of a simple class with instances sortable by when they are created:


Bear in mind that simply because a decorator can be used to solve a problem, that does not mean that it is necessarily the appropriate solution.

For instance, when it comes to this example, the same thing could be accomplished by using a "mixin," or a small class that simply defines the appropriate __init__, __lt__, and __gt__ methods. A simple approach using a mixin would look like this:


Applying the mixin to a class can be done using Python's multiple inheritance:


This approach has different advantages and drawbacks. On the one hand, it will not mercilessly plow over __lt__ and __gt__ methods defined by the class or its superclasses (and it may not be obvious when the code is read later that the decorator was clobbering two methods).

On the other hand, it would be very easy to get into a situation where the __init__ method provided by SortableByCreationTime does not run. If MyClass or MySuperclass or any class in MySuperclass's ancestry defines an __init__ method, it will win out. Reversing the class order does not solve this problem; it simply reverses it.

By contrast, the decorator handles the __init__ case very well, simply by augmenting the effect of the decorated class's __init__ method and otherwise leaving it intact.

So, which approach is the correct approach? It depends.

Professional Python

Подняться наверх