The initial stages of learning an interpreted language like Python normally involve entering statements on a command line and immediately executing them to see the result. In IPython notebooks, one can enter several lines and execute them all at once. Similarly, the lowest level of programming involves writing a moderate number of lines of code in a file, which the Python interpreter then executes from a system command line. This is useful for simple tasks like reformatting text files, but it doesn't go far beyond that.
Fortunately, programming power can be increased exponentially by writing functions. In this notebook we will illustrate the basics of functions with very simple examples chosen for pedagogical value, not as examples of functions you would be likely to write and use. The emphasis here is on showing how one gets information into, and out of, a function, not on the sorts of code one is likely to find in real functions.
def simple_message():
print("This is the simplest function that does something.")
Notice how we defined a function by starting with def
, followed by the name of the function, followed by parentheses, followed by a colon, followed by an indented block of code. Every one of these elements is needed.
Notice also that this definition is just that: a definition. After it is executed there is a new Python object in our namespace, simple_message
, but it has not been executed yet. Let's execute it:
simple_message()
No surprise. Just remember that the parentheses are needed when executing the function as well as when defining it.
def print_uppercase_version(some_text):
print(some_text.upper())
print_uppercase_version("using upper case can seem like shouting")
Here we defined a function that takes a single mandatory argument, a text string. It calls the upper
method of that string, which returns an uppercase version of it, and then it prints the result. (Remember, a method is just a function that knows how to operate on the object to the left of the dot. You can see it is a function because of the parentheses.)
A function can be defined with any number of positional arguments:
def print_more_uppercase(some_text, n):
for i in range(n):
print(some_text.upper())
print_more_uppercase("repeat", 2)
Now things get more interesting; we can define a function that takes some fixed number of positional arguments (one, in this example) and any number of additional positional arguments:
def show_variable_arguments(intro, *args):
print(intro)
# The built-in enumerate() function is explained below.
for i, a in enumerate(args):
print("additional argument #%d is %s" % (i, a))
show_variable_arguments("Only one:", "just this")
show_variable_arguments("Three this time:", "first", "second", "third")
Notice the *args
construct: it means, "take all additional arguments, pack them in a tuple, and make it available inside the function under the name 'args'". If there are no additional arguments the tuple will be empty. If present, the *args
construct must follow any mandatory arguments.
We introduced the built-in function enumerate()
; it is often used in loops like this, when one needs both an item and its index. It is an iterator. Each time through the loop, it returns a tuple containing the count, starting from zero, and the corresponding item in its argument. We are automatically unpacking the tuple into the variables i
and a
.
for i, a in enumerate(['dog', 'cat', 'bird']):
print("index is", i, "and the element is", a)
Even with the ability to have a variable number of arguments via *args
, positional arguments can get clumsy to handle and hard to remember as the potential inputs to a function get more complex. Here is an illustration of the solution:
def print_style(some_text, n=1, format='sentence'):
if format == 'sentence':
text = some_text.capitalize() + "."
elif format == 'shout':
text = some_text.upper()
elif format == 'plain':
text = some_text
else:
print("format keyword argument must be 'sentence, 'shout', or 'plain'")
return
for i in range(n):
print(text)
print_style("this is a sentence", n=2)
print_style("a bit loud", format='shout')
print_style("unchanged and only once", format='plain')
print_style("unchanged but 3 times", format='plain', n=3)
print_style("invalid keyword argument...", format='loopy')
There are several things to notice:
Notice the return
statement, to end execution after printing the error message. Of course, return
can do more, as we now show.
We are going to use a tiny bit of numpy now, so that we can operate on a sequence of numbers all at once.
import numpy as np
def sinsq(x):
return np.sin(x) ** 2
print(sinsq([-0.6, -0.3, 0.3, 0.6]))
Again, no surprise: the function returns what you tell it to return with the return
statement. Multiple objects can be returned as a sequence, in the following case a list:
def sinpows(x, n):
"""
Return the first n powers of sin(x), starting from zero.
"""
out = []
for i in range(n):
out.append(np.sin(x) ** i)
return out
zero, first, second = sinpows(0.3, 3)
print("zero: ", zero)
print("one: ", first)
print("two: ", second)
We used automatic unpacking of the returned list to give the outputs individual names. Any Python object can be returned--even a new function object that is defined inside the function. That is an advanced technique, however, so we will not illustrate it here.
Notice also that we included a docstring, a block of text immediately below the definition line, and above the body of function code.
We saw how we could have a variable number of positional arguments---that is, the number is not known when the function is defined, but it can handle any number when it is called. There is a similar ability with keyword arguments:
def show_kwargs_with_names(**kw):
print("kw is an object of type", type(kw))
print("it contains:")
for key, value in kw.items():
print(" key: '%s' with value: '%s'" % (key, value))
show_kwargs_with_names(first="the first",
second="and the second",
another="yet another")
So, just as *args
packs up remaining positional arguments in a tuple, **kw
packs remaining keyword arguments up in a dictionary named 'kw' and makes it available inside the function. Because it is a dictionary, the order in which the arguments appeared on the command line is lost; but that doesn't matter, because the entries are identified by name, the dictionary key.
There is nothing special about the names 'args' and 'kw'; *stuff
would pack arguments in a tuple called 'stuff', and **argdict
would make a dictionary named 'argdict'. But 'args' and 'kw' or 'kwargs' are used most often by convention, and observing such conventions tends to improve readability.
**kw
can directly follow *args
:
def no_explicit_kw(pos1, *args, **kw):
print("args is:", args)
print("kw is:", kw)
no_explicit_kw("dummy", "arg1", "arg2", kw1="first", kw2="second")
or **kw
can follow explicitly named keyword arguments, provided there is no *args
:
def no_star_args(pos1, kw1="the first", **kw):
print("kw1 is:", kw1)
print("kw is a dictionary:", kw)
no_star_args("arg0", kw2="the second", kw3="the third")
Notice that only the keyword arguments that are not included in the definition by name get packed up in the kw
dictionary.
The single and double asterisk constructs not only can be used in function definitions, they can also be used when calling functions. A single asterisk unpacks a sequence into a set of positional arguments, and a double asterisk unpacks a dictionary into a sequence of keyword arguments. Example:
def with_two_arguments(arg1, arg2):
print("arguments are %s and %s" % (arg1, arg2))
some_tuple = ("one", "two")
with_two_arguments(*some_tuple)
and
def with_kwargs(kw0="x", kw1="y"):
print("kw0 is", kw0, "and kw1 is", kw1)
with_kwargs() # defaults
kwdict = {"kw0":"a", "kw1":"b"}
with_kwargs(**kwdict)
Arguments are passed into Python functions by reference, not by value, so if the function modifies something that is passed in, the modification will be seen outside the function. Some Python objects can be modified---that is, they are "mutable", to use the jargon---and some cannot---they are "immutable". So if you pass a list into a function, and append an element to that list, the list will have the new element after the function has been executed:
def add_tail(x):
"""
Given a single list input, append the string "tail" to the list.
This function returns nothing, but modifies its input argument.
"""
x.append("tail")
y = [1, 2, 3]
add_tail(y)
print(y)
This is a valid technique---using a function to modify its argument---so long as you are doing it deliberately, and not by accident.