In this lecture, we'll introduce the concept of functions, critical abstractions in nearly every modern programming language. Functions are important for abstracting and categorizing large codebases into smaller, logical, and human-digestable components. By the end of this lecture, you should be able to:
A function in Python is not very different from a function as you've probably learned since algebra.
"Let $f$ be a function of $x$"...sound familiar? We're basically doing the same thing here.
A function ($f$) will [usually] take something as input ($x$), perform some kind of operation on it, and then [usually] return a result ($y$). Which is why we usually see $f(x) = y$. A function, then, is composed of three main components:
1: The function itself. A [good] function will have one very specific task it performs. This task is usually reflected in its name. Take the examples of print
, or sqrt
, or exp
, or log
; all these names are very clear about what the function does.
2: Arguments (if any). Arguments (or parameters) are the input to the function. It's possible a function may not take any arguments at all, but often at least one is required. For example, print
has 1 argument: a string.
3: Return values (if any). Return values are the output of the function. It's possible a function may not return anything; technically, print
does not return anything. But common math functions like sqrt
or log
have clear return values: the output of that math operation.
A core tenet in writing functions is that functions should do one thing, and do it well (with apologies to the Unix Philosophy).
Writing good functions makes code much easier to troubleshoot and debug, as the code is already logically separated into components that perform very specific tasks. Thus, if your application is breaking, you usually have a good idea where to start looking.
It's very easy to get caught up writing "god functions": one or two massive functions that essentially do everything you need your program to do. But if something breaks, this design is very difficult to debug.
You've probably heard the term "method" before, in this class. Quite often, these two terms are used interchangeably, and for our purposes they are pretty much the same.
BUT. These terms ultimately identify different constructs, so it's important to keep that in mind. Specifically:
Otherwise, functions and methods work identically.
So how do we write functions? At this point in the course, you've probably already seen how this works, but we'll go through it step by step regardless.
First, we define the function header. This is the portion of the function that defines the name of the function, the arguments, and uses the Python keyword def
to make everything official:
In [1]:
def our_function():
pass
That's everything we need for a working function! Let's walk through it:
def
keyword: required before writing any function, to tell Python "hey! this is a function!"pass
: since Python is sensitive to whitespace, we can't leave a function body blank; luckily, there's the pass
keyword that does pretty much what it sounds like--no operation at all, just a placeholder.Admittedly, our function doesn't really do anything interesting. It takes no parameters, and the function body consists exclusively of a placeholder keyword that also does nothing. Still, it's a perfectly valid function!
In [2]:
# Call the function!
our_function()
# Nothing happens...no print statement, no computations, nothing. But there's no error either...so, yay?
numpy.array()
functionality is indeed a function. When a function is in a module, to call it you need to prepend the name of the module (and any submodules), add a dot ".
" between the module names, and then call the function as you normally would.from
keyword during import:
In [2]:
from numpy import array
Now the array()
method can be called directly without prepending the package name numpy
in front. USE THIS CAUTIOUSLY: if you accidentally name a variable array
later in your code, you will get some very strange errors!
Arguments (or parameters), as stated before, are the function's input; the "$x$" to our "$f$", as it were.
You can specify as many arguments as want, separating them by commas:
In [3]:
def one_arg(arg1):
pass
def two_args(arg1, arg2):
pass
def three_args(arg1, arg2, arg3):
pass
# And so on...
Like functions, you can name the arguments anything you want, though also like functions you'll probably want to give them more meaningful names besides arg1
, arg2
, and arg3
. When these become just three functions among hundreds in a massive codebase written by dozens of different people, it's helpful when the code itself gives you hints as to what it does.
When you call a function, you'll need to provide the same number of arguments in the function call as appear in the function header, otherwise Python will yell at you.
In [4]:
try:
one_arg("some arg")
except Exception as e:
print("one_arg FAILED: {}".format(e))
else:
print("one_arg SUCCEEDED")
try:
two_args("only1arg")
except Exception as e:
print("two_args FAILED: {}".format(e))
else:
print("two_args SUCCEEDED")
To be fair, it's a pretty easy error to diagnose, but still something to keep in mind--especially as we move beyond basic "positional" arguments (as they are so called in the previous error message) into optional arguments.
"Positional" arguments--the only kind we've seen so far--are required. If the function header specifies a positional argument, then every single call to that functions needs to have that argument specified.
There are cases, however, where it can be helpful to have optional, or default, arguments. In this case, when the function is called, the programmer can decide whether or not they want to override the default values.
You can specify default arguments in the function header:
In [5]:
def func_with_default_arg(positional, default = 10):
print("'{}' with default arg {}".format(positional, default))
func_with_default_arg("Input string")
func_with_default_arg("Input string", default = 999)
If you look through the NumPy online documentation, you'll find most of its functions have entire books' worth of default arguments.
The numpy.array
function we've been using has quite a few; the only positional (required) argument for that function is some kind of list/array structure to wrap a NumPy array around. Everything else it tries to figure out on its own, unless the programmer explicitly specifies otherwise.
In [6]:
import numpy as np
x = np.array([1, 2, 3])
y = np.array([1, 2, 3], dtype = float) # Specifying the data type of the array, using "dtype"
print(x)
print(y)
Notice the decimal points that follow the values in the second array! This is NumPy's way of showing that these numbers are floats, not integers!
In this example, NumPy detected that our initial list contained integers, and we see in the first example that it left the integer type alone. But, in the second example, we override its default behavior in determining the data type of the elements of the resulting NumPy array. This is a very powerful mechanism for occasionally tweaking the behavior of functions without having to write entirely new ones.
Let's do one more small example before moving on to return values. Let's build a method which prints out a list of video games in someone's Steam library.
In [7]:
def games_in_library(username, library):
print("User '{}' owns: ".format(username))
for game in library:
print(game)
print()
games_in_library('fps123', ['DOTA 2', 'Left 4 Dead', 'Doom', 'Counterstrike', 'Team Fortress 2'])
games_in_library('rts456', ['Civilization V', 'Cities: Skylines', 'Sins of a Solar Empire'])
games_in_library('smrt789', ['Binding of Isaac', 'Monaco'])
In this example, our function games_in_library
has two positional arguments: username
, which is the Steam username of the person, and library
, which is a list of video game titles. The function simply prints out the username and the titles they own.
Just as functions [can] take input, they also [can] return output for the programmer to decide what to do with.
Almost any function you will ever write will most likely have a return value of some kind. If not, your function may not be "well-behaved", aka sticking to the general guideline of doing one thing very well.
There are certainly some cases where functions won't return anything--functions that just print things, functions that run forever (yep, they exist!), functions designed specifically to test other functions--but these are highly specialized cases we are not likely to encounter in this course. Keep this in mind as a "rule of thumb."
To return a value from a function, just use the return
keyword:
In [8]:
def identity_function(in_arg):
return in_arg
x = "this is the function input"
return_value = identity_function(x)
print(return_value)
This is pretty basic: the function returns back to the programmer as output whatever was passed into the function as input. Hence, "identity function."
Anything you can pass in as function parameters, you can return as function output, including lists:
In [9]:
def explode_string(some_string):
list_of_characters = []
for index in range(len(some_string)):
list_of_characters.append(some_string[index])
return list_of_characters
words = "Blahblahblah"
output = explode_string(words)
print(output)
This function takes a string as input, uses a loop to "explode" the string, and returns a list of individual characters.
(it should be noted this entire function can be replaced by one line: output = list(words)
, but it serves well as an illustration that you can pass in to and return from functions any data types you'd like)
You can even return multiple values simultaneously from a function. They're just treated as tuples!
In [3]:
import numpy.random as r
def list_to_tuple(inlist):
return r.randint(0, 100), inlist
print(list_to_tuple([1, 2, 3]))
print(list_to_tuple(["one", "two", "three"]))
This two-way communication that functions enable--arguments as input, return values as output--is an elegant and powerful way of allowing you to design modular and human-understandable code.
1: You're a software engineer for a prestigious web company named after a South American rain forest. You've been tasked with rewriting their web-based shopping cart functionality for users who purchase items through the site. Without going into too much detail, quickly list out a handful of functions you'd want to write with their basic arguments. Again, no need for excessive detail; just consider the workflow of navigating an online store and purchasing items with a shopping cart, and identify some of the key bits of functionality you'd want to write standalone functions for, as well as the inputs and outputs of those functions.
2: From where do you think the term "positional argument" gets its name?
3: In NumPy you have a lot of math-oriented utility functions, like numpy.log
, numpy.exp
, numpy.cos
, and so on. Describe in words (in terms of functions, their inputs, and their return values) how the code in this line works: x = numpy.log(numpy.exp(numpy.cos(100.0)))
4: Go back to the explode_string
example in Cell 9 above. Rewrite that loop in the form of a list comprehension (throwback review question! hashtag "trq").
5: Write a function, grade
, which accepts a positional argument number
(floating point) and returns a letter grade version of it ("A", "B", "C", "D", or "F"). Include a second, default argument that is a string and indicates whether there should be a "+", "-", or no suffix to the letter grade (default is no suffix).