Diferències
Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.
| Ambdós costats versió prèvia Revisió prèvia | |||
| info:cursos:pue:python-pcpp1:m1:2.4 [05/11/2023 21:33] – suprimit - edició externa (Unknown date) 127.0.0.1 | info:cursos:pue:python-pcpp1:m1:2.4 [05/11/2023 21:33] (actual) – ↷ Page moved from info:cursos:pue:python-pcpp1:2.4 to info:cursos:pue:python-pcpp1:m1:2.4 mate | ||
|---|---|---|---|
| Línia 1: | Línia 1: | ||
| + | = 2.4 Decorators | ||
| + | A decorator is one of the design patterns that describes the structure of related objects. Python is able to decorate functions, methods, and classes. | ||
| + | The decorator' | ||
| + | |||
| + | Of course, the **decorating** function does more, because it can take the parameters of the decorated function and perform additional actions and that make it a real decorating function. | ||
| + | |||
| + | The same principle is applied when we decorate classes. We'll talk about this a bit later. | ||
| + | |||
| + | So from now on, the term ' | ||
| + | |||
| + | Decorators are used to perform operations before and after a call to a wrapped object or even to prevent its execution, depending on the circumstances. As a result, we can change the operation of the packaged object without directly modifying it. | ||
| + | |||
| + | Decorators are used in: | ||
| + | |||
| + | * the validation of arguments; | ||
| + | * the modification of arguments; | ||
| + | * the modification of returned objects; | ||
| + | * the measurement of execution time; | ||
| + | * message logging; | ||
| + | * thread synchronization; | ||
| + | * code refactorization; | ||
| + | * caching. | ||
| + | |||
| + | == Function decorators | ||
| + | Let's analyze some examples before we get down to the next dose of theory. | ||
| + | |||
| + | So, let's create a function – '' | ||
| + | |||
| + | <code python> | ||
| + | def simple_hello(): | ||
| + | print(" | ||
| + | </ | ||
| + | |||
| + | <code python> | ||
| + | def simple_decorator(function): | ||
| + | print(' | ||
| + | return function | ||
| + | </ | ||
| + | |||
| + | The last lines are responsible for both method invocations: | ||
| + | <code python> | ||
| + | decorated = simple_decorator(simple_hello) | ||
| + | decorated() | ||
| + | </ | ||
| + | The whole code should look like the code presented in the right pane. | ||
| + | |||
| + | When you run the code, the result should be: | ||
| + | <code ; output> | ||
| + | We are about to call " | ||
| + | Hello from simple function! | ||
| + | </ | ||
| + | |||
| + | Now let's create another function, '' | ||
| + | |||
| + | We have created a simple decorator – a function which accepts another function as its only argument, prints some details, and returns a function or other callable object. | ||
| + | |||
| + | Well … you could say … we wrote so many lines of code just to print two other lines? Where is the simplicity or convenience in this approach? | ||
| + | |||
| + | Well … we could say … you should look at the following syntactic sugar: | ||
| + | |||
| + | As you can see, the definition of the '' | ||
| + | |||
| + | This means that: | ||
| + | |||
| + | * operations are performed on object names; | ||
| + | * **this is the most important thing to remember**: the name of the simple_function object ceases to indicate the object representing our '' | ||
| + | |||
| + | The implementation of the decorator pattern introduces this syntax, which appears to be very important and useful to developers. That is why decorators have gained great popularity and are widely used in Python code. It should be mentioned that decorators are very useful for refactoring or debugging the code. | ||
| + | |||
| + | <code python> | ||
| + | def simple_decorator(function): | ||
| + | print(' | ||
| + | return function | ||
| + | |||
| + | |||
| + | @simple_decorator | ||
| + | def simple_hello(): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | simple_hello() | ||
| + | </ | ||
| + | <code ; output> | ||
| + | We are about to call " | ||
| + | Hello from simple function! | ||
| + | </ | ||
| + | |||
| + | == Decorators should be universal | ||
| + | Consider a function that accepts arguments and should also be decorated. Decorators, which should be universal, must support any function, regardless of the number and type of arguments passed. In such a situation, we can use the *args and %%**%%kwargs concepts. We can also employ a closure technique to persist arguments. | ||
| + | |||
| + | The code presented in the right pane shows how the decorator can handle the arguments of the function being decorated. | ||
| + | <code python> | ||
| + | def simple_decorator(own_function): | ||
| + | |||
| + | def internal_wrapper(*args, | ||
| + | print('" | ||
| + | print(' | ||
| + | own_function(*args, | ||
| + | print(' | ||
| + | |||
| + | return internal_wrapper | ||
| + | |||
| + | |||
| + | @simple_decorator | ||
| + | def combiner(*args, | ||
| + | print(" | ||
| + | |||
| + | combiner(' | ||
| + | </ | ||
| + | |||
| + | The output is: | ||
| + | <code ; output> | ||
| + | " | ||
| + | (' | ||
| + | {' | ||
| + | |||
| + | Hello from the decorated function; received arguments: (' | ||
| + | Decorator is still operating | ||
| + | </ | ||
| + | Arguments passed to the decorated function are available to the decorator, so the decorator can print them. This is a simple example, as the arguments were just printed, but not processed further. | ||
| + | |||
| + | A nested function (internal_wrapper) could reference an object (own_function) in its enclosing scope thanks to the closure. | ||
| + | |||
| + | == Decorators can accept their own attributes | ||
| + | In Python, we can create a decorator with arguments. Let’s create a program in which the decorator will be more generic – we’ll allow you to pass the packing material in the argument. | ||
| + | |||
| + | See the code presented in the right pane. | ||
| + | |||
| + | <code python> | ||
| + | def warehouse_decorator(material): | ||
| + | def wrapper(our_function): | ||
| + | def internal_wrapper(*args): | ||
| + | print('< | ||
| + | our_function(*args) | ||
| + | print() | ||
| + | return internal_wrapper | ||
| + | return wrapper | ||
| + | |||
| + | |||
| + | @warehouse_decorator(' | ||
| + | def pack_books(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | @warehouse_decorator(' | ||
| + | def pack_toys(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | @warehouse_decorator(' | ||
| + | def pack_fruits(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | pack_books(' | ||
| + | pack_toys(' | ||
| + | pack_fruits(' | ||
| + | </ | ||
| + | |||
| + | The '' | ||
| + | |||
| + | Note that our decorator is enriched with one more function to make it able to handle arguments at all call levels. | ||
| + | |||
| + | The '' | ||
| + | |||
| + | * the '' | ||
| + | * the returned wrapper function will take the function it is supposed to decorate as an argument; | ||
| + | * the wrapper function will return the internal_wrapper function, which adds new functionality (material display) and runs the decorated function. | ||
| + | |||
| + | <code ; output> | ||
| + | < | ||
| + | We'll pack books: (' | ||
| + | |||
| + | < | ||
| + | We'll pack toys: (' | ||
| + | |||
| + | < | ||
| + | We'll pack fruits: (' | ||
| + | </ | ||
| + | |||
| + | The biggest advantage of decorators is now clearly visible: | ||
| + | |||
| + | * we don’t have to change every ' | ||
| + | * we just have to add a simple one liner in front of each function definition. | ||
| + | |||
| + | == Decorator stacking | ||
| + | Python allows you to apply multiple decorators to a callable object (function, method or class). | ||
| + | |||
| + | The most important thing to remember is the order in which the decorators are listed in your code, because it determines the order of the executed decorators. When your function is decorated with multiple decorators: | ||
| + | <code python> | ||
| + | @outer_decorator | ||
| + | @inner_decorator | ||
| + | def function(): | ||
| + | pass | ||
| + | |||
| + | abcd = subject_matter_function() | ||
| + | </ | ||
| + | the call sequence will look like the following: | ||
| + | * the outer_decorator is called to call the inner_decorator, | ||
| + | * when your function ends it execution, the inner_decorator takes over control, and after it finishes its execution, the outer_decorator is able to finish its job. | ||
| + | |||
| + | This routing mimics the classic stack concept. | ||
| + | |||
| + | The syntactic sugar presented above is the equivalent of the following nested calls: | ||
| + | <code python> | ||
| + | subject_matter_function = outer_decorator(inner_decorator(subject_matter_function()))) | ||
| + | abcd = subject_matter_function() | ||
| + | </ | ||
| + | |||
| + | It’s less readable than a simple call to your function, isn't it? | ||
| + | |||
| + | Another advantage becomes clear when you think about the number of modifications you should add to gain the same functionality, | ||
| + | |||
| + | In the right pane, you'll find a real example of stacked decorators. | ||
| + | |||
| + | <code python> | ||
| + | def big_container(collective_material): | ||
| + | def wrapper(our_function): | ||
| + | def internal_wrapper(*args): | ||
| + | our_function(*args) | ||
| + | print('< | ||
| + | print() | ||
| + | return internal_wrapper | ||
| + | return wrapper | ||
| + | |||
| + | def warehouse_decorator(material): | ||
| + | def wrapper(our_function): | ||
| + | def internal_wrapper(*args): | ||
| + | our_function(*args) | ||
| + | print('< | ||
| + | return internal_wrapper | ||
| + | return wrapper | ||
| + | |||
| + | @big_container(' | ||
| + | @warehouse_decorator(' | ||
| + | def pack_books(*args): | ||
| + | print(" | ||
| + | |||
| + | @big_container(' | ||
| + | @warehouse_decorator(' | ||
| + | def pack_toys(*args): | ||
| + | print(" | ||
| + | |||
| + | @big_container(' | ||
| + | @warehouse_decorator(' | ||
| + | def pack_fruits(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | pack_books(' | ||
| + | pack_toys(' | ||
| + | pack_fruits(' | ||
| + | </ | ||
| + | |||
| + | We’ve created two decorators: | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | We’ve also created functions for packaging different kinds of items, each decorated with two decorators. | ||
| + | |||
| + | This example demonstrates that packaging functions are called simply (and could be called many times in different places in your code) and every time those functions' | ||
| + | |||
| + | <code ; output> | ||
| + | We'll pack books: (' | ||
| + | < | ||
| + | < | ||
| + | |||
| + | We'll pack toys: (' | ||
| + | < | ||
| + | < | ||
| + | |||
| + | We'll pack fruits: (' | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | |||
| + | == Lab | ||
| + | === Objectives | ||
| + | * Improving the student' | ||
| + | === Scenario | ||
| + | * Create a function decorator that prints a timestamp (in a form like year-month-day hour: | ||
| + | * Create a few ordinary functions that do some simple tasks, like adding or multiplying two numbers. | ||
| + | * Apply your decorator to those functions to ensure that the time of the function executions can be monitored. | ||
| + | |||
| + | === Hint | ||
| + | To print the current time, you could use the following code: | ||
| + | |||
| + | <code python> | ||
| + | # import module responsible for time processing | ||
| + | from datetime import datetime | ||
| + | |||
| + | # get current time using now() method | ||
| + | timestamp = datetime.now() | ||
| + | |||
| + | # convert timestamp to human-readable string, following passed pattern: | ||
| + | string_timestamp = timestamp.strftime(' | ||
| + | |||
| + | print(string_timestamp) | ||
| + | </ | ||
| + | |||
| + | === resposta | ||
| + | <code python> | ||
| + | # import module responsible for time processing | ||
| + | from datetime import datetime | ||
| + | |||
| + | |||
| + | def multiplica_decorator(funcio): | ||
| + | def internal_wrapper(*args, | ||
| + | timestamp = datetime.now() | ||
| + | string_timestamp = timestamp.strftime(' | ||
| + | print(string_timestamp) | ||
| + | funcio(*args, | ||
| + | | ||
| + | return internal_wrapper | ||
| + | |||
| + | @multiplica_decorator | ||
| + | def multiplica(a, | ||
| + | print(a*b) | ||
| + | | ||
| + | multiplica(2, | ||
| + | </ | ||
| + | |||
| + | == Decorating functions with classes | ||
| + | A decorator does not have to be a function. In Python, it could be a class that plays the role of a decorator as a function. | ||
| + | |||
| + | We can define a decorator as a class, and in order to do that, we have to use a '' | ||
| + | |||
| + | Our previous example code: | ||
| + | |||
| + | <code python> | ||
| + | def simple_decorator(own_function): | ||
| + | |||
| + | def internal_wrapper(*args, | ||
| + | print('" | ||
| + | print(' | ||
| + | own_function(*args, | ||
| + | print(' | ||
| + | |||
| + | return internal_wrapper | ||
| + | </ | ||
| + | | ||
| + | could be transcribed to the code presented on the right. Run it to see the output and compare it to the output of the previously retrieved output. | ||
| + | |||
| + | <code python> | ||
| + | class SimpleDecorator: | ||
| + | def __init__(self, | ||
| + | self.func = own_function | ||
| + | |||
| + | def __call__(self, | ||
| + | print('" | ||
| + | print(' | ||
| + | self.func(*args, | ||
| + | print(' | ||
| + | |||
| + | |||
| + | @SimpleDecorator | ||
| + | def combiner(*args, | ||
| + | print(" | ||
| + | |||
| + | |||
| + | combiner(' | ||
| + | </ | ||
| + | |||
| + | A short explanation of special methods: | ||
| + | * the '' | ||
| + | * the '' | ||
| + | |||
| + | The advantage of this approach, when compared to decorators expressed with functions, is: | ||
| + | |||
| + | * classes bring all the subsidiarity they can offer, like inheritance and the ability to create dedicated supportive methods. | ||
| + | |||
| + | <code ; output> | ||
| + | " | ||
| + | (' | ||
| + | {' | ||
| + | |||
| + | Hello from the decorated function; received arguments: (' | ||
| + | Decorator is still operating | ||
| + | </ | ||
| + | |||
| + | == Decorators with arguments | ||
| + | Another previously discussed snippet showed that decorators can accept arguments: | ||
| + | <code python> | ||
| + | def warehouse_decorator(material): | ||
| + | def wrapper(our_function): | ||
| + | def internal_wrapper(*args): | ||
| + | print(' | ||
| + | our_function(*args) | ||
| + | print() | ||
| + | return internal_wrapper | ||
| + | return wrapper | ||
| + | @warehouse_decorator(' | ||
| + | def pack_books(*args): | ||
| + | print(" | ||
| + | </ | ||
| + | And this code could be transcribed to a decorator expressed as a class, presented in the right pane. | ||
| + | <code python> | ||
| + | class WarehouseDecorator: | ||
| + | def __init__(self, | ||
| + | self.material = material | ||
| + | |||
| + | def __call__(self, | ||
| + | def internal_wrapper(*args, | ||
| + | print('< | ||
| + | own_function(*args, | ||
| + | print() | ||
| + | return internal_wrapper | ||
| + | |||
| + | |||
| + | @WarehouseDecorator(' | ||
| + | def pack_books(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | @WarehouseDecorator(' | ||
| + | def pack_toys(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | @WarehouseDecorator(' | ||
| + | def pack_fruits(*args): | ||
| + | print(" | ||
| + | |||
| + | |||
| + | pack_books(' | ||
| + | pack_toys(' | ||
| + | pack_fruits(' | ||
| + | |||
| + | </ | ||
| + | |||
| + | When you pass arguments to the decorator, the decorator mechanism behaves quite differently than presented in example of decorator that does not accept arguments (previous slide): | ||
| + | * the reference to function to be decorated is passed to '' | ||
| + | * the decorator arguments are passed to '' | ||
| + | |||
| + | == Class decorators | ||
| + | Class decorators strongly refer to function decorators, because they use the same syntax and implement the same concepts. | ||
| + | |||
| + | Instead of wrapping individual methods with function decorators, class decorators are ways to manage classes or wrap special method calls into additional logic that manages or extends instances that are created. | ||
| + | |||
| + | If we consider syntax, class decorators appear just before the ' | ||
| + | |||
| + | The simplest use can be presented as follows: | ||
| + | <code python> | ||
| + | @my_decorator | ||
| + | class MyClass: | ||
| + | |||
| + | obj = MyClass() | ||
| + | |||
| + | </ | ||
| + | and it is adequate for the following snippet: | ||
| + | <code python> | ||
| + | def my_decorator(A): | ||
| + | ... | ||
| + | |||
| + | class MyClass: | ||
| + | ... | ||
| + | |||
| + | MyClass = my_decorator(MyClass()) | ||
| + | |||
| + | obj = MyClass() | ||
| + | </ | ||
| + | Like function decorators, the new (decorated) class is available under the name ' | ||
| + | |||
| + | Now we’ll talk about a class decorated with a function that allows us to monitor the fact that some code gets access to the class object attributes. When you’re debugging your code or optimizing it, you might be curious how many times the object attributes are accessed. In such a situation, a class decorator might be handy. | ||
| + | |||
| + | Let's create a simple class representing a car. Each object should own two attributes: mileage and VIN, and it should be possible to read the values of those attributes. | ||
| + | <code python> | ||
| + | class Car: | ||
| + | def __init__(self, | ||
| + | self.mileage = 0 | ||
| + | self.VIN = VIN | ||
| + | |||
| + | car = Car(' | ||
| + | print(' | ||
| + | print(' | ||
| + | </ | ||
| + | |||
| + | Now let's create a function that will decorate a class with a method that issues alerts whenever the ' | ||
| + | |||
| + | <code python> | ||
| + | def object_counter(class_): | ||
| + | class_.__getattr__orig = class_.__getattribute__ | ||
| + | |||
| + | def new_getattr(self, | ||
| + | if name == ' | ||
| + | print(' | ||
| + | return class_.__getattr__orig(self, | ||
| + | |||
| + | class_.__getattribute__ = new_getattr | ||
| + | return class_ | ||
| + | |||
| + | </ | ||
| + | |||
| + | Look at the code in the editor. Let's analyze it: | ||
| + | * line 1: '' | ||
| + | * line 2: '' | ||
| + | * line 4: '' | ||
| + | * line 5: '' | ||
| + | * line 6: '' | ||
| + | * line 7: '' | ||
| + | * line 9: '' | ||
| + | * line 10: '' | ||
| + | |||
| + | The last thing we should do is decorate the Car class: | ||
| + | <code python> | ||
| + | @object_counter | ||
| + | class Car: | ||
| + | </ | ||
| + | When you run the code, you can see that access to the ' | ||
| + | <code ; output> | ||
| + | We noticed that the mileage attribute was read | ||
| + | The mileage is 0 | ||
| + | The VIN is ABC123 | ||
| + | </ | ||
| + | |||
| + | == Decorators – summary | ||
| + | A decorator is a very powerful and useful tool in Python, because it allows programmers to modify the behavior of a function, method, or class. | ||
| + | |||
| + | Decorators allow us to wrap another callable object in order to extend its behavior. | ||
| + | |||
| + | Decorators rely heavily on closures and *args and %%**kwargs%%. | ||
| + | |||
| + | Interesting note: | ||
| + | |||
| + | * the idea of decorators was described in two documents – PEP 318 and PEP 3129. Don't be discouraged that the first PEP was prepared for Python 2, because what matters here is the idea, not the implementation in a specific Python. | ||