Advanced Python - Decorators
Finkel feedback on Yu’s original draft
Advanced Python - 1. Decorators
1.1 Functions as First Class Objects
1.2 Introduction to Decorators
1.3 Use the @ symbol to apply the decorator
1.5 Decorating Functions With Arguments
1.6 Returning Values From Decorated Functions
1.8 Common Use Cases for Decorators
1. Decorators
1.1 Functions as First Class Objects
(might need to make 1.1 into its own chapter - Call it “Advanced Functions”)
Before diving deep into decorators, let us understand some concepts that will come in handy in learning the decorators. In Python, functions are first class objects, meaning they can be used or passed as arguments.
(brief one line explanation/review of what an argument is)
(review of anatomy of a function. IN/OUT/parameters/return type/parentheses)
Properties of first class functions:
(are we talking about first class objects or first class functions? Difference?)
(need many examples of all of the following - Build out this material for quizzes and assignments)
- A function is an instance of the Object type. (what does this really mean?)
- You can store the function in a variable.
- You can pass the function as a parameter to another function.
- You can return a function from a function.
- You can store them in data structures such as lists, dictionaries, etc.
Example #1: Treating Functions as Objects
def shout(text): print(shout('Have a good day!')) # Output: HAVE A GOOD DAY! |
|---|
Example #2: Passing the Function as an Argument
def say_hello(name): (adding space for clarity) greet_lucy(say_hello) # Output: Hello Lucy |
|---|
Note that greet_lucy(say_hello) refers to two functions, greet_lucy() and say_hello. say_hello function is named without parentheses, meaning that only a reference to the function is passed. (Might need even a more detailed walkthrough)
Example #3: Returning functions from another function (needs an explanation)
def create_adder(x): print(add_10(5)) # Output: 15 |
|---|
TO BE REVIEWED:
1.2 Introduction to Decorators
A decorator in Python is a special type of function that is used to modify the behavior of another function or method. It’s like adding extra features or functionality to a function without changing its actual code.
Example #4: Simple decorator
def my_decorator(func): |
|---|
Can you guess what happens when you call say_whee()?
say_whee() # Output: |
|---|
To understand what’s going on here, look back at the previous examples. We are literally just applying everything you have learned so far.
The so-called decoration happens at the following line:
say_whee = my_decorator(say_whee) |
|---|
In effect, the name say_whee now points to the wrapper() inner function. Remember that you return wrapper as a function when you call my_decorator(say_whee)
print(say_whee) # Output: |
|---|
However, wrapper() has a reference to the original say_whee() as func, and calls that function between the two calls to print().
Put simply: decorators wrap a function, modifying its behavior.
1.3 Use the @ symbol to apply the decorator
In the previous section, we manually apply the decorator. We could also use the @ symbol to apply a decorator to a function. This is called “syntactic sugar” because it makes the code easier to read and write. In this case, @my_decorator is a shorter way of saying say_whee = my_decorator(say_whee).
1.4 Reusing Decorators
Using decorators in Python also ensures that your code is DRY(Don't Repeat Yourself). You could create a module where you store your decorators and that you can use in other functions.
Let’s create a file named decorators.py with the following content:
# decorators.py |
|---|
Next, create another file named main.py where you want to use the do_twice decorator.
# main.py # Output: # Whee! # Whee! |
|---|
This modular approach allows you to reuse the do_twice decorator in multiple places by simply importing it, making your code more organized and maintainable.
Note: You can name your inner function whatever you want, and a generic name like wrapper() is usually okay.
1.5 Decorating Functions With Arguments
Let’s say you have a function that takes an argument and you want to use do_twice decorator. What will happen with this code?
# main.py |
|---|
This will raise an error:
TypeError: do_twice.<locals>.wrapper_do_twice() got an unexpected keyword argument 'name' |
|---|
This is because our inner function wrapper_do_twice() doesn’t accept any arguments, yet we passed name="World" to it. One way to resolve this would be to allow wrapper_do_twice() to accept a single argument. However, this solution would cause it to fail when used with the say_whee() function created earlier, which does not require any arguments.
The solution is to use *args and **kwargs in the inner wrapper function. Then it will accept an arbitrary number of positional and keyword arguments.
Positional arguments are arguments that are passed to a function in a specific order.
Example #5: Positional arguments
def greet(name, age): |
|---|
In this example, “Alice” is the first positional argument assigned to name, and 16 is the second positional argument assigned to age.
Keyword arguments are passed to a function by explicitly stating the name of the parameter along with its value. The order of keyword arguments does not matter.
Example #6: Keyword arguments
def greet(name, age): |
|---|
Let’s rewrite decorators.py as follows:
def do_twice(func): |
|---|
The wrapper_do_twice() inner function now accepts any number of arguments and passes them on to the function it decorates.
Then update main.py as follows:
# main.py # Output: # Whee! |
|---|
Now you can use @do_twice to decorate different functions. This is one of the powers of decorators.
1.6 Returning Values From Decorated Functions
What if we decorate a function with a return value? Take a look at the following example and guess the output.
Example #7
# main.py # Output: # Creating greeting! # Creating greeting! # None |
|---|
Oops, the return value disappears.This is because wrapper_do_twice() does not explicitly return a value. As a result, calling return_greeting("Adam") ends up returning None.
To fix this, you need to make sure the wrapper function returns the return value of the decorated function. Change your decorators.py file:
def do_twice(func): |
|---|
1.7 Chaining Decorators
Chaining decorators means decorating a function with multiple decorators. Let’s take a look at the decorator order in the following example.
Example #8:
@decorator1 |
|---|
Example #9: Changing order of decorators
# decorators.py |
|---|
Now we use these decorators in main.py.
# main.py @exclaim def greet(name): return f"hello, {name}" print(greet("world")) # Output: HELLO, WORLD! @exclaim @uppercase def greet(name): return f"hello, {name}" print(greet("world")) # Output: HELLO, WORLD!! |
|---|
Chaining decorators in Python involves applying multiple decorators to a single function, allowing for modular and reusable code enhancements.
1.8 Common Use Cases for Decorators
(1)Authorization and Logging: One practical use case is in web applications where you need to check if a user is authorized to access a resource and then log the access. For example, you can chain an authorization decorator with a logging decorator to ensure that authorization is checked before logging the access:
Example #10:
def authorize(func): |
|---|
(2) Validation
Chaining decorators can also be used for input validation to ensure that function arguments meet certain criteria before execution.
Example #11:
def validate_positive(func): print(square(5)) # Output: 25 print(square(-5)) # Output: Raises ValueError: Input must be a positive value. print(square(5.5)) # Output: Raises ValueError: Input must be an integer. |
|---|
(3) Formatting and Timing
Decorators can be used to format the output of a function, such as converting it to JSON, adding HTML tags, or applying any other formatting.
Example #12:
import time # Output: # Execution time: 1.1920928955078125e-06 seconds # Result: 30 |
|---|
In conclusion, Chaining decorators in Python allows for clean, modular, and reusable code, enhancing the functionality of functions in a structured manner.
Assignments
A1.1 Basic Decorator
Create a decorator named print_args that prints the arguments passed to a function. Apply print_args to a function add(a,b) that returns the sum of a and b. Test the function with print(add(3,6)).
Expected output:
Arguments: (3, 6), {} |
|---|
Follow-up Question: why is there an empty dictionary {} in the output? Think of it.
Solution:
def print_args(func): |
|---|
A1.2 Watering plants
Create a decorator named repeat_task that takes an argument n and repeats the execution of the decorated function n times. Apply repeat_task(n) to a function water_plants() that prints “Watering plants''. Test it with different n values.
Solution:
def repeat_task(times): # Output: # Watering plants # Watering plants # Watering plants |
|---|
A1.3 Baking Cake
Create a decorator named time_it that measures the time taken by a function to execute and prints the duration. Apply time_it to a function bake_cake() that simulates baking a cake by sleeping for 2 seconds.
Hints:
- You could use the example code in 1.6 Chaining Decorators for reference.
- To mimic the sleeping status, use time.sleep(2).
Expected Output:
Baking cake... |
|---|
Solution:
import time |
|---|
A1.4 Order Coffee
You're developing a system for a coffee shop where customers can customize their coffee orders through a digital interface. Your task is to implement a flexible coffee customization system using decorators. Each decorator should update both the name and the price of the coffee.The final function should return a dictionary with the complete coffee name and total price.
Instructions for decorators:
- add_milk(milk_type): Adds specified type of milk (e.g., 'whole', 'skim', 'almond') and increases the price by $0.50.
- add_sugar(spoons): Adds the specified number of sugar spoons and increases the price by $0.25 per spoon.
- add_syrup(flavor): Adds syrup of the specified flavor and increases the price by $0.75.
Hints: add_milk(milk_type) allows us to create a decorator that takes a parameter. Before you start to code, consider the structure of your decorator, what information do you need when applying the decorator? And think about the layers of functions, for example, which function should accept the function to be decorated?
Example Usage:
@add_milk("almond") |
|---|
Expected Output:
{'name': 'Coffee with vanilla syrup, 2 spoons of sugar, and almond milk', 'price': 4.25} |
|---|
Solution:
def add_milk(milk_type): |
|---|
Project
P1.1 Password Validation
Create a password validation system using decorators. The system will ensure that passwords meet the following criteria before they are accepted. Simply printing a message such as “Password must have at least 8 characters” is okay.
1. Minimum Length: The password must be at least 8 characters long.
2. Uppercase Letter: The password must contain at least one uppercase letter.
3. Lowercase Letter: The password must contain at least one lowercase letter.
4. Digit: The password must contain at least one digit.
Test your implementation with different passwords to ensure all validation rules are enforced.
Example Usage:
passwords = [ |
|---|
Expected output:
Testing password: short |
|---|
Solution:
def min_length(func): |
|---|