Functions are blocks of code that execute when the function is called by name. If a function is never called, the code never gets executed. If a function is called more than once, then the body of the code can be executed more than once although the programmer only had to code it one time. This ability for code re-use is one of the advantages of using functions. They make your code shorter by reducing repetition.
Functions are called functions because they are similar in concept to functions in math. There a function y = f(x)
maps a value x
to a value y
. A function z = f(x,y)
maps the combination of x
and y
values to a new value z
. The existing values are like input and the new values are like output. Functions take input values or input data and do stuff with them, basicially.
"Do stuff" often means returning a new value like in math class, but functions in programming are more general, for they do not have to return a value. They may instead print output or perform a computation that modifies other data in your program. A function that modifies other data in your program is said to have "side effects". You've seen functions that do not return values before in your Java programming classes; they are called void methods in Java. Value methods in Java are methods that return a value (duh!).
Another way functions are more general in programming is that they also do not have to take input values. This leads us to a more precise definition of a function:
Since functions are blocks of code that get executed when called, that block of code has to be written before the function can be called. The programmer indicates what that block of code is in the function definition. In Python, the def keyword is used to introduce a function definition, and the body of the function is indented with respect to this first line:
def f(): # define a function f that takes no parameters
print("This function returns no value")
# the end of the indented block is the end of the function here ...
f() # call the function f a lot to demonstrate code re-use
f()
f()
f()
This function returns no value This function returns no value This function returns no value This function returns no value
Notice that once the function has been defined, it gets called by using its name (which the programmer chose when they wrote the function) followed by ()
immediately after the name. We'll later see that the only modification to this syntax is that functions that accept arguments or parameters will be passed the values for those parameters in a comma-separated list inside the ()
.
# call the function f
f()
This function returns no value
f # this doesn't call the function. It just describes the name f
<function __main__.f()>
f () # this works, but don't write it this way. Antipattern!
This function returns no value
f x() # one reason not to add whitespace between the name and () is that typos introduce problems
File "<ipython-input-5-30c6795e7597>", line 1 f x() # one reason not to add whitespace between the name and () is that typos introduce problems ^ SyntaxError: invalid syntax
It's been mentioned several times in I427 to take a "data-driven" approach to your programming. Always be aware of and be able to articulate what data your program is manipulating at each step. Be able to say for each object what that objects name is, what information that object stores, what you need to do with that information, and what type that object is in the program. Functions help with all of these things. You have to articulate what the input and output data is, and if you have, say, a document in a corpus stored in an object and you need to tokenize that document, then writing a function called tokenize_document
that takes a document as its input and returns a list of tokens as output is a very strong way to keep track of the flow of data through your code.
Functions’ behavior is typically specified in advance (often in written form in an aptly named “specification”), and most specifications of behavior do not indicate that functions will produce output. So if the specification does not indicate a function prints output, then it doesn't! Specifications also typically indicate
The name must be specified because programming done in professional teams involves multiple programmers (hence the team). Everyone must agree on the names of things so that one team member can write the function while another team member can call it. If the dev writing the function uses a different name, there will be errors when the other dev attempts to call the non-existant function.
# The spec said that the other dev Tony would write a function g, so now I get to call it!
# The spec between Tony and me was a contract. Tony broke that contract by using the wrong name!
g()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-6-bd6606c81ebd> in <module> 1 # The spec said that the other dev Tony would write a function g, so now I get to call it! 2 # The spec between Tony and me was a contract. Tony broke that contract by using the wrong name! ----> 3 g() NameError: name 'g' is not defined
In items 2 and 4 above, not only must the kind of arguments be specified, the types must be as well. A function that is written as returning "the client's bank balance" is vague. It needs to indicate what type that return value you should be. Is it an integer (the number of cents in the balance), a floating point number, or a string? If it's a string, how is it formatted? These type details should also be specified for the arguments a function takes as well.
To make a function that returns a value, we need only add a return statement to the method. Once we’ve done that, we can use the function in two ways. We can invoke it and get its returned value as immediate output, or we can save that value in an object to use its value again later in the program as needed.
def f(): # define a function f that takes no parameters
return "This function returns a string value" # return is the end of the block ...
f() # call the function f and have its value be immediate output
'This function returns a string value'
fs_ret_val = f() # store f's return value in an object for later use
print(fs_ret_val)
This function returns a string value
Note also that the name f
was re-used above. That f
called above is not the same as the one defined earlier in this notebook that printed output instead of returning it. If I change the function that’s the name f
references by re-defining it, Python has no trouble doing that. This is another example of Python being a dynamically-typed language.
To define a function that takes arguments, we simply name the arguments in order in between the parentheses after the function name.
# this function takes one argument
def f(name):
return f"Hello there, {name}"
f("H.P. Lovecraft")
'Hello there, H.P. Lovecraft'
# this function takes two arguments
def f(greeting, name):
return f"{greeting}, {name}"
f("Hello", "H.P. Lovecraft")
'Hello, H.P. Lovecraft'
In the previous two examples, the input data in the arguments was used to produce output data whose value depended on the arguments. In a nutshell, this is what functions do. The allow programmers to simplify code by replacing long blocks of code with function calls (where the long blocks of code still happen, but you don’t have to read them in the code at that point). Moreover, functions can be called repeatedly so that the block of code in the function body only needs to be written once even though it can be used many times—each time it is called. Even better, if the block of code needs to be changed, it only needs to be changed once inside the body of the function. As long as the input and the result of the computation on that input doesn’t change, the program will still work the same.
The final thing to note is that the previous two examples introduced f-strings. F-strings are a quick- and-dirty way to produce strings that contain the values of other data in them. Adding an f
before the single- or double-quotes makes it an f-string rather than a regular, constant string. Curly brackets can be used within the quotes to include expressions that are evaluated so that there values appear in the resulting strings. Above, the expressions were other strings, and they were stored in variables, but neither of those are required.
int_var = 4
print(f"int_var holds the value {int_var}")
print(f"but f strings can also involve expressions. This one equals {2 * 4}.")
int_var holds the value 4 but f strings can also involve expressions. This one equals 8.
I see an inexecusable amount of students who have nominally passed I210 and I211 yet do not know how methods in Java or functions in Python work. I see A LOT of code like this example.
Sample Homework Problem: Write a function antipattern_function
that adds three to its argument argument1
and returns that value. Below is an example of antipattern_function
being used:
returned_value = antipattern_function(3)
print(returned_value) # prints 6 because 3 + 3 == 6
Here is the code many students write for this kind of specification:
def antipattern_function(argument1):
argument1 = 6
return argument1
The problem with this approach is that it's wrong! The function above happens to return the correct answer then the value 3 is passed in, but it doesn't work for any other value of argument1. Instead, the function needs to be:
def antipattern_function(argument1):
return argument1 + 3
The antipattern I see so much in students' code is the immediate assigning of a new value to the variable representing a function argument. That line argument1 = 6 above is the problem. When that function is called, a value for that argument must be passed in. The argument's name in the function declaration, which is argument1
in this example, acts as a local variable (see next section) in the function that holds the value passed in. When the call
antipattern_function(3)
is made, the variable argument1
will hold the value 3 as the statements in the body of the function are executed. The body of this version of the function
def antipattern_function(argument1):
argument1 = 6
return argument1
immediately replaces whatever value is passed in and stored in argument1
with the hard-coded value 6. So it doesn't matter what value was passed in. Many students have the incorrect idea that they need to somehow initialize the value of function arguments in the body of the function. That's not how it works; function values are passed in instead!
def how_arguments_work(argument1, argument2, argument3):
print(f"I was passed these values {argument1} {argument2} {argument3}")
def how_arguments_dont_work(argument1, argument2, argument3):
argument2 = 6
print(f"Was I passed these values {argument1} {argument2} {argument3}?")
how_arguments_work(3,'potato',4.5)
I was passed these values 3 potato 4.5
how_arguments_dont_work(3,'potato',4.5)
Was I passed these values 3 6 4.5?
The body of the function carries out a computation. You often need to use variables to do that computation, and it's allowed to create new variables in the body of the function. These are called local variables, and they only exist while the body of the function is being executed. They cannot be used outside the function.
Sample specification: A function concatenate_list
should take a list of strings list_of_strings
as its arguments and return a string that includes each item from list_of_strings concatenated with ,
as a separator.
def concatenate_list(list_of_strings):
separator = ', ' # this is a local variable. It can be used only in the body of the function
returned_string = '' # another local variable
for item in list_of_strings:
returned_string += item + separator
return returned_string
joined_list = concatenate_list(['first','second','third'])
print(joined_list)
first, second, third,
# the local variable is not defined outside the function
separator
--------------------------------------------------------------------------- NameError Traceback (most recent call last) <ipython-input-17-c00c1986fbf1> in <module> 1 # the local variable is not defined outside the function ----> 2 separator NameError: name 'separator' is not defined
In the example above, we see that the local variable is not accessible outside the function. We also see how to make a local variable accessible outside the function: we return its value. Once we've returned the value, we can use it immediately as output or save the value into a variable/object to use later.
Variables that exist outside of the functions in your program are global variables. You can use them inside your functions, but you shouldn't. It's a bad programming habit that won't be accepted for full credit in I427.
global_variable = 55
def print_global():
print(global_variable)
print_global()
55
# our function needs to modify a global variable. Seems simple enough
def modify_global():
global_variable = 45
print(global_variable)
# looks good, it's printing the modified variable in the body of the function
modify_global()
45
# now let's make sure it worked. Oh, it didn't work
print(global_variable)
55
One reason using global variables in a function is a bad habit is that it's tempting to think we can modify these variables this way, but we can't. When we attempt to by assigning what we assume is a new value to a global variable, we're realling hiding the global variable by creating a local variable with the same name. We can verify that behavior by checking the value of the global_variable outside of the function after we called it. It hasn't changed!
There is a way to use global variables in your Python function so that you can modify them. I won't show it to you here becuase you don't need to use global variables inside your functions in I427. Functions should be self-contained. Any data they need to do their job will be passed in as input parameters.