In this lesson we'll cover functions in Python, a concept that you've already encountered but to which you've not yet been formally introduced. Now we're going to dig into this a little bit more because writing functions is where lazy programmers become good programmers.
In other words, as we saw with the concept of iteration, programmers are lazy and they tend want to avoid doing boring tasks over and over again. The idea is to avoid "wasting time re-inventing the wheel" and programmers have abbreviated this idea to the acronym D.R.Y. (Do not Repeat Yourself): if you are doing something more than once or twice, ask yourself if there's a way to encapsulate what you are doing in a function: you write the function once, and then call it whenever you need to complete that task.
Naturally, D.R.Y. has its opposite: W.E.T. (We Enjoy Typing or Write Everything Twice). Dry is nearly always better than wet.
Encapsulating regularly-used bits of code in functions has several advantages:
Basically, a function is a way to do something to something in a portable, easy-to-use little bundle of code.
In [ ]:
myList = [1,"two", False, 9.99]
len(myList) # A function
In [ ]:
print(myList) # A different function!
As we briefly mentioned in another notebook, any 'word' followed by a set of parenthesis is a function. The 'word' is the function's name, and anything that you write within the parantheses are the function's inputs (also known as parameters). Like so:
function_name(optional_parameter_1, optional_parameter_2, ...)
So how do we create (instantiate in programming terms) a new function? Like everything else in Python, functions have specific rules that you have to follow for the computer to understand what you want it to do. In this case there are two separate steps: the function definition and the function call.
This is a function definition:
def myFirstFunc():
print("Nice to meet you!")
Let's see what happened there:
def
) a new function.def
we gave the function a name: myFirstFunc
.The reason for the indenting is the same as for a while
loop or an if
condition! It indicates to the Python interpreter that whatever is indented belongs to the function. Is like saying: "Look man, I'm going to define this myFirstFunc
function, and whatever is indented afterwards is part of the function". That is what we call the function's body, and it's the full package of instructions that we want the computer to run every time we call the function.
In [ ]:
# the function definition
def myFirstFunc():
print("Nice to meet you!")
# the function call
myFirstFunc()
Notice that the sequence of function definiton (def
) and then function call (function_name()
) is important! Think about it: how would Python know what we are referring to (i.e. what is the myFirstFunc
it has to call?), if we haven't yet specified it?
It's the same as with variables: try to print
one before you've defined it and Python will complain!
In [ ]:
print(myVariable)
myVariable = "Hallo Hallo!"
Reading (out loud!) the error message hopefully makes the error obvious... Quite explicit, isn't it? :)
In [ ]:
myVariable = "Hallo Hallo!"
print(myVariable)
In [ ]:
#your code here
In [1]:
def sunnyDay():
print("What a lovely day!")
Now define a function named "gloomyDay" that prints "I hate rainy days!"
In [ ]:
#your code here
Finally, call the two functions you have defined so that "I hate rainy days!" is printed before "What a lovely day!"
In [ ]:
#your code here
In [ ]:
gloomyDay()
sunnyDay()
Those are pretty basic functions, and as you might have noticed they all kind of do the same thing but are no shorter than the thing they replaced (a single print command). You will definetely need them though whenever you are using a function to process some input and return some output. In that case the paramters are inputs that you are passing to the function.
def myFunction( input_parameter ):
# do something to the input
return input_parameter
In [ ]:
def printMyName( name ):
print("Hi! My name is: " + name)
printMyName("Gerardus")
In [ ]:
#your code here
In [ ]:
printMyName("James")
A little more useful, right? If we had to print out name badges for a big conference, rather than typing "Hi! My name is ..." hundreds of times, if we had a list of people's names, we could just use a for
loop to print out each one in turn using this function. The function adds the part that is the same for every name badge and all we need to do is pass it the input parameters. In fact, why don't we try that now?
In [ ]:
for name in ["Jon Reades", "James Millington", "Chen Zhong", "Naru Shiode"]:
printMyName(name)
In the function printMyName
we used just one parameter as an input, but we are not constrained to just one. We can input many parameters separated by commas; let's redefine the printMyName
function:
In [ ]:
def printMyName(name, surname):
print("Hi! My name is "+ name + " " + surname)
printMyName("Gerardus", "Merkatoor")
And now can pass input parameters to a function dynamically from a data structure within a loop:
In [ ]:
britishProgrammers = [
["Babbage", "Charles"],
["Lovelace", "Ada"],
["Turing", "Alan"],
]
for p in britishProgrammers:
printMyName(p[1], p[0])
Neat right? We've simplified things to that we can focus only on what's important: we have our 'data structure' (the list-of-lists) and we have our printing function (printMyName
). And now we just use a for
loop to do the hard work. If we had 1,000 british programmers to print out it would be the same level of effort.
See what we mean about it being like Lego? We've combined a new concept with a concept covered in the last notebook to simplify the process of printing out nametags.
In [ ]:
#your code here
In [ ]:
def printMyAge(name, age):
print(name + " is " + str(age) + " years old.")
printMyAge('Jon',25)
There's actually another way to do this that is quite helpful because it's easier to read:
In [ ]:
def printMyAge(name, age):
print(f"{name} is {age} years old.") # This is called a 'f-string' and we use {...} to add variables
printMyAge('Jon',25)
Now I'd like you to focus on a particuarly important concept: something called 'scoping'. Notice that the names we are using for the parameters are de facto creating new variables that we then use in the function body (the indented block of code). In the example below, 'name' and 'surname' are scoped to the body of the funciton. Outside of that block (outside of that scope) they don't exit!
Here's the proof:
In [ ]:
def whoAmI(myname, mysurname):
if not myname:
myname = 'Charles'
if not mysurname:
mysurname = 'Babbage'
print("Hi! My name is "+ myname + " " + mysurname + "!")
print(myname) # myname _only_ exists 'inside' the function definition
Notice how the ErrorMessage is the same as before when we tried to print
a variable that wasn't defined yet? It's the same concept: the variables defined as parameters exist only in the indented code block of the function (the function scope ).
But notice too that if you replace print name
with whoAmI("Ada", "Lovelace")
then the error disappears and you will see the output: "Hi! My name is Ada Lovelace." So to reiterate: parameters to a function exist as variables only within the function scope.
In [ ]:
whoAmI('Ada','Lovelace')
Let's say that your namebade printing function is a worldwide hit, and while most conferences take place in English, in some cases they might need to say 'Hello' in a different languages. In this case, we might like to have a parameter with a default value ("Hi") but allow the programmer to override that with a different value (e.g. "Bonjour").
Here's how that works:
In [ ]:
def printInternational(name, surname, greeting="Hi"):
print(greeting + "! My name is "+ name + " " + surname)
printInternational("Ada", "Lovelace")
printInternational("Charles", "Babbage")
printInternational("Laurent", "Ribardière", "Bonjour")
printInternational("François", "Lionet", "Bonjour")
printInternational("Alan", "Turing")
printInternational("Harsha","Suryanarayana", "Namaste")
So we only have to provide a value for a parameter with a default setting if we want to change it for some reason.
Up to here we've only had a function that printed out whatever we told it to. Of course, that's pretty limited and there are a lot of cases where we would want the function to do something and then come back to us with an answer! And remember that the problem of variable scoping means that variables declared inside a function aren't visible to the rest of the program.
So if you want to access a value calculated inside a function then you have to explicitely return it using the reserved keyword return
:
In [ ]:
def sumOf(firstQuantity, secondQuantity):
return firstQuantity + secondQuantity
print(sumOf(1,2))
print(sumOf(109845309234.30945098345,223098450985698054902309342.43598723900923489))
In [ ]:
returnedValue = sumOf(4, 3)
# Notice the casting from int to str!
print(f"This is the returned value: {returnedValue}")
One important thing to remember is that return
always marks the end of the list of instructions in a function. So whatever code is written below return
and yet still indented in the function scope won't be executed:
def genericFunc(parameter):
# do something to parameter
# ...
# do something else..
# ...
return
print("this line won't be ever executed! how sad!")
print("nope. this won't either, sorry.")
In [ ]:
def printNumbers():
print(2)
print(5)
return
print(9999)
print(800000)
printNumbers()
5 is the last value printed becayse a return
statement ends the execution of the function, regardless of whether a result (i.e. a value following the return
keyword on the same line ) to the caller.
Now that you have seen a bit more what is happening in a function, we can combine some concepts that we have seen in previous notebooks to produce interesting bits of code. Take a look at how I've combined the range
function, and the for in
loop to print only the odd numbers for a given range.
In [ ]:
def oddNumbers(inputRange):
"""
A function that prints only the odd numbers for a given range from 0 to inputRange.
inputRange - an integer representing the maximum of the range
"""
for i in range(inputRange):
if i%2 != 0:
print(i)
oddNumbers(10)
print("And...")
oddNumbers(15)
help(oddNumbers)
Let's take a closer look at what's happening above...
def oddNumbers(inputRange):
"""
A function that prints only the odd numbers for a given range from 0 to inputRange.
inputRange - an integer representing the maximum of the range
"""
for i in range(inputRange):
if i%2 != 0:
print(i)
This defines a new function called oddNumbers
which takes one parameter – it's not immediately clear what type of variable inputRange
is, but we can guess it pretty quickly from what happens next.
You'll notice that there's are some lines immediately after the function definition (between the triple-quotes) that aren't printed or obviously used, but that look like documentation of some sort. We'll come back to that in a minute.
The next line is a simple for
loop: for i in range(inputRange)
. The range
function generates a list of numbers from 0 to the input parameter passed to it. So we are going to be running a loop from 0 to n (where n=inputRange
) and assigning the result of that to i
.
The next line is nested inside the for loop: so we take each i
in turn and perform the modulo calculation on it: if i%2
is 0
then i
is divisble by 2. It's even. If it's not equal to 0
then it's not an even number, and in that case we'll print it out.
Which is exactly what happens with:
oddNumbers(10)
oddNumbers(15)
The last line is something new:
help(oddNumbers)
If you look at the output of this, you'll see that it prints out the content we wrote into the triple-quotes in the function definition. So if you want to give your function some documentation that others can access, this is how you do it. In fact, this is how every function in Python should be documented.
Try these (and others) in the empty code block below:
help(len)
help(str)
myList = [1,2,3]
help(myList.append)
In [ ]:
help(len)
myList = [1,2,3]
help(myList.append)
In [ ]:
#your code here
In [ ]:
def oddNumbers(inputRange):
for i in range(inputRange):
if i%2 != 0:
print(i)
else:
print("Yuck, an even number!")
oddNumbers(8)
In [ ]:
def addTwo(param1):
return param1 + 2
def multiplyByThree(param1): # Note: this is a *separate* variable from the param1 in addTwo() because of scoping!
return param1 * 3
# you can use multiplyByThree
# with a regular argument as input
print(multiplyByThree(2))
# but also with a function as input
print(multiplyByThree(addTwo(2)))
# And then
print(addTwo(multiplyByThree(2)))
In [ ]:
# London's total population
london_pop = 7375000
# list with some of London's borough. Feel free to add more!
london_boroughs = {
"City of London": {
"population": 8072,
"coordinates" : [-0.0933, 51.5151]
},
"Camden": {
"population": 220338,
"coordinates" : [-0.2252,1.5424]
},
"Hackney": {
"population": 220338,
"coordinates" : [-0.0709, 51.5432]
},
"Lambeth": {
"population": 303086,
"coordinates" : [-0.1172,51.5013]
}
}
Now, fix the code in the next cell to use the variables defined in the last cell. The calcProportion
function should return the proportion of the population that the boro
borough composes of London. The getLocation
function should return the coordinates of the boro
borough.
In [ ]:
def calcProportion(boro,city_pop=???):
return ???['population']/???
def getLocation(???):
return boro[???]
In [ ]:
#in this function definition we provide a default value for city_pop
#this makes sense here because we are only dealing with london
def calcProportion(boro,city_pop=7375000):
return boro['population']/city_pop
def getLocation(boro):
return boro['coordinates'] #returns the value for the `coordinates` key from the value for the `Lambeth` key
Write some code to print the longitude of Lambeth. This could be done in a single line but don't stress if you need to use more lines...
In [ ]:
In [ ]:
#one-liner (see if you can understand how it works)
print(getLocation(london_boroughs['Lambeth'])[0])
# A longer but possibly more user-friendly way:
coord = getLocation(london_boroughs['Lambeth'])
long = coord[0]
print(long)
Write some code to print the proportion of the London population that lives in the City of London. Using the function defined above, this should take only one line of code.
In [ ]:
In [ ]:
print(calcProportion(london_boroughs['City of London']))
Write code to loop over the london_boroughs
dictionary, use the calcProportion
and getLocation
functions to then print proportions and locations of all the boroughs.
In [ ]:
In [ ]:
for boro, data in london_boroughs.items():
prop = calcProportion(data)
location = getLocation(data)
print(prop)
print(location)
print("")
#to print more nicely you could use string formatting:
#print("Proportion is {0:3.3f}%".format(prop*100))
#print("Location of " + boro + " is " + str(location))
The following individuals have contributed to these teaching materials:
The content and structure of this teaching project itself is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license, and the contributing source code is licensed under The MIT License.
Supported by the Royal Geographical Society (with the Institute of British Geographers) with a Ray Y Gildea Jr Award.
This notebook may depend on the following libraries: None