The Basics of Python Inner Functions
Here are some resources for more information about topics covered in this lesson:
00:00 In the previous lesson, I gave an overview of the course. In this lesson, I will introduce you to inner functions. Let’s jump right into some code. The simplest version of an inner function is merely a function defined inside of another function. In the REPL, I’ll do just this.
00:19 That’s the outer function,
00:23 and then right inside of it, I’ve defined another function, the inner function.
00:32 The inner function will print a message,
00:38 and then, because I’ve out-dented, I’m back in the context of the outer function, printing a different message. Inside of the outer function, I can call the inner function, and that’s all I’m going to do in this function.
00:52 Let’s see it work. I called the outer function, and you see the message from the outer and then from the inner. To prove that the inner function is in a different scope, I’ll attempt to call it directly. That doesn’t work.
01:09 The global namespace, at this point, only contains the outer function. The inner function is only within the scope of the outer function, the containing function.
01:19 Inner functions have access to the variables in the scope of their containing functions. Let me create another function to demonstrate.
01:39
The function called inside()
has access to everything within the mug()
function’s scope, including its calling arguments. When I call mug()
, the f-string within the inside()
function can use the stuff
argument from mug()
and print it out. Let’s take a look at a more practical example.
02:01 In case you haven’t used the concept before, a recursive function is one that calls itself. This is a common pattern in computer science for doing computation where a result is based on a previous result in a sequence.
02:14 It is also a useful tool for working with data trees. For more information on recursion, see the articles linked in the details below. The code in the top area here is comprised of two functions.
02:26 The one on line 8 is what you call to calculate a factorial. A factorial is all the numbers from 1 to N multiplied together for any given N. 3 factorial is 1 * 2 * 3.
02:39 Factorials are a perfect use of recursive functions, as each calculation is just the factorial of the number of 1 less than the number. Lines 9 through 13 check that the number being passed in meets the required conditions.
02:54 It has to be an integer and it has to be a positive number. Line 15 calls the function that actually does the work and is the one that will do recursion. Line 6 is where the magic happens. It calls itself.
03:08
This will result in _recurse_factorial()
being called again and again until num
has been reduced to 1
, thus giving you N * N - 1 * N - 2, all the way down to 1, which is the factorial.
03:23 The code I’ve used here, where there are two functions to do the recursion, is a common pattern. You often want to do a bit of work before starting the recursion and don’t want to do that pre-work on every call. In this case, the pre-work is just checking the parameters, but you still don’t want to do it every time.
03:40 To see this in practice, in the lower window I’ll import the function and run it.
03:51
4 factorial, 1 * 2 * 3 * 4, is 24. Although I’ve marked _recurse_factorial()
as private by naming it with a leading underscore (_
), it is available in the module.
04:03 Let’s refactor this code to use an inner function instead.
04:10
This redone function, second_factorial()
, starts out the same as first_factorial()
and has the same purpose. What I’ve done here is moved the private _recurse_factorial()
inside of second_factorial()
, and the rest is the same.
04:26
The advantage of this style of code is that nobody should ever be calling inner_factorial()
except for the containing function. This moves from, “Hey!
04:34 I’m going to signal this as private by naming it specially,” to “You can’t actually see this from outside the module.” This is called encapsulation. Just to show that it’s the same, let me import it and run it as well.
04:52
And there is 24
—a robust, happiness-causing number if there ever was. Choosing whether to use an inner function is a design decision. Let me show you three versions of the same code using different techniques so you can think about the differences. In the top area here, you see process_hotspots()
, a function that reads a CSV file containing data on Wi-Fi hotspots in New York City and prints some summary information. In this version, I’m using an inner function called most_common_provider()
that does the processing on the CSV file.
05:29
This function uses the csv
library to read the file, enumerate over the content in lines 11 and 12, then create a Counter
object based on the Wi-Fi providers.
05:39
Counter
objects are part of the collections
library and are a special instance of sets, where you can add the same thing into it and, instead of just keeping unique values, it also counts how many times a value was added.
05:52 Once the data is ready, it prints out a summary. Let me just scroll down here.
06:00
Similar to the factorial example, some setup is being done here with the argument passed in, then the inner function is called. In this case, the container function’s file
argument is checked to see if it is a string or a file handle. If it is a string, it treats it like a filename, opens the corresponding file, and passes the handle into the inner function.
06:21 If it’s not a string, it assumes it’s a file handle already and passes it in. Let me import the function,
06:32 and then call it with a string.
06:38
There are the results! 3,319 hotspots with LinkNYC being the most common provider. Now I’m going to do it again, this time using a file handle. I’ll use a context manager to open the file and get the handle, named f
.
07:00
Passing f
into process_hotspots()
, and the result is the same as before.
07:09 Here, I’ve refactored the code, removing the inner function and making it a private helper function in the module. Everything else is the same. When should you do this instead of using an inner function?
07:19
It’s a matter of preference and a matter of reuse. If there’s any chance at all that _most_common_provider()
could be used by any function besides process_hotspots()
, then it can’t be an inner function.
07:31 The trade-off here is reuse of the function versus hiding it through encapsulation.
07:41
One last alternative. This is another refactor. Here, I’ve moved the contents of _most_common_provider()
right into the process_hotspots()
function, not encapsulating it in a function at all.
07:53
I’ve also changed how the filename versus file handle code is done. On line 6, I assume it is a handle, then check if it is a string. If so, I open it and overwrite file_obj
. The rest of the function now just uses that file handle. Honestly, of the three versions, this is my preferred one, but it doesn’t have an inner function in it, and that’s what this course is about. Again, the trade-off here is one of reusability. My gut is _most_common_provider()
won’t be reused in its current state, so it doesn’t need special treatment.
08:25 If you were to start adding features to this code, like counting boroughs or area codes in addition to providers, then you’d want to rethink what I’ve done. In all likelihood, you’d also want to separate the output portion from the statistic gathering portion, and you’d be reorganizing a bunch here.
08:40 Writing software is all about trade-offs and crystal balls. You try to see into the future. If you’re right, then there’s less work. If you’re wrong, you’ve either over-engineered or have some refactoring to do.
08:52 I personally try to err on the side of simplicity until other needs arise. Now that you understand inner functions, I can move on to their primary use: closures.
Become a Member to join the conversation.