login
Holden Web
What you'll need to know tomorrow

Defining and Calling Your Own Functions

Exploring Functions

While working through examples so far in this course, you typed similar pieces of code over and over again. Take a look at this snippet of code from our earlier complex file handling example:

Observe
open_tasks = open('open_tasks.txt','r').readlines()
if open_tasks:
    print('-' * 10)
    print('Open Tasks')
    print('-' * 10)
    for i, task in enumerate(open_tasks):
        print(i, task.strip())

done_tasks = open('done_tasks.txt','r').readlines()
print('-' * 10)
print('Done Tasks')
print('-' * 10)
for i, task in enumerate(done_tasks):
    print(i, task.strip())

You typed in almost exactly the same code twice. Wouldn't it be nice if you could just write it once and then call it whenever you needed it, like you've done with Python's various built-in functions? Fortunately, you can! Look at the same code, this time rewritten using a function:

Observe
def task_report(task_file):
    tasks = open(task_file,'r').readlines()
    if tasks:
        print('-' * 10)
        print(task_file.replace('_',' ').replace('.txt','').title())
        print('-' * 10)
        for i, task in enumerate(tasks):
            print(i, task.strip())
task_report('open_tasks.txt')
task_report('done_tasks.txt')

The second example defines a function task_report. It executes the same task as the first example, but operates by calling the function twice. The differences between the two examples are in the file name and the heading that was printed out. The file name is a formal parameter (task_file) of the function, and the heading is created by using replace() to change the underscore ('_') to a space (' ') and the extension ('.txt') to nothing ('') in the file name, and then applying title case to the remainder with title(). (The net result? 'open_tasks.txt' becomes 'Open Tasks' and 'done_tasks.txt' becomes 'Done Tasks.')

Defining and using this function saved three whole lines of code, which isn't that impressive. But there's more.

Now, suppose you need to add three more types of task state to your code: "not yet confirmed," "in testing," and "under review." Without our task_report function, we would have had to add 18 lines of code to accomplish this task! But using the function, we can process each file with a single line, and get the job done with just three lines of code! Take a look at the example:

Observe: Sample function code
def task_report(task_file):
    tasks = open(task_file,'r').readlines()
    if tasks:
        print('-' * 10)
        print(task_file.replace('_',' ').replace('.txt','').title())
        print('-' * 10)
        for i, task in enumerate(tasks):
            print(i, task.strip())

task_report('open_tasks.txt')
task_report('done_tasks.txt')
task_report('not_yet_confirmed.txt')
task_report('in_testing.txt')
task_report('under_review.txt')

Developers don't like to repeat a stanza of code twice. Instead, we put it into a function, and call that function as often as we need it. If you see lots of repetitive code, that represents a code smell as mentioned earlier. The code might work, but changing it, maintaining it,and using it in other places will be harder than it needs to be and the code will be more error-prone.

Write Your First Function

Let's take a shot at writing a function. We'll write some code that averages a list of values. In the editor window, type the code as shown below:

Code
def average(lst):
    """ Averages a list, tuple, or set of numeric values"""
    return sum(lst) / len(lst)

tst_lst = [1, 2, 3, 4]
print('Average this list: {0}'.format(tst_lst))
print(average(tst_lst))
t = (243, 132, 987, 342, 13)
print('Average this tuple: ',t)
print(average(t))
s = {1, 2, 3, 4, 25}
print('Average this set: {0}'.format(s))
print(average(s))

Save it as average.py and run it:

Output
Average this list: [1, 2, 3, 4]
2.5
Average this tuple:  (243, 132, 987, 342, 13)
343.4
Average this set: {1, 2, 3, 4, 25}
7.0

How does it work?

Observe
def average(lst):
    """ Averages a list, tuple, or set of numeric values"""
    return sum(lst) / len(lst)

tst_lst = [1, 2, 3, 4]
print('Average this list: {0}'.format(tst_lst))
print(average(tst_lst))
t = (243, 132, 987, 342, 13)
print('Average this tuple: ',t)
print(average(t))
s = {1, 2, 3, 4, 25}
print('Average this set: {0}'.format(s))
print(average(s))

