In this video, I'll be discussing an advanced topic in Python called decorators. Now, decorators are a way to change or modify the behavior of any of your functions or methods without directly changing any of the code. They're extremely powerful, have a lot of different use cases, and throughout this video, we'll go through a few different examples and discuss when and why you might use a decorator. That being said, we'll also discuss how they work on a low level by understanding some fundamental concepts in Python, and then we'll talk about creating our own and using them properly. But before we dive in, I want to show you a tool to turbocharge your Python programming called Kite. Now, Kite is a free plug-in for your IDE or text editor that uses machine learning to give you the best possible completions for your Python code. It's capable of completing entire lines, entire function calls, and it ranks all of its completions by relevance so you get shown the best ones first. It even has another feature called intelligent snippets, which allows you to quickly tab through the different options and choose which completion you'd like. One of the coolest features that comes with Kite is called Copilot. Now, what Copilot does is provide one-click documentation. It shows you information about modules, classes, methods, and functions based on your cursor location. Now, the best part of Kite is that it's free and you can download it at the link below.
So before we can start to understand decorators, we need to learn about a fundamental aspect of functions in Python, and that is that they can be represented as objects. Now, I'm going to show you an example to see how this works, but I want to quickly illustrate that I have a function here called func1, and we know that when calling func1, what will happen is we'll simply print out "called func1". Very standard, makes a lot of sense. We all understand how that works. Now, what I'm actually going to do is rather than printing func1 or calling func1, I'm going to print the function name func1 and not put the brackets beside it. Now, some of you might have done this before accidentally, but let's have a look at what happens. You can see that we get out here some output, function func1 at some random memory address. Now, what this actually means is func1 is an object, and since it's an object, we can actually pass it around our program. Now, this is very powerful and I'm going to show you exactly what I mean. So I'm going to create a new function called func2, and inside here, it's going to take an argument called func. What I'm going to do is simply call whatever this argument is, func. And now what I'm going to do is call func2, and instead of simply putting in, you know, some value for func, I'm actually going to put the function func1. Now, take a guess at what's going to happen here. What are we going to output? Is this an error? Is this correct? Well, since we can represent functions as objects, this is actually going to work properly. And when I look at my output here, we can see that we get "called func1". Now, the way that this worked fundamentally is func1 is an object that represents this function. Since it's an object, we can pass it through parameters, we can store it in variables, we can do all kinds of things with it. So when I call func2, I'm going to pass through the object representing function one. Then I'm simply going to call that function from inside of func2 and I will get that value returned or outputted to my screen. And that is the basic principle of functions in Python that we need to understand to understand decorators.
So now that we understand that functions are objects and they can be passed through parameters, what I want to show you is another really interesting aspect of functions, and that is something called wrapper functions. So what I'm going to do is actually define a function. In this case, I'm going to call it func, like we've had before, that takes as its value another function or as its parameter. So just like we have previously, we're going to take another function. But inside of this function, I'm going to do something very interesting. And this is called creating a wrapper function. Now, what this wrapper function is actually going to do is simply print out some value, let's say maybe "Started". It's actually going to call the function that we grabbed here from the first parameter in our initial function, and it's going to print out another value here that just says "Ended". So essentially, what this, you know, whole function is going to do is, well, it has another function defined inside of it called wrapper. It's going to print "Started", it's going to call this function, and then once that function finishes running, it's going to call "Ended" or print "Ended". Now, what I need to do to actually trigger this wrapper function and have it work properly is, well, call it. So what I could do is I could call wrapper just like this from inside the actual function. But this isn't actually what I want to do. What I want to do is return a value called wrapper. So what I'm going to do now is whenever I call function one, I'm going to pass some function to it, then what I'm going to do is actually return another function that has this functionality inside of it, that essentially prints "Started", runs the function, and then prints "Ended". Now, you might be able to kind of figure out what's going on here, but I'm going to show you exactly what I mean. So let's create another function. Let's just call it func2. And inside here, let's simply print out "Hello". Now, what I want to do is every time I call this function func2, I want it to do this function one functionality. I want it to, you know, say, print "Started", print "Hello" for this function, and then print "Ended". Well, how can I do this? Well, what I can do is I can say something like func and then in here, pass func2. And watch what actually is going to end up happening when I do this. Nothing. That's pretty strange. Why did that happen? Well, what do we get here when we have func? What we do is we create a function and we return another function. We don't actually end up calling this function inside here, so that means we never actually end up calling the function func2. How can we call the function func2? Well, this is going to be a really weird thing, but watch when I show you this. I'm actually going to put another set of brackets here. And when I run the program now, you see this changes our functionality and our output actually says "Started", "Hello", and "Ended". Very weird, I know, but the way to think of it here is that this value wrapper is a function. So when I actually return the value func func2, and I'll print it out so we can actually have a look at exactly what this is, you're going to see that we get the function func.<locals>.wrapper at some memory address, which means that it's telling us, you know, when you call this function, you got back a value which was actually equal to another function. So if we want to call that other function, well, we know how to call functions. We need to put our two brackets at the end of it like that. Now, this property is what's actually going to allow us to what we call decorate a function. So what I can do is if I want to say, every time I call func2, I want it to do the functionality of func, which essentially means, you know, do this, call function, do this, I can say something like func2 = func(func2). Now, this is really another strange line, but I'll just show you how this actually works. So when I run this, and we call func2, we can see that rather than simply printing "Hello", we're actually printing now "Started", "Hello", and "Ended". And that's because I've set func2, which is a variable, equal to the function func with the parameter func2 passed into it. So what happens when I call func2 is we actually call func, giving it the value of our function, which is func2 right here. Then we have print "Started", func, print "Ended". Now, I could easily change this name to be anything I want. I could change it to be x, and I now can call x like this. And you can see when I run this, we get the same result. And this is what we call function aliasing, essentially changing the name of a function and changing the functionality. Now, you might be like, "Well, what the heck? I thought this video was on decorators and we haven't even talked really about decorators yet." Well, if you understand everything that I've just said, you pretty much understand decorators. So this line right here, x = func(func2) can be replaced with a nicer thing called, well, decorators. So what I can actually do is above my function func2, I can decorate it with the function func. What this is going to do is essentially write this line in Python for us that's automatically going to execute, which means every time we call func2, it's actually going to call func and pass the function func2 as a parameter to that. So let's try this now and let's call func2. And we can see we get the same result we had before, and now we no longer need to include that line that says func2 = func(func2), or, you know, func2 inside of there.
So I've made a slight modification to my code now to show you something wrong with the current way we've been doing things, and that is notice what happens when I add a parameter to my function func2. So this is the decorated function func2, and now rather than printing "Hello", we're printing a, and you can see we call func2 with the value "hi". Now, notice immediately that up here in our wrapper function, we call this function func, which is actually function, right? And it doesn't have any parameters, and this wrapper function doesn't have any parameters as well. So how can we actually go about fixing this? Now, this is where we can use something called *args and **kwargs. Now, you've probably seen this before. It's a fairly commonly used thing in the Python language. I'm not really going to explain exactly how these work, but I will give you a brief explanation. So what I'm going to do is actually put *args and **kwargs in the parameters of my wrapper function, and I'm going to copy this and put the same thing inside of my function here. Now, the reason I'm doing this is so that we know that this wrapper function will have a certain amount of arguments. We don't know what those arguments are going to be. We don't know if there's any keyword arguments or regular in-place arguments. All we know is that we will have some kind of arguments. And same thing goes for whatever function is going to be wrapped by this function func. We have no idea what arguments to expect or what kwargs are going to be passed to that specific function. So what we simply do is put *args, **kwargs. Now, this will actually allow us to have any amount of arguments on any specific decorated function and this will work properly. So you can see I actually ran this code before when I didn't have those **kwargs or *args, and we say, you know, "wrapper takes 0 positional arguments but 1 was given." So watch what happens now if I actually run this code. We get the same value that we expected before, and we get this value "hi" printed out. And if I decide to add another argument, maybe we say something like b=9, and then I'll put, um, just print b here as well. When I run this, this continues to work and there's no issues with it, and that's what this *, so args and kwargs is going to do for us.
So the final thing to talk about, and something that I'm hoping you guys were actually looking at, is returning values from decorated functions. Now, so far, all I've been doing inside of some of my functions is printing out values. But what if I want to return those actual values? Well, this is pretty easy to do, and I'm going to make another example by creating a new function. So in this case, I'm going to create a function called add, which is going to take two parameters, x, y. Now, what it's going to do is simply return the summation of these, so x + y, and what I need to do now is find a way that I can modify this wrapper function such that it's actually going to return that value. Well, what some of you might say is, "Well, why don't I just do something like this? Return and then whatever the function call is." In that way, when I return this wrapper function, well, we can simply return that value. Well, that's okay, but what's going to end up happening is we'll print "Started" and then we'll simply finish execution after we return the value of this function. So if there's things that we need to happen after the function is called, those won't happen. So what I need to do, and this is pretty easy, is simply store the value returned by the function in a variable, and then return that variable at the end of my wrapped function, or my wrapper function. So here, we get val equals whatever the value that was returned by this specific function that was passed in, and then what we'll do is return that value at the end of this. So this wrapper function that we return here, when we decorate, will work like this original function and actually return to us that value. So if I decorate func and now what I do is decide to print out add of 4 and 5, we should see that we get our value 9, which we do. We get "Started", "Ended", 9, like we were assuming. So that is essentially how you return values from a, uh, decorated function. Pretty straightforward. Just make sure you keep track of this value inside of this wrapper function and return it at the end. And that being said, you don't necessarily need to return this specific value. If you always want to return one value from a decorated function, you could just hard code that and put that in here. This also illustrated that you can use a decorator on more than one function at a time, and when you call it, it will work just like any other function that is decorated with that.
So that has been the basics of decorators. I'm going to quickly go through a few examples just to show you in case any of you are still confused or you want to see when we would actually end up using these in a real-world situation. So I'm just going to run through a few quick examples now to illustrate when you might actually use these decorators and some different ways of using them. So first of all, I just wanted to show with this example that you actually can use a decorator for a method. So I've been showing specifically functions, but you can do this with a method in the same way. We create our decorator up here. We can see that I've actually only taken positional arguments for this wrapper because I knew that it would only be positional arguments. That's fine to do that if you want to. And then I print "before", print the function, and print "after". Or in this case, I guess it will be the method. So if I run this, we can see that we get the same functionality that we would have assumed, and that works for a method.
So moving onto example two, and this is the timer decorator example. Very common. Probably a lot of you have seen this before, but in many cases, you want to record how long it takes for a function or a method to run, and that's when you would use something like this timer decorator. So without dragging this on too long, I will run this and show you what happens. You can see that inside of this function, I've delayed 2 seconds, and now it's telling me the function took 2. blank whatever seconds to run. Very useful if you're trying to compare the run time of different algorithms or methods and you want a really quick way that you can just decorate a function and it tells you how long it takes to run.
And finally, onto example three. So this is the most complicated example I've written here just to show you a few different things you can do with these kind of decorators, and this is a log decorator. So what this is actually going to do is tell you, uh, when you've called a function with what specific arguments, so in this case, it's actually going to be keyword arguments, and at what time, and save that into a log file. So sometimes you want to record when specific things in your program happen, so maybe you want to record every time the run function is run. Well, in that case, you could decorate it with @log, and if we run this, we can see that inside our logs.txt file, it says "called function with 1, 3 at", and then in this case, the time that it was called at. So again, a very basic example of a way in which you can use a decorator.
So that has been how to use decorators in Python. A fairly advanced topic, if you guys understood this, you're definitely well on your way to becoming an expert in Python. You may have noticed that throughout this video, I've been using the autocomplete engine called Kite. Now, Kite works as a plug-in for your IDE or text editor and is available for the most popular editors like VS Code, PyCharm, Sublime, Atom, and Vim. You can give it a try by downloading it from the link in the description. It's definitely saved me a ton of time by providing better, more relevant suggestions that help me cut down on the amount of keystrokes I need to use while I'm coding. So with that being said, I hope you all enjoyed the video and you learned something new, and I'd love to hear your thoughts on it in the comments down below.