What is a Python Decorator?
34sVisually explains the mysterious @ syntax that many beginners find confusing.
▶ Play ClipThis video explains Python decorators, which are functions that enhance other functions without modifying their original code. It demonstrates how decorators promote code reuse and adhere to the single responsibility principle.
The @ symbol above a function definition is decorator syntax, used to enhance a function.
Adding timing code directly into a function violates the single responsibility principle and makes code less reusable.
A decorator is a function that takes a base function, defines an inner enhanced function, and returns it.
Decorators can be applied manually by calling the decorator with the function as an argument, or using @ syntax above the function definition.
The @ syntax allows easy reuse of decorators across multiple functions, as shown with brew_tea and make_matcha.
To decorate functions with different numbers of arguments, use *args to accept any number of positional arguments and unpack them when calling the base function.
Use **kwargs to accept any number of keyword arguments, unpacking them with ** when calling the base function.
To support return values, capture the result of the base function call and return it from the enhanced function.
Decorators are a powerful Python feature that allow you to add functionality to functions in a clean, reusable way. By using *args and **kwargs, you can create flexible decorators that work with any function signature.
"The title accurately describes the content: a visual explanation of Python decorators."
What is a decorator in Python?
A decorator is a function that takes another function as an argument, adds functionality, and returns the enhanced function.
What is the syntax to apply a decorator to a function?
Use the @ symbol followed by the decorator name above the function definition.
Why should you avoid adding extra functionality directly inside a function?
It violates the single responsibility principle and makes code less reusable.
1:58
What does *args do in a function definition?
It packs all positional arguments into a tuple named args.
9:57
What does *args do in a function call?
It unpacks a tuple into separate positional arguments.
10:53
What does **kwargs do in a function definition?
It packs all keyword arguments into a dictionary named kwargs.
12:17
What does **kwargs do in a function call?
It unpacks a dictionary into separate keyword arguments.
13:03
How do you make a decorator work with functions that return values?
Capture the return value of the base function call and return it from the inner function.
14:44
Decorator Syntax
Introduces the @ syntax for decorators, a key Python feature.
Single Responsibility Principle
Explains why mixing concerns in a function is problematic.
1:58Creating a Decorator
Step-by-step demonstration of building a timer decorator.
2:46Reusing Decorators
Shows how decorators promote code reuse across functions.
6:40Using *args for Flexibility
Teaches how to make decorators work with any number of positional arguments.
9:49Using **kwargs for Flexibility
Extends decorators to handle keyword arguments.
12:14Handling Return Values
Completes the decorator to support functions that return values.
14:44[00:00] Have you ever seen an at sign on top of
[00:02] a function definition before? This is
[00:05] decorator syntax in action.
[00:08] A decorator like timer deck here is
[00:11] itself a function. The purpose of that
[00:14] function is to decorate or enhance a
[00:17] base function that we pass as an
[00:18] argument and return the enhanced
[00:21] function.
[00:22] By writing at timer deck on top of the
[00:26] definition of brew t, we tell Python
[00:29] that the base function brew t must be
[00:31] enhanced by the decorator timer deck
[00:34] before it is used. After receiving the
[00:37] base function as an input, the decorator
[00:40] bundles it with additional features
[00:42] without modifying the base functions
[00:44] original code.
[00:46] Once these new features are added, the
[00:48] decorator returns the enhanced version
[00:50] of the function.
[00:52] This enhanced version of the function is
[00:54] what Python will use whenever brew tea
[00:56] is called. In a nutshell, this is how
[01:00] decorators work.
[01:03] Okay, but what's the point? Why use
[01:06] decorators to add on extra code when we
[01:08] could simply include the additional
[01:10] operations in the original function
[01:12] definition?
[01:14] Let's see why by developing our tea
[01:16] brewing example a bit.
[01:18] We'll begin by writing the body of the
[01:20] brew tea function.
[01:22] The function simply prints brewing tea,
[01:25] pauses for a second, and then prints tea
[01:29] is ready. Of course, the execution of
[01:32] this function will take about 1 second.
[01:34] But suppose we want to know the exact
[01:37] runtime.
[01:38] To do this, we could add lines of code
[01:41] to record the start time of a function
[01:43] call, the end time of the function call,
[01:47] and print the difference between the
[01:49] two. Running the code, the function does
[01:52] calculate the execution time.
[01:55] But there are issues with this approach.
[01:58] First, the brew tea function violates
[02:01] the single responsibility principle by
[02:03] performing two distinct tasks. Brewing
[02:06] tea and timing the process.
[02:09] In programming, functions should focus
[02:11] on a single well-defined responsibility
[02:14] to make code reusable.
[02:16] In this example, by combining brewing
[02:18] and timing, we can't easily reuse the
[02:21] timing logic in another function.
[02:24] For example, suppose we also have a
[02:27] matcha making function
[02:30] and we want to time it as well. We could
[02:34] do this by rewriting the timing code
[02:37] from the brew tea function,
[02:39] but duplicating code is not ideal since
[02:42] it makes our code base repetitive and
[02:44] harder to maintain.
[02:46] Decorators offer a great solution to
[02:49] these problems. Let's see how. For
[02:53] simplicity, we'll first decorate the
[02:54] brew tea function. We'll bring back make
[02:57] matcha later. Since decorators are just
[03:00] functions, to create one, we'll start
[03:02] with the keyword deaf.
[03:05] In this example, the purpose of our
[03:07] decorator is to time the execution of a
[03:09] function. So, we'll name it timer deck.
[03:13] Remember that decorators take a base
[03:15] function as an input.
[03:18] So we'll define a parameter called base
[03:20] function to represent that input
[03:22] function.
[03:24] This completes the header of the
[03:25] decorator. Let's move to the body. Since
[03:28] the goal of a decorator is to build
[03:30] [music] an enhanced function and return
[03:33] it.
[03:35] Let's define that enhanced function.
[03:38] The enhanced function will use the base
[03:40] function as its foundation. So it should
[03:42] include a call to base function.
[03:45] From there, we can add the code we want
[03:47] to execute before and after the base
[03:50] function call. To measure the execution
[03:53] time of the base function, we'll record
[03:56] the start time before the call,
[03:59] the end time after the call,
[04:02] and then print the difference.
[04:04] And that's it. We've created a decorator
[04:06] that takes a base function as an input
[04:09] and adds the operations to measure its
[04:11] execution time.
[04:13] The last step is to tell our decorator
[04:15] to return the enhanced function.
[04:18] Note that this return statement is part
[04:20] of the decorator, not part of the
[04:22] enhanced function.
[04:24] Great. Now, let's use our timer
[04:26] decorator to measure how long the focus
[04:28] brew [music] t function takes to run. To
[04:32] apply decorators in Python, we have two
[04:34] options.
[04:36] First, we can call the decorator and
[04:38] pass the function we want to decorate as
[04:40] an argument.
[04:42] Running the code, we get a function
[04:43] object.
[04:45] This function object is the enhanced
[04:47] function that the timer deck returned.
[04:51] To call this function later in the
[04:52] program, we can give it a name by
[04:54] assigning it to a variable, say [music]
[04:57] deck brew tea. Calling deck brew tea
[05:00] will brew tea and time the execution.
[05:04] In contrast, calling brew tea still just
[05:07] brews tea.
[05:09] Note that when passing a base function
[05:11] to a decorator, we simply write the name
[05:14] of the function. We do not follow the
[05:16] name with parenthesis.
[05:18] Adding the parenthesis would immediately
[05:20] trigger a bruty function call, which is
[05:23] not what we want here. It's also
[05:26] important to note when applying
[05:27] decorators, we could reassign the
[05:29] decorated function back to the original
[05:32] function name.
[05:33] This approach ensures that every call to
[05:36] the original function name brew tea
[05:39] automatically includes the timer
[05:41] functionality.
[05:43] This is quite powerful.
[05:45] By using a decorator, we've added
[05:47] features to brew tea without modifying
[05:49] the original code.
[05:52] But at the same time, applying
[05:53] decorators this way separates the
[05:55] decoration from the function definition.
[05:59] This makes it less obvious to someone
[06:00] reading this code or to our future
[06:03] selves that the base brew function will
[06:05] be enhanced.
[06:07] Luckily, we can address this drawback
[06:09] with the at decorator syntax that we
[06:11] mentioned in the beginning. Writing this
[06:14] above a function is equivalent to
[06:16] applying the decorator manually.
[06:19] Running the code, we see that a call to
[06:21] brew tea both brews tea and times the
[06:24] process.
[06:26] The at decorator syntax makes the
[06:29] decoration an explicit and visible part
[06:31] of the function definition itself.
[06:34] For that reason, this syntax is
[06:36] generally how decorators are applied in
[06:39] Python.
[06:40] Now that we have seen how decorators
[06:42] help us write focus functions, let's see
[06:44] how they also help us reuse code. To do
[06:48] this, we'll bring back the make matcha
[06:50] function to time it.
[06:54] Now that we've defined the timer
[06:55] decorator, timing the make matcha
[06:57] function is easy.
[06:59] We simply write at timer deck above the
[07:02] function header and call the function.
[07:06] Running the code, we see that a call to
[07:08] make matcha both makes matcha and times
[07:11] the process.
[07:13] Perfect. The at syntax lets us easily
[07:16] reuse the decorator and clearly show
[07:18] which functions are enhanced.
[07:21] But so far we've only decorated simple
[07:24] functions that don't have parameters.
[07:27] To decorate functions that do have
[07:29] parameters, we need to do a bit more
[07:31] work. To give ourselves some space to
[07:34] work, let's fold the make matcha
[07:36] function
[07:37] to see how to decorate functions with
[07:39] parameters. We'll modify our brew tea
[07:42] function to take two arguments. The type
[07:44] of tea to brew and how long it should
[07:46] steep. We'll use these values inside our
[07:49] function to update the first print
[07:51] statement and the sleep function.
[07:55] Then we'll update our brew tea function
[07:57] call to pass the t type green and a
[08:00] steep time of one as arguments.
[08:04] Now that our brew tea function takes
[08:05] arguments, running the code generates an
[08:08] error. This error tells us the function
[08:11] that we've called takes zero arguments,
[08:13] but we gave it two. Wait, what? Haven't
[08:17] we defined brew tea to take two
[08:19] arguments?
[08:21] Well, we did, but remember when we
[08:24] decorate brew tea with the timer
[08:26] decorator, calling brew tea actually
[08:30] triggers a call to the enhanced function
[08:32] defined in the decorator
[08:35] and enhanced function currently doesn't
[08:37] take any arguments.
[08:40] We could try to fix this issue by
[08:42] creating type and steep time parameters
[08:45] in the enhanced function and passing
[08:48] those into a base function call. Running
[08:51] the code, this fix worked for the brew t
[08:53] call, but we get another error when
[08:56] Python tries to execute make matcha.
[08:59] This happens because we've now specified
[09:01] the enhanced function must take exactly
[09:04] two arguments.
[09:06] The problem is make matcha doesn't take
[09:09] any arguments and let's suppose we don't
[09:11] want it to. Can we make our decorator
[09:14] flexible enough for both functions?
[09:17] The answer is yes. But we'll need to
[09:19] modify the enhance function header.
[09:22] Again, the issue with the current header
[09:24] is that it specifies exactly two
[09:27] parameters.
[09:28] In Python, we can relax this requirement
[09:31] and allow a function to take any number
[09:33] of inputs using star args and doublestar
[09:36] kW args. If you aren't familiar with
[09:39] star args and its sister syntax,
[09:41] doublestar kws, make sure to check out
[09:44] our video linked in the description
[09:46] first. To allow enhanced function to
[09:49] take any number of positional arguments,
[09:51] we replace the explicit type and steep
[09:54] time parameters with star args.
[09:57] By writing star args in the function
[09:59] definition, we're telling Python to pack
[10:01] all positional arguments into a tuple
[10:04] named args.
[10:06] This means that when the brew function
[10:08] is called, the arguments green and one
[10:11] are passed to the enhance function and
[10:13] packed into the args tpple. To use these
[10:17] arguments in the base function call, it
[10:19] might be tempting to simply pass the
[10:21] args tpple.
[10:23] However, this won't work because the
[10:25] args tpple itself counts as a single
[10:28] argument [music] and our base function
[10:30] expects two.
[10:32] Instead, what we need to do is split the
[10:34] tpple into multiple arguments.
[10:37] We can do this by applying the unpacking
[10:40] operator star to the args tpple. While
[10:44] the star arg syntax looks the same in
[10:46] both the function definition and the
[10:49] function call, Python does something
[10:51] different in each case.
[10:53] When used in a function call, the star
[10:56] operator unpacks a tpple into separate
[10:59] positional arguments instead of packing
[11:02] positional arguments into a tpple. Since
[11:05] our args tpple contains green and one,
[11:08] these values will be unpacked into two
[11:11] separate arguments.
[11:13] Now the number of arguments in the base
[11:15] function call matches the number of
[11:17] parameters in brew tea. Better yet,
[11:21] since enhance function accepts a
[11:23] flexible number of arguments, the timer
[11:25] decorator will now also work with the
[11:27] make matcha function.
[11:30] Because the make matcha call passes no
[11:32] arguments, the arc tpple is empty. So
[11:36] when it's time to unpack the tpple in
[11:38] the base function call, there's nothing
[11:40] to unpack.
[11:42] This is a good thing since make matcha
[11:44] has no parameters.
[11:46] Running the code, both functions work as
[11:49] expected.
[11:50] However, if we stop here, our decorator
[11:53] isn't as flexible as it could be. For
[11:56] example, if in the brew tea function
[11:59] call, we instead pass type and steep
[12:02] time as keyword arguments,
[12:05] our code breaks again.
[12:08] To fix this, we need to modify enhance
[12:10] function to also accept keyword
[12:12] arguments.
[12:14] We can do this by adding doublestar kws
[12:17] as a parameter.
[12:19] In this example, doublestar kws tells
[12:23] Python to pack all keyword arguments
[12:25] into a dictionary named kwarks.
[12:29] So when the brew t function is called
[12:32] the keyword arguments t type equals
[12:34] green and steep time equals 1 are passed
[12:38] into the enhanced function.
[12:40] At this point they are all packed into
[12:42] the kws [music]
[12:43] dictionary as key value pairs.
[12:47] To use these arguments we need to pass
[12:49] them into the base function.
[12:52] Like with args we can't simply pass the
[12:54] kws dictionary itself. we need to split
[12:57] it into two separate arguments using the
[13:00] dictionary unpacking operator double
[13:03] star. When used in a function call, the
[13:06] double star operator unpacks a
[13:08] dictionary [music] into separate keyword
[13:10] arguments.
[13:11] So during the brew tea function call
[13:14] when the kw args dictionary contains
[13:16] these key value pairs doublestar kws
[13:20] splits up the dictionary into keyword
[13:22] arguments typed equals green and steep
[13:25] time equals 1. These keyword arguments
[13:28] match up with the parameters defined in
[13:30] the brew t header.
[13:33] Running the code, the keyword arguments
[13:35] are now accepted.
[13:37] By using star args and double star kW
[13:40] args, we've made our timer decorator
[13:42] more general and flexible,
[13:45] it can take any number of positional or
[13:47] keyword arguments and forward them into
[13:49] the base function.
[13:51] At this point, the only thing preventing
[13:53] our decorator from becoming fully
[13:55] general and flexible is how it currently
[13:57] handles return values or more accurately
[14:00] doesn't handle them.
[14:03] For example, suppose we want our make
[14:05] matcha function to calculate and return
[14:07] the optimal time window to drink the
[14:10] matcha which is 30 minutes after it's
[14:12] prepared.
[14:14] We can do this by first importing the
[14:16] datetime libraries date time and time
[14:19] delta modules.
[14:22] modify our make matcha function to
[14:24] return a string that says drink matcha
[14:26] by now plus 30 minutes and print make
[14:30] matcha's output.
[14:32] However, running the cell, we get none
[14:35] instead of a drink by time. This happens
[14:38] because our decorator doesn't yet
[14:40] capture and pass along the return value
[14:42] from the base function.
[14:44] To fix this, we need to modify our
[14:46] decorator to capture the return value
[14:48] from base function in a variable. then
[14:51] return that variable at the end of
[14:53] enhanced function.
[14:55] Running the cell again, we now get the
[14:58] exact time by which we should drink our
[15:00] matcha. And our decorator still works
[15:03] with brew tea even though it doesn't
[15:05] return a value. Awesome. Now our
[15:08] decorator is as flexible and general as
[15:11] possible.
[15:13] It can decorate functions with any
[15:14] number of positional or keyword
[15:16] arguments thanks to star args and double
[15:19] star kws.
[15:22] It also supports functions that return
[15:24] values without breaking when used with
[15:26] functions that don't.
[15:28] If you'd like to practice what you've
[15:30] learned in this video, check out the
[15:32] notebook we created. It [music] has a
[15:33] few exercises to get you started.
[15:36] We're working on lots more Python
[15:38] explainer videos like this one, so be
[15:40] sure to subscribe so that you don't miss
[15:41] out. If you have any questions or topics
[15:43] that you'd like to learn about, let us
[15:45] know in the comments below. We'd love to
[15:47] hear from you. Thanks for watching.
⚡ Saved you time reading this? Transcribe any YouTube video for free — no signup needed.