Читать книгу Professional Python - Luke Sneeringer - Страница 10
Part I
Functions
Chapter 1
Decorators
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:
A Pitfall
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.
Конец ознакомительного фрагмента. Купить книгу