Functions
Contents
4.4. Functions#
4.4.1. What are (Python) Functions?#
In this class, we will often talk about function
s.
So what is a function?
We like to think of a function as a production line in a manufacturing plant: we pass zero or more things to it, operations take place in a set linear sequence, and zero or more things come out.
We use functions for the following purposes:
Re-usability: Writing code to do a specific task just once, and reuse the code by calling the function.
Organization: Keep the code for distinct operations separated and organized.
Sharing/collaboration: Sharing code across multiple projects or sharing pieces of code with collaborators.
4.4.2. How to Define (Python) Functions?#
The basic syntax to create our own function is as follows:
def function_name(inputs):
# step 1
# step 2
# ...
return outputs
Here we see two new keywords: def
and return
.
def
is used to tell Python we would like to define a new function.return
is used to tell Python what we would like to return from a function.
Let’s look at an example and then discuss each part:
def mean(numbers):
total = sum(numbers)
N = len(numbers)
answer = total / N
#print(answer)
return answer
Here we defined a function mean
that has one input (numbers
),
does three steps, and has one output (answer
).
Let’s see what happens when we call this function on the list of numbers
[1, 2, 3, 4]
.
numbers=[1,2,3,4,5,6]
del numbers
x = [1, 2, 3]
the_mean = mean(x)
print(the_mean)
2.0
Additionally, as we saw in the control flow lecture, indentation controls blocks of code (along with the scope rules).
To see this, compare a function with no inputs or return values.
def f():
print("1")
print("2")
f()
1
2
With the following change of indentation…
def f():
print("1")
print("2")
f()
2
1
4.4.2.1. Scope#
Notice that we named the input to the function x
and we called the output
the_mean
.
When we defined the function, the input was called numbers
and the output
answer
… what gives?
This is an example of a programming concept called variable scope.
In Python, functions define their own scope for variables.
In English, this means that regardless of what name we give an input variable (x
in this example),
the input will always be referred to as numbers
inside the body of the mean
function.
It also means that although we called the output answer
inside of the
function mean
, that this variable name was only valid inside of our
function.
To use the output of the function, we had to give it our own name (the_mean
in this example).
Another point to make here is that the intermediate variables we defined inside
mean
(total
and N
) are only defined inside of the mean
function
– we can’t access them from outside. We can verify this by trying to see what
the value of total
is:
def mean(numbers):
total = sum(numbers)
N = len(numbers)
answer = total / N
return answer # or directly return total / N
# uncomment the line below and execute to see the error
# total
This point can be taken even further: the same name can be bound to variables inside of blocks of code and in the outer “scope”.
x = 4
print(x)
def f():
# this function does this and that
x = 5 # a different "x"
print(x)
f() # calls function
print(x)
4
5
4
The final point we want to make about scope is that function inputs and output don’t have to be given a name outside the function.
mean([10, 20, 30])
20.0
4.4.2.2. Documentation#
To provide help information, we need to add what Python programmers call a “docstring” to our functions.
This is done by putting a string (not assigned to any variable name) as
the first line of the body of the function (after the line with
def
).
Below is a new version of the template we used to define functions.
def function_name(inputs):
"""
Docstring
"""
# step 1
# step 2
# ...
return outputs
Let’s re-define our portfolio_returns
function to include a docstring.
def portfolio_returns(w, r):
"""
Computes the returns of a portfolio that invests w on the asset with return r and (1-w) in the risk-free rate
Takes the form F(w, r,rf) = w*r+(1-w)rf, here we fix the rf to 0.01
"""
return w*r + (1-w)*0.01
Now when we have Jupyter evaluate portfolio_returns?
, our message is
displayed (or use the Contextual Help window with Jupyterlab and Ctrl-I
or Cmd-I
).
portfolio_returns?
Signature: portfolio_returns(w, r)
Docstring:
Computes the returns of a portfolio that invests w on the asset with return r and (1-w) in the risk-free rate
Takes the form F(w, r,rf) = w*r+(1-w)rf, here we fix the rf to 0.01
File: c:\users\alan.moreira\documents\github\textbook\chapters\essentials\<ipython-input-8-4efdb59d826b>
Type: function
We recommend that you always include at least a very simple docstring for nontrivial functions.
This is in the same spirit as adding comments to your code — it makes it easier for future readers/users (including yourself) to understand what the code does.
Evaluate the function
portfolio_returns(0.5, 0.05)
0.030000000000000002
4.4.2.3. Default and Keyword Arguments#
Functions can have optional arguments.
To accomplish this, we must these arguments a default value by saying
name=default_value
instead of just name
as we list the arguments.
To demonstrate this functionality, let’s now make \(rf \) arguments to our portfolio_return function!
def portfolio_returns(w, r,rf=0.01):
"""
Computes the returns of a portfolio that invests w on the asset with return r and (1-w) in the risk-free rate
Takes the form F(w, r,rf) = w*r+(1-w)rf
"""
return w*r + (1-w)*rf
We can now call this function by passing in just w and r. Notice that it will
produce same result as earlier because rf
are the same as earlier.
portfolio_returns(0.5, 0.05)
0.030000000000000002
However, we can also set the other arguments of the function by passing
more than just w
and r
.
portfolio_returns(0.5, 0.05,0.03)
0.04
In the example above, we used w= 0.5
, r= 0.05
, and rf=0.03
We can also refer to function arguments by their name, instead of only their position (order).
To do this, we would write func_name(arg=value)
for as many of the arguments
as we want.
Here’s how to do that with our portfolio_returns
example.
portfolio_returns(0.5, 0.05,rf=0.02)
0.035
In terms of variable scope, the z
name within the function is
different from any other z
in the outer scope.
To be clear,
x = 5
def f(x):
return x
f(x) # "coincidence" that it has the same name
5
This is also true with named function arguments, above.
rf = 0.03
portfolio_returns(0.5, 0.05,rf=rf)# no problem!
0.04
In that example, the rf
on the left hand side of rf = rf
refers
to the local variable name in the function whereas the rf
on the
right hand side refers to the rf
in the outer scope.
This is an appealing feature of functions for avoiding coding errors: names of variables within the function are localized and won’t clash with those on the outside.
Importantly, when Python looks for variables matching a particular name, it begins in the most local scope.
That is, note that having an rf
in the outer scope does not impact the local one.
print(portfolio_returns(0.5, 0.05,rf=0.03))
rf = 0.02
portfolio_returns(0.5, 0.05,0.03)# no problem!
0.04
0.04
A crucial element of the above function is that the rf
variable
was available in the local scope of the function.
Consider the alternative where it is not. We have removed the rf
function parameter as well as the local definition of rf
.
def portfolio_returns(w, r):
"""
Computes the returns of a portfolio that invests w on the asset with return r and (1-w) in the risk-free rate
net of fees fee
Takes the form F(w, r,rf) = w*r+(1-w)rf-fee
I set fees to zero
"""
fee=0
return w*r + (1-w)*rf-fee
rf = 0.02 # in the outer scope
print(f"rf = {rf} gives {portfolio_returns(0.5, 0.05)}")
rf = 0.03
print(f"rf = {rf} gives {portfolio_returns(0.5, 0.05)}")
rf = 0.02 gives 0.035
rf = 0.03 gives 0.04
The intuition of scoping does not apply only for the “global” vs. “function” naming of variables, but also for nesting.
For example, we can define a version of portfolio_returns
which
is also missing a fee
in its inner-most scope, then put the function
inside of another function.
rf = 0.01
def return_given_fee(fee):
# Scoping logic:
# 1. local function name doesn't clash with global one
# 2. fee comes from the function parameter
# 3. rf comes from the outer global scope
def portfolio_returns(w, r):
return w*r + (1-w)*rf-fee
# using this function
return portfolio_returns(0.5, 0.05)
fee = 100 # this will not matter
fees = [0.02, 0.03, 0.05]
# comprehension variables also have local scope
# and don't clash with the alpha = 100
[return_given_fee(fee) for fee in fees]
[0.010000000000000002, 3.469446951953614e-18, -0.02]
4.4.2.4. Methods#
As we learned earlier, all variables in Python have a type associated with them.
Different types of variables have different functions or operations defined for them.
For example, I can divide one number by another or make a string uppercase.
It wouldn’t make sense to divide one string by another or make a number uppercase.
When certain functionality is closely tied to the type of an object, it is often implemented as a special kind of function known as a method.
For now, you only need to know two things about methods:
We call them by doing
variable.method_name(other_arguments)
instead offunction_name(variable, other_arguments)
.A method is a function, even though we call it using a different notation.
When we introduced the core data types, we saw many methods defined on these types.
Let’s revisit them for the str
, or string type.
Notice that we call each of these functions using the dot
syntax
described above.
s = "This is my handy string!"
s.upper()
'THIS IS MY HANDY STRING!'
s.title()
'This Is My Handy String!'