The function occupies only the first three lines of code in this example. The Python keyword def introduces a function definition. It must be followed by the function name and the list of formal parameters in parentheses. The code that makes up the function is called the function body. The function body must be indented. The string """ Averages a list, tuple, or set of numeric values""" is the function's documentation string, often abbreviated as docstring. The interpreter uses docstrings to give programmers information about how the function works and how it should be called. Finally, the last line tells the function to return the sum() of the values entered, divided by the number of values as determined by the len() function. This returned value becomes the value of the function call during evaluation of expressions.

Our function is followed by test code that lets us verify that the function works correctly. Each time the average() function is written with a list of numbers in it, such as average(tst_lst) or even average([10,20,30,40,50]), our function code is run with those numbers as formal parameters.

In the average.py example, we used the name lst for our parameter. It could have any name, but for the sake of clarity, use a name that makes the purpose of the variable clear. Also, be careful not to use names of existing Python functions or other objects. You don't want to use list or tuple as variable or parameter names, for example, because they are the names of Python built-in functions. If you do, your program may behave in completely incomprehensible ways.

Modern Python The set {1, 2, 3, 4, 25} now prints in that order on current Python. The original 3.1 output showed {25, 2, 3, 4, 1} because sets are unordered and iteration order was different. The average is the same either way; only the display order changed.
Parameters and Arguments

Parameters are the names you give to the inputs to the function when the function is defined. Arguments are the values you provide when you call the function. Inside the function body, your code can access the arguments using the names of the parameters.

Suppose you want to write a function that prints out the elements in a list, and you want to provide an option to have the function print the list in reverse order. To do this, you'll use positional and keyword parameters. Type the code as shown:

Code
def print_list(lst, rev=False):
    """ prints the contents of a list. """
    if rev:
        lst = reversed(lst)
    for i in lst:
        print(i)

print_list(['Printing', 'a', 'list'])
print()
print_list(['Printing', 'a', 'reversed', 'list'], True)
print()
print_list(lst=['A', 'list', 'with', 'specified', 'arguments'],rev=False)

Save it as print_list.py and run it. This function takes two parameters. You're familiar with the first parameter, lst; the second parameter, rev=False, introduces a new feature—a keyword parameter, which has a default value (the value following the equals sign, which in this case is False). If you call the function without passing an argument corresponding to the rev parameter, it uses that default value.

The function's code looks at the value of rev, and if it is true, it re-binds the parameter to a reversed copy of the list. It does this rather than reversing the list in place, because such a reversal would affect code outside of the function (though there's nothing illegal about changing a mutable object inside of a function, you want to make sure that the users of the function know they should expect such changes. We'll go over parameters and arguments in greater detail in future lessons).

Modern Python The mention of mutable objects and unexpected changes applies especially to mutable default arguments. If you write def f(lst=[]):, the same list object is reused across every call that omits the argument—a classic source of subtle bugs. The safe idiom is def f(lst=None): lst = [] if lst is None else lst. Here rev=False is a boolean (immutable), so there is no issue.
Returning Values

The first function you wrote in this lesson, average(), returned a value that your code then displayed via the built-in print() function. When a function call is written in an expression (for example, in print(average(tst_lst))), the value of the function call in that expression is actually the value that the function returned in its return statement (2.5). But the second function you created, print_list(), did not include a return statement. This is equivalent to the function ending with return None. So all functions will return some value, but by convention, functions that don't need to return anything can implicitly return None. If the function isn't intended to return a value, it's confusing to add an explicit return statement.

You can either use the function calls in control flow code (that is, code that controls the order in which tasks are executed, such as if or while statements) or save the values returned by functions, binding them to a variable in an assignment statement and using that value again and again without needing to rerun the function. To see these principles in action, create a new file as shown:

Code
def structure_list(text):
    """Returns a list of punctuation in a text"""
    punctuation_marks = "!?.,:;"
    punctuation = []
    for mark in punctuation_marks:
        if mark in text:
            punctuation.append(mark)
    return punctuation

text_block = """\
Python is used everywhere nowadays.
Major users include Google, Yahoo!, CERN and NASA (a team of 40 scientists and engineers
is using Python to test the systems supporting the Mars Space Lander project).
ITA, the company that produces the route search engine used by Orbitz, CheapTickets,
travel agents and many international and national airlines, uses Python extensively.
The YouTube video presentation system uses Python almost exclusively, despite their
application requiring high network bandwidth and responsiveness.
This snippet of text taken from chapter 1"""

