Professional Python. Luke Sneeringer
Чтение книги онлайн.
Читать онлайн книгу Professional Python - Luke Sneeringer страница 7
@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.
Type Switching
Thus far, the discussion in this chapter has only considered cases in which a decorator is expected to decorate a function and provide a function, or when a decorator is expected to decorate a class and provide a class.
There is no reason why this relationship must hold, however. The only requirement for a decorator is that it is a callable that accepts a callable and returns the callable. There is no requirement that it return the same kind of callable.
One more advanced use case for decorators is actually when they do not do this. In particular, it can be valuable for a decorator to decorate a function, but return a class. This can be a very useful tool for situations where the amount of boilerplate code grows, or for allowing developers to use a simple function for simple cases, but subclass a class in an application's API for more advanced cases.
An example of this in the wild is a decorator used in a popular task runner in the Python ecosystem: celery. The celery package provides a @celery.task
decorator that is expected to decorate a function. What the decorator actually does is return a subclass of celery's internal Task
class, with the decorated function being used within the subclass's run
method.
Consider the following trivial example of a similar approach:
What is happening here? The decorator creates a subclass of Task
and returns the class. The class is callable calling a class creates an instance of that class and runs its _
init_
method
The value of doing this is that it provides a hook for lots of augmentation. The base Task
class can define much, much more than just the run
method. For example, a start
method might run the task asynchronously. The base class might also provide methods to save information about the task's status. Using a decorator that swaps out a function for a class here enables the developer to only consider the actual body of his or her task, and the decorator does the rest of the work.
You can see this in action by taking an instance of the class and running its identify
method, as shown here:
This exact approach carries with it some problems. In particular, once a task function is decorated with the @task_class
decorator, it becomes a class.
Consider the following simple task function decorated in this way:
Now, attempt to run it directly in the interpreter:
That is a bad thing. This decorator alters the function in such a way that if the developer runs it, it does not do what anyone expects. It is usually not acceptable to expect the function to be declared as foo
and then run using the convoluted foo().run()
(which is what would be necessary in this case).
Fixing this requires putting a little more thought into how both the decorator and the Task
class are constructed. Consider the following amended version:
A couple of key differences exist here. The first is the addition of the __call__
method to the base Task
class. The second difference (which complements the first) is that the @task_class
decorator now returns an instance of the TaskSubclass
, rather than the class itself.
Конец ознакомительного фрагмента.
Текст предоставлен ООО «ЛитРес».
Прочитайте эту книгу целиком, купив полную легальную версию на ЛитРес.
Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.