[0:00] Have you ever seen an at sign on top of [0:02] a function definition before? This is [0:05] decorator syntax in action. [0:08] A decorator like timer deck here is [0:11] itself a function. The purpose of that [0:14] function is to decorate or enhance a [0:17] base function that we pass as an [0:18] argument and return the enhanced [0:21] function. [0:22] By writing at timer deck on top of the [0:26] definition of brew t, we tell Python [0:29] that the base function brew t must be [0:31] enhanced by the decorator timer deck [0:34] before it is used. After receiving the [0:37] base function as an input, the decorator [0:40] bundles it with additional features [0:42] without modifying the base functions [0:44] original code. [0:46] Once these new features are added, the [0:48] decorator returns the enhanced version [0:50] of the function. [0:52] This enhanced version of the function is [0:54] what Python will use whenever brew tea [0:56] is called. In a nutshell, this is how [1:00] decorators work. [1:03] Okay, but what's the point? Why use [1:06] decorators to add on extra code when we [1:08] could simply include the additional [1:10] operations in the original function [1:12] definition? [1:14] Let's see why by developing our tea [1:16] brewing example a bit. [1:18] We'll begin by writing the body of the [1:20] brew tea function. [1:22] The function simply prints brewing tea, [1:25] pauses for a second, and then prints tea [1:29] is ready. Of course, the execution of [1:32] this function will take about 1 second. [1:34] But suppose we want to know the exact [1:37] runtime. [1:38] To do this, we could add lines of code [1:41] to record the start time of a function [1:43] call, the end time of the function call, [1:47] and print the difference between the [1:49] two. Running the code, the function does [1:52] calculate the execution time. [1:55] But there are issues with this approach. [1:58] First, the brew tea function violates [2:01] the single responsibility principle by [2:03] performing two distinct tasks. Brewing [2:06] tea and timing the process. [2:09] In programming, functions should focus [2:11] on a single well-defined responsibility [2:14] to make code reusable. [2:16] In this example, by combining brewing [2:18] and timing, we can't easily reuse the [2:21] timing logic in another function. [2:24] For example, suppose we also have a [2:27] matcha making function [2:30] and we want to time it as well. We could [2:34] do this by rewriting the timing code [2:37] from the brew tea function, [2:39] but duplicating code is not ideal since [2:42] it makes our code base repetitive and [2:44] harder to maintain. [2:46] Decorators offer a great solution to [2:49] these problems. Let's see how. For [2:53] simplicity, we'll first decorate the [2:54] brew tea function. We'll bring back make [2:57] matcha later. Since decorators are just [3:00] functions, to create one, we'll start [3:02] with the keyword deaf. [3:05] In this example, the purpose of our [3:07] decorator is to time the execution of a [3:09] function. So, we'll name it timer deck. [3:13] Remember that decorators take a base [3:15] function as an input. [3:18] So we'll define a parameter called base [3:20] function to represent that input [3:22] function. [3:24] This completes the header of the [3:25] decorator. Let's move to the body. Since [3:28] the goal of a decorator is to build [3:30] [music] an enhanced function and return [3:33] it. [3:35] Let's define that enhanced function. [3:38] The enhanced function will use the base [3:40] function as its foundation. So it should [3:42] include a call to base function. [3:45] From there, we can add the code we want [3:47] to execute before and after the base [3:50] function call. To measure the execution [3:53] time of the base function, we'll record [3:56] the start time before the call, [3:59] the end time after the call, [4:02] and then print the difference. [4:04] And that's it. We've created a decorator [4:06] that takes a base function as an input [4:09] and adds the operations to measure its [4:11] execution time. [4:13] The last step is to tell our decorator [4:15] to return the enhanced function. [4:18] Note that this return statement is part [4:20] of the decorator, not part of the [4:22] enhanced function. [4:24] Great. Now, let's use our timer [4:26] decorator to measure how long the focus [4:28] brew [music] t function takes to run. To [4:32] apply decorators in Python, we have two [4:34] options. [4:36] First, we can call the decorator and [4:38] pass the function we want to decorate as [4:40] an argument. [4:42] Running the code, we get a function [4:43] object. [4:45] This function object is the enhanced [4:47] function that the timer deck returned. [4:51] To call this function later in the [4:52] program, we can give it a name by [4:54] assigning it to a variable, say [music] [4:57] deck brew tea. Calling deck brew tea [5:00] will brew tea and time the execution. [5:04] In contrast, calling brew tea still just [5:07] brews tea. [5:09] Note that when passing a base function [5:11] to a decorator, we simply write the name [5:14] of the function. We do not follow the [5:16] name with parenthesis. [5:18] Adding the parenthesis would immediately [5:20] trigger a bruty function call, which is [5:23] not what we want here. It's also [5:26] important to note when applying [5:27] decorators, we could reassign the [5:29] decorated function back to the original [5:32] function name. [5:33] This approach ensures that every call to [5:36] the original function name brew tea [5:39] automatically includes the timer [5:41] functionality. [5:43] This is quite powerful. [5:45] By using a decorator, we've added [5:47] features to brew tea without modifying [5:49] the original code. [5:52] But at the same time, applying [5:53] decorators this way separates the [5:55] decoration from the function definition. [5:59] This makes it less obvious to someone [6:00] reading this code or to our future [6:03] selves that the base brew function will [6:05] be enhanced. [6:07] Luckily, we can address this drawback [6:09] with the at decorator syntax that we [6:11] mentioned in the beginning. Writing this [6:14] above a function is equivalent to [6:16] applying the decorator manually. [6:19] Running the code, we see that a call to [6:21] brew tea both brews tea and times the [6:24] process. [6:26] The at decorator syntax makes the [6:29] decoration an explicit and visible part [6:31] of the function definition itself. [6:34] For that reason, this syntax is [6:36] generally how decorators are applied in [6:39] Python. [6:40] Now that we have seen how decorators [6:42] help us write focus functions, let's see [6:44] how they also help us reuse code. To do [6:48] this, we'll bring back the make matcha [6:50] function to time it. [6:54] Now that we've defined the timer [6:55] decorator, timing the make matcha [6:57] function is easy. [6:59] We simply write at timer deck above the [7:02] function header and call the function. [7:06] Running the code, we see that a call to [7:08] make matcha both makes matcha and times [7:11] the process. [7:13] Perfect. The at syntax lets us easily [7:16] reuse the decorator and clearly show [7:18] which functions are enhanced. [7:21] But so far we've only decorated simple [7:24] functions that don't have parameters. [7:27] To decorate functions that do have [7:29] parameters, we need to do a bit more [7:31] work. To give ourselves some space to [7:34] work, let's fold the make matcha [7:36] function [7:37] to see how to decorate functions with [7:39] parameters. We'll modify our brew tea [7:42] function to take two arguments. The type [7:44] of tea to brew and how long it should [7:46] steep. We'll use these values inside our [7:49] function to update the first print [7:51] statement and the sleep function. [7:55] Then we'll update our brew tea function [7:57] call to pass the t type green and a [8:00] steep time of one as arguments. [8:04] Now that our brew tea function takes [8:05] arguments, running the code generates an [8:08] error. This error tells us the function [8:11] that we've called takes zero arguments, [8:13] but we gave it two. Wait, what? Haven't [8:17] we defined brew tea to take two [8:19] arguments? [8:21] Well, we did, but remember when we [8:24] decorate brew tea with the timer [8:26] decorator, calling brew tea actually [8:30] triggers a call to the enhanced function [8:32] defined in the decorator [8:35] and enhanced function currently doesn't [8:37] take any arguments. [8:40] We could try to fix this issue by [8:42] creating type and steep time parameters [8:45] in the enhanced function and passing [8:48] those into a base function call. Running [8:51] the code, this fix worked for the brew t [8:53] call, but we get another error when [8:56] Python tries to execute make matcha. [8:59] This happens because we've now specified [9:01] the enhanced function must take exactly [9:04] two arguments. [9:06] The problem is make matcha doesn't take [9:09] any arguments and let's suppose we don't [9:11] want it to. Can we make our decorator [9:14] flexible enough for both functions? [9:17] The answer is yes. But we'll need to [9:19] modify the enhance function header. [9:22] Again, the issue with the current header [9:24] is that it specifies exactly two [9:27] parameters. [9:28] In Python, we can relax this requirement [9:31] and allow a function to take any number [9:33] of inputs using star args and doublestar [9:36] kW args. If you aren't familiar with [9:39] star args and its sister syntax, [9:41] doublestar kws, make sure to check out [9:44] our video linked in the description [9:46] first. To allow enhanced function to [9:49] take any number of positional arguments, [9:51] we replace the explicit type and steep [9:54] time parameters with star args. [9:57] By writing star args in the function [9: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.