for line in text_block.splitlines():
    print(line)
    p = structure_list(line)
    if p:
        print("Contains:", p)
    else:
        print("No punctuation in this line of text")
    if ',' in p:
        print("This line contains a comma")
    print('-'*80)

Save it as return_value.py and run it. The structure_list() function accepts a single parameter called text. This value is checked to find common punctuation marks. These results are placed into a list and that list is returned.

The tricky part is the loop itself and what it does with the returned value of structure_list(). Instead of immediately printing the value, we save it to the variable p. This variable is subsequently used in two different if statements. The first checks to see if the list p is empty, then prints an appropriate result. Then the variable is used again to determine whether or not a comma is present.

Output
Python is used everywhere nowadays.
Contains: ['.']
--------------------------------------------------------------------------------
Major users include Google, Yahoo!, CERN and NASA (a team of 40 scientists and engineers
Contains: ['!', ',']
This line contains a comma
--------------------------------------------------------------------------------
is using Python to test the systems supporting the Mars Space Lander project).
Contains: ['.']
--------------------------------------------------------------------------------
ITA, the company that produces the route search engine used by Orbitz, CheapTickets,
Contains: [',']
This line contains a comma
--------------------------------------------------------------------------------
travel agents and many international and national airlines, uses Python extensively.
Contains: ['.', ',']
This line contains a comma
--------------------------------------------------------------------------------
The YouTube video presentation system uses Python almost exclusively, despite their
Contains: [',']
This line contains a comma
--------------------------------------------------------------------------------
application requiring high network bandwidth and responsiveness.
Contains: ['.']
--------------------------------------------------------------------------------
This snippet of text taken from chapter 1
No punctuation in this line of text
--------------------------------------------------------------------------------
Multiple Return Values

So, what if you need to return two values? Suppose that, in addition to the punctuation in our last example, you also want to return the location of the word "Python." You could write a second function, but it's often more efficient when the two results require related logic in order to have your function return another value. Try out the example below and get a better look at this concept:

Code
def structure_list(text):
    """Returns a list of punctuation and the location of the word 'Python' in a text"""
    punctuation_marks = "!?.,:;"
    punctuation = []
    for mark in punctuation_marks:
        if mark in text:
            punctuation.append(mark)
    return punctuation, text.find('Python')

text_block = """\
Python is used everywhere nowadays.
Major users include Google, Yahoo!, CERN and NASA (a team of 40 scientists and engineers
is using Python to test the systems supporting the Mars Space Lander project).
ITA, the company that produces the route search engine used by Orbitz, CheapTickets,
travel agents and many international and national airlines, uses Python extensively.
The YouTube video presentation system uses Python almost exclusively, despite their
application requiring high network bandwidth and responsiveness.
This snippet of text taken from chapter 1"""

for line in text_block.splitlines():
    print(line)
    p, l = structure_list(line)
    if p:
        print("Contains:", p)
    else:
        print("No punctuation in this line of text")
    if ',' in p:
        print("This line contains a comma")
    if l >= 0:
        print("Python is first used at {0}".format(l))
    print('-'*80)

Save and run it. We modified the function to return a two-element tuple. The first element is the punctuation as computed in the previous version. The second element is the location of the word "Python." If the word doesn't exist in the text, -1 is returned, as determined by the find() method's specification.

The function result is assigned to two separate variables using an unpacking assignment, and an additional test is made on the returned index value to determine whether to report the presence of the word "Python."

