Have you ever walked into a room where the color scheme was
despair and the layout was chaos?
There are people who can fix this-- decorators.
Have you ever encountered a function that was littered
with boilerplate code and reinvented wheels?
There are tools that can fix this-- decorators.
In Python, a decorator is a function
that sprinkles magic across other functions and classes.
This makes it easy to extract common commands
and pack them together into a single reusable function.
Today, we will redecorate some Python code
and show you how to make your repo a more beautiful place.
A decorator is a command in Python
that starts with an @ symbol.
Decorators are used before functions,
classes,
or methods.
A decorator in front of a function
is called a function decorator.
When a decorator is in front of a class,
we call it a class decorator.
And while not as common, you can put a decorator
in front of a method and call it a method decorator.
Decorators change the behavior of functions and classes
without you having to change the code.
For example, in this lesson,
we will create a decorator called timer,
which will make it easy to measure the execution speed of a function.
The @ notation is an example of syntactic sugar.
Syntactic sugar is not a coffee additive.
It is a programming additive,
syntax that makes code easier to read and write.
You may be thinking: "How does Python perform this trick?"
You may also be thinking: " The correct word is illusion".
A magician never reveals their secrets.
I am not a magician.
So let us go inside Python to learn the secret of decorators.
In Python, functions and classes are first class citizens.
In programming language design, the term “first class citizen”
means anything that can be passed around like regular data types.
For example, in Python, you can create a function called “compose”
where the first argument is a function,
the second argument is a function,
and the third argument is a variable.
We can then call ‘compose’
and pass in the print and length functions, and a string.
When we run..
..the function prints the length of our string.
We passed in 3 arguments: two were functions, one was a string.
You may be asking your monitor, “What does this have to do with decorators?”
Before I answer that question, let us see another clever feature of Python.
Functions in Python are like birds – they can be nested.
Here is a comical yet suitable example.
The function random_power will return one of three functions at random.
A square function, a cube function, or a fourth power function.
Notice that we define three functions inside the random_power function.
These are called nested functions.
We could simply not test this function
and assume it will all work according to plan.
Or we can test it.
Let us test it..10 times.
Each time, get a random_power function and evaluate it at 3.
Run.
I am convinced.
We have established that in Python,
functions are first-class citizens that can be nested.
Now let us use these capabilities to create
a decorator called timer.
Here is a function that computes the prime factorization of a positive integer.
How would you measure the execution speed of this function?
One way would be to import the time module..
Record the time when the function starts..
Record the time when the function stops..
And then print the difference.
Let us test this on a handful of large integers.
Run.
This works.
But this approach to timing is inflexible.
The timing code would have to be added to each function you want to time.
Then when you are done, this extra code would need to be removed.
We will instead use a more flexible and general approach.
First, rewind..
Remove the timing code from the prime factorization function.
Initiating better approach..
Create a function called timer that takes any function f
and returns a new function that includes code for measuring execution time.
The wrapper function is a nested function.
This inner function has two parts.
The first part evaluates f using the arguments and returns the result.
The other lines measure the start and stop times
and then prints the elapsed time.
Finally, we return the wrapper function
like we would return any other value.
This function has it all.
It treats functions as first-class citizens
by accepting a function as an input
and returning a new function as the output.
And there is a nested function.
We can use timer to create a new function called
prime factorization timer.
When we use this new function,
we get both the time required and the output of the prime factorization function.
This is an improvement
because we didn't have to change the original function at all.
Better still,
we can use this timer function on any function.
Win, win.
Plus, plus.
But we still had to write throwaway code.
That increases my sadness level to 2.9.
This is where the decorator syntax saves the day..
and the night.
Put the @timer decorator in front of the prime factorization function.
Now, when you call the prime factorization function,
Python will wrap it in the timer function for you.
Run.
That syntactic sugar is definitely sweet.
Let us see this process again,
but in a more abstract way.
A decorator is a function that accepts a function
and returns a function.
Inside the decorator function, you build a new nested function.
Finally, you return the new function.
When building this nested function,
people often use this notation to handle all possible arguments.
'args' is a tuple of positional arguments.
And 'kwargs' is a dictionary of keyword arguments.
To augment any function with this decorator,
use the @ notation.
Now, whenever you call the function,
Python will actually execute this code.
Here is a different way to see how Python interprets
the decorator notation.
Python has a module named 'functools',
which comes with a collection of function tools.
'func tools'.. 'function tools'.
There is a connection there that eludes my nets.
Some of these tools are decorators, like cache and wraps.
Many people use cache to buy wraps to save time.
And many programmers cache their wraps to save time.
Since time is finite, we will not explore every decorator in this module.
But we will explore these two.
Imagine a simple function that uses the 'catch-all' approach to arguments.
This function will print args and keyword args.
Call this function.
You will see that args contains a tuple of all positional arguments.
'kwargs' is a dictionary with the named arguments.
You can also access the name of the function and the doc string with the name and doc special attributes.
Run.
Now watch what happens if we create a do nothing decorator
and apply it to our function.
Run.
Concern rising .
Our carefully crafted function name and doc string are gone.
We can recover them with the wraps decorator.
Apply it to the inner function with the outer function
as an argument and run.
Better.
By the way, notice the wraps decorator accepts an argument.
This is a very important thing to notice.
It is possible to design a decorator that
accepts arguments to modify the behavior of the decorator.
An argument to modify a decorator that
modifies a class or function.
Can we go deeper?
Should we go deeper?
The cache decorator in the func tools module provides a function with memoization.
This is an optimization technique that caches function calls to avoid repeated calculations.
A classic example for demonstration purposes
is the Fibonacci sequence.
This is the sequence that starts with a pair of ones.
Every additional number is the sum of the previous two numbers.
Here is a recursive function that
computes the n-th term of the Fibonacci sequence.
The first thing to do is to verify the input
is a positive integer.
Next, we handle the first two cases separately
from the subsequent terms.
If n is greater than 2, we compute the term recursively.
Finally, check the first few values to confirm correctness.
Run.
If you try to use our timer decorator
to measure execution speed, you will run into a problem.
Run.
Because the function is recursive,
it displays the time required for each step.
There is an easy workaround.
Define a wrapper function that
calls the Fibonacci function.
And then time the wrapper function.
Run.
The later function calls take an observable amount of time to compute.
We will now use the cache decorator
on the Fibonacci function to see the benefits of caching.
Run.
The speed benefits are clear.
To create a decorator, we wrote a wrapper function
that added new features to a function.
This inner function with additional features
is returned by the wrapper function.
The decorator syntactic sugar transforms this trick
into highly readable code.
But we do not have to stop here.
It is possible to have one function nested inside another
nested inside another.
This is the approach used to create a decorator that accepts an argument.
There is no limit to how deep we can go,
but there are limits on the length of this video.
Whenever you find yourself writing the same code over
and over and over again and over again,
you should stop and ask yourself, am I wrong?
Am I right?
How did I get here?
Where does that highway go to?
Because your code does not have
to be the same as it ever was.
The same as it ever was.