Understanding functions¶
Every function you will write today will have the same structure:
def name(input1, input2, ...):
'''
docstring
'''
pass # body goes here
where name
is the name of the function, input1
is name of the
first input to the function, input2
is the name of the second
input to the function, etc. Function inputs are often called either
arguments or parameters: typically, the former is what we use when
referring to the actual values given to a function at the specific
places it is called, and the latter is used when talking about the
named inputs declared in a function’s header (everything from the
def
to the :
). The ellipsis (...
) is not part of the
syntax. It is included to indicate that a function can have more than
two parameters.
As an aside, you may see the term actual parameters used to refer to the actual values passed to the function through a function call and the term formal parameters used to refer to the names used for the function inputs within the function definition.
Python functions often include a comment (in triple quotes), called a docstring, immediately following the function header. This comment typically contains a brief description of the purpose of the function along with some information about the parameters and the expected return value. Every function you write for a programming assignment or lab should include a docstring.
The body of the function is everything following the :
that is
indented at least one level past the def
in the function’s header.
In the following example:
import math
def sinc(x):
'''
Real valued sinc function f(x) == sin(x)/x
Inputs:
x: float
Return: float
'''
# Make sure we don't divide by zero
if x != 0:
return math.sin(x) / x
else:
# sin(0) / 0 == 1
return 1.0
the function’s name is sinc
and it has a single parameter named
x
. We do not indicate the type of the parameters or return
value explicitly in Python. Instead, the types are checked at
runtime. For some functions, a given parameter may take on many
different types, perhaps even depending on the value of other parameters.
For others, one specific type is needed. It is common to specify the
expected types for parameters and return value in the function’s
docstring, as seen in this example. The return type is merely the type
of the expression in the return statement.
A call to a function is an expression of the form:
name(argument1, argument2, ...)
Arguments to a function are also expressions. Each input should evaluate to a value with the expected type for the corresponding parameter. Particular care should be taken with respect to argument types: if you provide an argument with the wrong type to a function, the call will not necessarily fail at runtime! For example, if the parameter with the wrong type happens to go unused during that particular function invocation, then the parameter’s type is not checked. This language design can lead to type errors that pop up unexpectedly.
In our next example, we use sinc
in an example function named
go
:
def go():
'''
Example uses of sinc
'''
a = sinc(3.14)
b = 3.14
print("A is {:f}".format(a))
c = sinc(b * 2) + sinc(b * 3.0)
print("C is {:f}".format(c))
Notice that the argument to the first call to sinc
is a constant
of type float
, whereas the arguments to the second and third calls
are more complex expressions that yield floating point values when
evaluated. Although the 2
in b * 2
is of type int
,
because b
is a float
, 2
is implicitly converted to a
float
to match it (this process is called upcasting). Also,
notice that the results of the second and third calls yield floating
point values that are added together to yield a new value with type
float
.
When we use a function we need to make sure that the parameters have the expected type.
sinc
expects a float so the following use is correct:
b = sinc(5.3) # OK
But this next one is incorrect:
b = sinc("cat") # INCORRECT
and will fail at runtime.
sinc
returns a float and can only be used in a context that
requires a float. The following expression is correct:
sinc(5.3) + 1.0 # OK
But this next one is wrong:
sinc(5.3)[0] # INCORRECT: because we're trying to index into
# a float.
The above error is just a simple example, and is fairly easy to catch. However, as functions and their types become more complex, this practice of thinking about the types will become both more challenging and more important. Many problems in programming can be detected and resolved quickly just by being careful about using values with the right types.
As an aside, many statically-typed languages like Java require programmers to specify types for variables, parameters, and return values. In such languages, type conflicts must be fixed before the code can be run, which leads to fewer runtime errors.
Returning None
Some functions, such as print
, perform an effect, but do not compute a
value to be returned. Because all functions in Python return something, such functions
return a special value that represents nothing: None
.
For example, here’s a function that just takes a name and prints a message:
def hello(name):
print("Hello " + name)
Anytime a return statement is left off in a function definition, None
is implicitly returned,
so the above is equivalent to:
def hello(name):
print("Hello " + name)
return None
What is the value of x
after I run the following code?
x = hello("Sam")
The value of x
is an object where anything useful you may try to do
with it results in an error. Although the expression x == None
evaluates
to True
, and print(x)
does print None
, basically anything else will
cause a runtime error. Thus, the result of functions which return None
is typically
not bound (assigned) to a variable, effectively throwing it away.
Lists and Functions¶
Functions can take lists as arguments and return lists as values. For example, here is a function that finds the largest value in a non-empty list:
def max(vals):
'''
Compute the maximum element in a non-empty list.
Inputs:
vals: non-empty list
Returns: largest value in the list
'''
assert vals != [], "vals must be non-empty"
current_max = vals[0]
for x in vals[1:]:
if x > current_max:
current_max = x
return current_max
One nice thing about Python is that it is often the case that a
function can be used with multiple types. For example, what is the
result of evaluating max([1.0, 27, -3, 4])
versus max(["zzz",
"xyz", "abc"])
?
Here is a function that takes a list returns a new list with the squares of its elements:
def sqr_list(vals):
'''
Compute the squares of values in a list
Inputs:
vals: list
Returns: list of the squared values.
'''
squared = []
for x in vals:
squared.append(x * x)
return squared
Here are some very simple uses of these functions:
def go():
'''
sample uses of sqr_list and max.
'''
some_vals = [1.0, 3.0, 5.0, 7.0]
squared_vals = sqr_list(some_vals)
print(squared_vals)
print("Maximum value is: {:f}".format(max(squared_vals)))