Output
Python is used everywhere nowadays.
Contains: ['.']
Python is first used at 0
--------------------------------------------------------------------------------
Major users include Google, Yahoo!, CERN and NASA (a team of 40 scientists and engineers
Contains: ['!', ',']
This line contains a comma
--------------------------------------------------------------------------------
is using Python to test the systems supporting the Mars Space Lander project).
Contains: ['.']
Python is first used at 9
--------------------------------------------------------------------------------
ITA, the company that produces the route search engine used by Orbitz, CheapTickets,
Contains: [',']
This line contains a comma
--------------------------------------------------------------------------------
travel agents and many international and national airlines, uses Python extensively.
Contains: ['.', ',']
This line contains a comma
Python is first used at 65
--------------------------------------------------------------------------------
The YouTube video presentation system uses Python almost exclusively, despite their
Contains: [',']
This line contains a comma
Python is first used at 43
--------------------------------------------------------------------------------
application requiring high network bandwidth and responsiveness.
Contains: ['.']
--------------------------------------------------------------------------------
This snippet of text taken from chapter 1
No punctuation in this line of text
--------------------------------------------------------------------------------
Functions and Namespaces

Using functions in Python has the added benefit of helping us begin to understand namespaces.

When you call a function, Python dynamically creates a new namespace and binds the argument values to the appropriate parameter names. Assignments made during execution of the function call result in bindings in the function call namespace. When the function returns, the namespace is automatically destroyed, and any bindings inside the namespace are lost.

You can sum up how functions handle namespaces in Python by understanding these two rules:

  1. Variables bound within a Python function body only exist in namespaces created by calls of that function.
  2. Variables bound in the global namespace can be accessed by functions, but may not be bound unless specifically declared to be global.

Let's test out the first rule. As you will see, the variable c defined below is assigned inside of the test() function. Start an interactive session and enter the commands shown:

Code and output
>>> def test(a, b):
...     c = a + b
...     return c
...
>>> test(1, 2)
3
>>> c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

And now let's test the second rule. Type the commands below as shown:

Code and output
>>> def test_a():
...     print(a)
...
>>> test_a()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in test_a
NameError: name 'a' is not defined
>>> a = "Python"
>>> test_a()
Python
Modern Python The original output showed NameError: global name 'a' is not defined. Current Python drops the word "global" and reports NameError: name 'a' is not defined in both cases. The behaviour is identical; only the wording changed.

You can see that when the function attempts to access a global variable a, the function fails in its first call, because a has not yet been created in the global environment. The interpreter knows that a is not local to the function because the function body contains no assignment to it. Once the variable is created by an assignment in the module namespace, a call to the function succeeds without raising an exception.

So, if any assignment is made to a variable inside a function body, the variable is local to the function. Changing a global variable inside a function body isn't a best practice, but sometimes it's a necessary evil. To achieve that end, you use a global statement to declare that the variable, although assigned inside of the function body, is in the module (global) scope. To demonstrate this, type the commands below as shown:

Code and output
>>> def test_a():
...     global a
...     a = "XML"
...     print(a)
...
>>> a = "Python"
>>> test_a()
XML
>>> print(a)
XML

Here the value "Python" is bound to a in module scope. After the function is called, you can see that a has been re-bound by the assignment inside of the function.

Parameters That Receive Multiple Arguments

Sometimes when you create a Python function, you don't know how many arguments you are going to get and you want the caller to be able to provide any number of arguments. For example, you may want to create a function that takes all the numbers given as parameters and multiply them together. To do this, we use a special parameter specification, *name. There can be only one such parameter, and it must follow any standard positional and/or keyword parameters.

When you prefix the parameter with the asterisk (*) character in the function definition, this tells the interpreter to collect any unmatched positional arguments into a tuple and then bind the tuple to the name following the asterisk in the called function's namespace. Inside of your function, this tuple can be used like any other Python iterable. Let's check it out. Create a new file in the editor window as shown:

Code
def multiplier(*args):
    """ Multiply the arguments together and return the result.
        Return 0 if nothing is provided.
    """
    if not args:
        return 0
    product = args[0]
    for a in args[1:]:
        product *= a
    return product

print(multiplier())
print(multiplier(1,2,3,4))
print(multiplier(6,7,8,9,10,11,12,13))
print(multiplier(10,20,100))

Save it as argument_list.py and run it. The multiplier() function, our single parameter args (which you can think of as the "sequence parameter") is prefixed with *, so all positional arguments to a call will appear inside of this tuple. The rest of the function is made up of code that you should find comprehensible by now. We can call the function with any number of arguments.

Output
0
24
51891840
20000

The * sequence parameter must follow any standard positional or keyword parameters. This can be useful when regular arguments are also required. For instance, you may want to provide an optional amount to be added to the product. You'd accomplish that by using a keyword argument with a default value of zero. Let's see how this is done. Modify the program as shown:

Code
def multiplier(total=0.0, *args):
    """ Multiply the arguments together, add a prior total, and return the result.
        Return 0 if nothing is provided.
    """
    if not args:
        return 0total
    product = args[0]
    for a in args[1:]:
        product *= a
    return product
    print("product:", product)
    return product + total

print(multiplier())
print(multiplier(1,2,3,4))
print(multiplier(6,7,8,9,10,11,12,13))
print(multiplier(10,20,100))

Save and run it. The first parameter of each set is now passed as the total, and the rest as args.

Output
0.0
product: 24
25
product: 8648640
8648646
product: 2000
2010
Putting It All Together

When you were in grade school, you learned that six times seven (6 x 7) was equivalent to adding six to itself seven times (6 + 6 + 6 + 6 + 6 + 6 + 6). Calculating this the long way took time, so you memorized the end result. If you learned your "times tables" at school, you can probably still respond immediately, even now, when asked "what is six times seven?" Storing something in memory to save the trouble of working it out each time you need the answer is called caching.

Caching is a technique used to avoid repeating computations. In this case, we take calculated values from arguments and storing them so that you can return the values if asked to compute a result from the same arguments again later. This way, instead of calculating the same thing a hundred times, you save each calculation the first time you make it, and recall it when needed. When applied to a function, this caching technique is often referred to as memoization.

To illustrate, we will use the built-in input() method to prompt for two numeric values. The code does multiplication the old way (6 + 6 + 6 + 6 + 6 + 6 + 6). Finally, we'll use the ability of functions to use the global namespace to cache the results, so when you try it with big numbers (10 million * 10 million), you don't need to repeat lengthy calculations.

In the code example below, we create a kid() function to do the math. It's a simple piece of code that does multiplication the hard way - by repeated addition! Create a new file in the editor window and type the code shown:

Code
""" Demonstrates the need for caching """

def kid(a, b):
    """ Multiplication the hard way """
    c = 0
    for i in range(b):
        c += a
    return c

while True:
    a = input('enter a number: ')
    b = input('enter another number: ')
    a = int(a)
    b = int(b)
    print(kid(a,b))

Save it as caching.py and run it. Try it with small numbers first, perhaps 4 and 5. Then try something large like 5 and 10000000 (one and seven zeros). You may have to wait awhile as the computer adds 5 ten million times. If not, increase the number until you see an appreciable delay.

Now, modify your kid() function so that it maintains a record of the arguments it has been called with, and saves previously-computed results in a global dict so that before it even starts to perform a calculation, it can provide a previously-computed result, thereby saving time. Edit the code below as shown:

Code
""" Demonstrates the need for caching """
""" Demonstrates caching """

global_cache = {}

def kid(a, b):
    """ Multiplication the hard way """
    if (a, b) in global_cache:
        return global_cache[(a, b)]

    c = 0
    for i in range(b):
        c += a
    global_cache[(a, b)] = c
    return c

while True:
    a = input('enter a number: ')
    b = input('enter another number: ')
    a = int(a)
    b = int(b)
    print(kid(a,b))
    print(global_cache)
    print('-'*40)

Now try the program again. Enter 5 * 10000000 (or whatever big number you chose). Wait for the response and try it again. You'll notice the second time the result appears instantly.

Here, when the function is called, it immediately checks the global global_cache dict to see whether this particular set of arguments has been used before. If it has, the cached result is immediately returned, bypassing the lengthy computation. If the argument set isn't found in global_cache, then it is computed in the usual way, but before the result is returned, it is added to the global_cache so this new result can be produced immediately if we ever need it again.

Modern Python Python 3.2 added functools.lru_cache (and, since 3.9, functools.cache) as a ready-made decorator for exactly this pattern. Applying @functools.cache to a function gives you automatic memoization with no hand-written cache dict required.
A Solid Foundation

In this lesson, you started to learn how to write functions, understand the difference between parameters and arguments, how return values work, and a little more about namespaces. I'm really impressed with your progress so far! Now that you have a pretty good grip on Python basics, let's move on and learn about modules and imports, and even more about namespacing.

See you in the next lesson!