Python Introduction
Lecture 1
Python
- Python is a high-level programming language created in 1991.
- While it is an old language, its become vastly popular thanks to its use in data science and other mathematics-based disciplines. While also being able to perform tasks such as GUI, web-development and much more.
- Because the language is high-level and interpreted, programmers can often find themselves more productive in Python than in other languages such as say C++.
A first program
We’re going to start with the ‘Hello, World’ program that prints Hello, World!
to the screen. In python this is as simple as writing:
print("Hello, World!") # this prints: Hello, World!
Results:
# => Hello, World!
NOTE anything following a #
is a comment and is completely ignored by the computer. It is there for you to document your code for others, and most importantly, for yourself.
Running this program
Before we can run this program, we need to save it somewhere. For this, will create a new file, insert this text, and save it as <filename>.py
, where <filename>
is what we want to call the script. This name doesn’t matter for its execution.
Once we have created the script, we can run it from the command line. We will get into the command line in a later lecture, but right now all you need to know is:
python3 <filename>.py
An alternative method of running python
You may notice that if you don’t give python
a filename to run, you will enter something called the REPL
.
Python 3.9.5 (default, Jun 4 2021, 12:28:51)
[GCC 7.5.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
REPL
stands for READ
, EXECUTE
, PRINT
, LOOP
.
Variables
A variable is a symbol associated with a value. This value can differ widely, and we will take a look at different types of values/data later.
Neverthless, variables are useful for referring to values and storing to the results of a computation.
= 1
x = 2
y = x + y
z print(z) # prints: 3
# variables can be /overwritten/
= "hello, world"
z print(z) # prints: hello, world
Results:
# => 3
# => hello, world
Types of data
Primitive data types
Primitive data types are the most fundamental parts of programming, they cannot be broken down.
"Hello" # string
1 # integer
1.0 # float
True # Boolean (or bool for short)
We can get the type of some data by using the type(...)
function. For example,
print(type(5))
print(type(5.0))
= "all cats meow"
x
print(type(x))
Results:
# => <class 'int'>
# => <class 'float'>
# => <class 'str'>
Basic Math with primitives
Using these primitive data types, we can do some basic math operations!
print(1 + 2) # Addtion
print(1 - 2) # Subtraction
print(1 * 2) # Multiplication
print(1 / 2) # Division
print(2 ** 2) # Exponent
print(3 % 2) # Modulo operator
Results:
# => 3
# => -1
# => 2
# => 0.5
# => 4
# => 1
Sometimes types get converted to the same type:
print(1.0 + 2) # float + integer = float
Results:
# => 3.0
Even more interesting is with Booleans!
True + True
Results:
# => 2
BODMAS in Python
Like in mathematics, certain math operator take precedence over others.
- B - Brackets
- O - Orders (roots, exponents)
- D - division
- M - multiplication
- A - addition
- S - subtraction.
To make the context clear as to what operations to perform first, use brackets.
5 / 5) + 1
(5 / (5 + 1)
Results:
# => 2.0
# => 0.8333333333333334
Basic Math – Quick exercise
Write the following equation in python:
\((5 + 2) \times (\frac{10}{2} + 10)^2\)
Remember to use parentheses ( )
to ensure that operations take precedence over others.
Your answer should come out as: 1575.0
Working with Strings
Formatting strings
In many previous examples when we’ve printed strings, we’ve done something like:
= 35
age
print("The value of age is", age)
Results:
# => The value of age is 35
While this works in this small context, it can get pretty cumbersome if we have many variables we want to print, and we also want to change how they are displayed when they are printed.
We’re going to take a look now at much better ways of printing.
Better ways of printing strings - %
The first method is using %
. When we print, we first construct a string with special delimiters, such as %s
that denotes a string, and %d
that denotes a number. This is telling Python where we want the values to be placed in the string.
Once we’ve created the string, we need to specify the data, which we do with % (...)
. Like, for example:
= 35
age = "John"
name
print("%d years old" % age) # no tuple for one variable
print("%s is %d years old" % (name, age))
Results:
# => 35 years old
# => John is 35 years old
Here we are specifying the a string %s
and number %d
, and then giving the variables that correspond with that data type.
The special delimiters correspond with a data type. Here are some of the most common:
%s
– For strings%d
– For numbers%f
– For floating point numbers.
There are others such as %x
that prints the hexadecimal representation, but these are less common. You can find the full list at: https://docs.python.org/3/library/stdtypes.html#old-string-formatting
When using these delimiters, we can add modifiers to how they format and display the value. Take a very common example, where we have a floating point value, and, when printing it, we only want to print to 3 decimal places. To accomplish this, we again use %f
but add a .3
to between the %
and f
. In this example, we are printing π to 3 decimal places.
print("Pi to 3 digits is: %.3f" % 3.1415926535)
Results:
# => Pi to 3 digits is: 3.142
In the previous example, we used .3
to specify 3 decimal places. If we put a number before the decimal, like 10.3
we are telling Python make this float occupy 10 spaces and this float should have 3 decimal places printed. When it gets printed, you will notice that it shifts to the right, it gets padded by space. If we use a negative number in front of the decimal place, we are telling python to shift it to the left.
print("Pi to 3 digits is: %10.3f" % 3.1415926535)
print("Pi to 3 digits is: %-10.3f" % 3.1415926535)
Results:
# => Pi to 3 digits is: 3.142
# => Pi to 3 digits is: 3.142
The final method of formatting strings is a newcomer within the language, it is the so-called f-string
. Where a f
character is prefixed to the beginning of the string you’re creating. f-string
’s allow you to use Python syntax within the string (again delimited by {}
.
Take this for example where we are referencing the variables name
and age
directly.
= "Jane"
name = 35
age
print(f"{name} is {age} years old")
Results:
# => Jane is 35 years old
f-string
’s allow you to execute Python code within the string. Here we are accessing the value from the dictionary by specifying the key within the string itself! It certainly makes it a lot easier, especially if we only need to access the values for the string itself.
= {"name": "Jane", "age": 35}
contact_info
print(f"{contact_info['name']} is {contact_info['age']} years old")
Results:
# => Jane is 35 years old
We can still format the values when using f-string
. The method is similar to those using the %f
specifiers.
= 3.1415926535
pi print(f"Pi is {pi:.3f} to 3 decimal places")
Results:
# => Pi is 3.142 to 3 decimal places
Many more examples can be found at: https://zetcode.com/python/fstring/
Splitting strings
Apart from formatting, there are plenty more operations we can perform on strings. We are going to highlight some of the most common here.
The first we’re going to look at is splitting a string by a delimiter character using the .split()
method. If we don’t pass any argument to the .split()
method, then by default, it will split by spaces. However, we can change this by specifying the delimiter.
= "This is a sentence, where each word is separated by a space"
my_string
print(my_string.split())
print(my_string.split(","))
Results:
# => ['This', 'is', 'a', 'sentence,', 'where', 'each', 'word', 'is', 'separated', 'by', 'a', 'space']
# => ['This is a sentence', ' where each word is separated by a space']
Joining strings together
As .split()
splits a single string into a list, .join()
joins a list of strings into a single string. To use .join()
, we first create a string of the delimiter we want to use to join the list of strings by. In this example we’re going to use "-"
. Then we call the .join()
method, passing the list as an argument.
The result is a single string using the delimiter to separate the items of the list.
= ['This', 'is', 'a', 'sentence,', 'where', 'each', 'word', 'is', 'separated', 'by', 'a', 'space']
x
print("-".join(x))
Results:
# => This-is-a-sentence,-where-each-word-is-separated-by-a-space
Changing cases
Other common operations on strings involve change the case. For example:
- Make the entire string uppercase or lowercase
- Making the string title case (every where starts with a capital letter).
- Stripping the string by removing any empty spaces either side of the string.
Note we can chain many methods together by doing .method_1().method_2()
, but only if they return string. If they return None
, then chaining will not work.
= " this String Can change case"
x
print(x.upper())
print(x.lower())
print(x.title())
print(x.strip())
print(x.strip().title())
Results:
# => THIS STRING CAN CHANGE CASE
# => this string can change case
# => This String Can Change Case
# => this String Can change case
# => This String Can Change Case
Replacing parts of a string
To replace a substring, we use the .replace()
method. The first argument is the old string you want to replace. The second argument is what you want to replace it with.
= "This is a string that contains some text"
x
print(x.replace("contains some", "definitely contains some"))
Results:
# => This is a string that definitely contains some text
Compound data structures
Container data types/Data structures
Container data types or data structures, as the name suggests, are used to contain other things. Types of containers are:
- Lists
- Dictionaries
- Tuples
- Sets
1, "hello", 2] # list
["my-key": 2, "your-key": 1} # dictionary (or dict)
{1, 2) # tuple
(set(1, 2) # set
We’ll take a look at each of these different container types and explore why we might want to use each of them.
An aside on Terminology
To make our explanations clearer and reduce confusion, each of the different symbols have unique names.
I will use this terminology consistently throughout the course, and it is common to see the same use outside the course.
[ ]
brackets (square brackets).{ }
braces (curly braces).( )
parentheses.
Lists
A hetreogenious container. This means that it can store any type of data.
= [1, "hello", 2] x
Elements can be accessed using indexing [ ]
notation. For example:
print(x[0]) # this will get the first element (i.e. 1)
print(x[1]) # the second element (i.e. "hello")
print(x[2]) # the third element (i.e. 2)
Results:
# => 1
# => hello
# => 2
notice how the first element is the 0-th item in the list/ we say that python is 0-indexed.
Slices
If we wanted to access an element from a data structure, such as a list, we would use the [ ]
accessor, specifying the index of the element we wish to retrieve (remember that indexes start at zero!). But what if we ranted to access many elements at once? Well to accomplish that, we have a slice or a range of indexes (not to be confused with the range
function). A slice is defined as:
start_index:end_index
where the end_index
is non inclusive – it doesn’t get included in the result. Here is an example where we have a list of 6 numbers from 0 to 5, and we slice the list from index 0 to 3. Notice how the 3rd index is not included.
= [0, 1, 2, 3, 4, 5]
x print(x[0:3])
Results:
# => [0, 1, 2]
Ranges
When we use start_index:end_index
, the slice increments by 1 from start_index
to end_index
. If we wanted to increment by a different amount we can use the slicing form:
start_index:end_index:step
Here is an example where we step the indexes by 2:
= list(range(100))
x print(x[10:15:2])
Results:
# => [10, 12, 14]
Reverse
One strange fact about the step is that if we specify a negative number for the step, Python will work backwards, and effectively reverse the list.
= list(range(5))
x
print(x[::-1])
Results:
# => [4, 3, 2, 1, 0]
In a previous example, I created a slice like 0:3
. This was a little wasteful as we can write slightly less code. If we write :end_index
, Python assumes and creates a slice from the first index (0) to the end_index
. If we write start_index:
, Python assumes and creates a slice from start_index
to the end of the list.
= list(range(100))
x
print(x[:10])
print(x[90:])
Results:
# => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# => [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
Indexing backwards
Finally, we also work backwards from the end of list. If we use a negative number, such as -1, we are telling Python, take the elements from the end of the list. -1 is the final index, and numbers lower than -1 work further backwards through the list.
= list(range(100))
x
print(x[-1])
print(x[-2])
Results:
# => 99
# => 98
Slicing with negative indexes, also works. Here we are creating a slice from the end of the list - 10, to the last (but not including) index.
= list(range(100))
x
print(x[-10:-1])
Results:
# => [90, 91, 92, 93, 94, 95, 96, 97, 98]
Adding data to a list
If we want to add items to the end of the list, we use the append
function:
= []
my_list
"all")
my_list.append("dogs")
my_list.append("bark")
my_list.append(
print(my_list)
Results:
# => ['all', 'dogs', 'bark']
Dictionaries
Dictionaries are a little different from lists as each ‘element’ consists of a key-pair value. Let’s have a look at some examples where the dictionaries contains one element:
= {"key": "value"}
my_dictionary = {"age": 25} my_other_dict
To access the value, we get it using [key]
notation:
"age"] my_other_dict[
Results:
# => 25
NOTE keys are unique, i.e:
= {"age": 25, "age": 15}
my_dictionary "age"] my_dictionary[
Results:
# => 15
The key in the dictionary doesn’t necessarily need to be a string. For example, in this case, we have created two key-pair elements, where the keys to both are tuples of numbers.
= {(1, 2): "square", (3, 4): "circle"}
my_dictionary
print(my_dictionary[(1, 2)])
Results:
# => square
adding data
If we want to add data to a dictionary, we simply perform the accessor method with a key that is not in the dictionary:
= {}
my_dict
"name"] = "James"
my_dict["age"] = 35
my_dict[
print(my_dict)
Results:
# => {'name': 'James', 'age': 35}
Quick Exercise
- Create a dictionary for the following address, and assign it a variable name called
address
:
Key | Value |
---|---|
number | 22 |
street | Bakers Street |
city | London |
- Print out the address’s street name using the
[ ]
accessor with the correct key.
Tuples
= (1, 56, -2) my_tuple
Like lists, elements of the tuple can be accessed by their position in the list, starting with the 0-th element:
print(my_tuple[0]) # => 1
print(my_tuple[1]) # => 56
print(my_tuple[2]) # => -2
Results:
# => 1
# => 56
# => -2
Unlike lists, tuples cannot be changed after they’ve been created. We say they are immutable. So this will not work:
2] = "dogs" # creates an Error my_tuple[
Results:
# => Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/tmp/pyKdIIcx", line 18, in <module>
File "<string>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
Sets
Sets in Python are like tuples, but contain only unique elements.
You can use the set( )
function (more on functions later!), supplying a list, to create a set:
= set([1, 2, 2, 2, 3, 4])
my_set my_set
Results:
# => {1, 2, 3, 4}
Notice how there is only one ‘2’ in the resulting set, duplicate elements are removed.
adding data
If we want to add data to a set, we use the .add()
method. The element used as an argument to this function will only be added to the set if it is not already in the set.
= set([])
my_set
1)
my_set.add(2)
my_set.add(1)
my_set.add(
print(my_set)
Results:
# => {1, 2}
Conditional expressions
If statement
If statements allow for branching paths of execution. In other words, we can execute some statements if some conditions holds (or does not hold).
The structure of a simple if statement is:
if <condition>:
<body>
= 2
x = "stop"
y
if x < 5:
print("X is less than five")
if y == "go":
print("All systems go!!")
Results:
# => X is less than five
In the previous example, the first print
statement was only executed if the x < 5
evaluates to True
, but in python, we can add another branch if the condition evaluates to False
. This branch is denoted by the else
keyword.
= 10
x
if x < 5:
print("X is less than five")
else:
print("X is greater than or equal to five")
Results:
# => X is greater than or equal to five
does it contain a substring?
We can check if a string exists within another string using the in
keyword. This returns a Boolean value, so we can use it as a condition to an if
statement.
= "This is a string that contains some text"
x
if "text" in x:
print("It exists")
Results:
# => It exists
If statement – Quick Exercise
- Create a variable called
age
and assign the value of this variable35
. - Create and
if
statement that prints the square ofage
if the value ofage
is more than 24. - This if statement should have an else condition, that prints
age
divided by 2. - What is the printed value?
Multiple paths
If we wanted to add multiple potential paths, we can add more using the elif <condition>
keywords.
Note: The conditions are checked from top to bottom, only executing the else if none evaluate to True
. The first condition that evaluates to True
is executed, the rest are skipped.
x = 15
if x < 5:
print("X is less than five")
elif x > 10:
print("X is greater than ten")
else:
print("X is between five and ten")
Results:
# => X is greater than ten
Inline if-statements
Sometimes, we might want to conditionally set a variable a value. For this, we can use an inline if statement. The form of an inline if statement is:
<value-if-true> if <condition> else <value-if-false>
x = 10
y = 5 if x > 5 else 2
print(x + y)
Results:
# => 15
Boolean Logic
As we’ve seen, if
statements are checking for conditions to evaluate to True
or False
. In python we use various comparison operators to check for conditions that evaluate to Booleans
.
Comparison operators
<
less than<=
less than or equal to>
greater than>=
greater than or equal to==
is equal tonot
negation
If we want to check for multiple conditions, we can use conjunctives or disjunctive operators to combine the Boolean formulas.
Conjunctives/Disjunctives
and
all boolean expressions must evaluate to trueor
only one expression needs to be true
Not
Using not
you can invert the Boolean result of the expression.
print(not True)
Results:
# => False
x = 10
if not x == 11:
print("X is not 11")
Results:
# => X is not 11
And
Let’s take an example using the and
keyword. and
here is checking that x
is above or equal to 10 and y
is exactly 5. If either of the conditions is False
, python will execute the else
path (if there is one, of course!).
x = 10
y = 5
if x >= 10 and y == 5:
z = x + y
else:
z = x * y
print(z)
Results:
# => 15
Or
Here we see the use of the or
keyword. If any of the conditions evaluates to True
then the whole condition evaluates to True
.
x = 10
y = 5
if x < 5 or y == 5:
print("We got here!")
else:
print("We got here instead...")
Results:
# => We got here!
Note: or
is short-circuiting. This means that if tests the conditions left-to-right, and when it finds something that is True
it stops evaluating the rest of the conditions.
x = 10
if x < 20 or print("We got to this condition"):
print("The value of x is", x)
Results:
# => The value of x is 10
Combining And and Or
If your Boolean logic refers to a single variable, you can combine the logic without the and
and or
. But its not always common.
For example,
x = 7
if x < 10 and x > 4:
print("X is between 5 and 10")
Can be the same as:
x = 7
if 5 < x < 10:
print("X is between 5 and 10")
Results:
# => X is between 5 and 10
Iteration
For loop
Looping or iteration allows us to perform a series of actions multiple times. We are going to start with the more useful for
loop in python. The syntax of a for
loop is:
for <variable_name> in <iterable>:
<body>
for i in range(3):
print(i)
Results:
# => 0
# => 1
# => 2
break
The previous example loops over the body a fix number of times. But what if we wanted to stop looping early? Well, we can use the break
keyword. This keyword will exit the body of the loop.
for i in range(10):
if i > 5:
break
print(i)
Results:
# => 0
# => 1
# => 2
# => 3
# => 4
# => 5
continue
A different keyword you might want to use is continue
. Continue allows you to move/skip onto the next iteration without executing the entire body of the for
loop.
for i in range(10):
if i % 2 == 0:
continue
print(i)
Results:
# => 1
# => 3
# => 5
# => 7
# => 9
ranges
Instead of using continue
like in the previous slide, the range
function provides us with some options:
range(start, stop, step)
In this example, we are starting our iteration at 10, ending at 15, but stepping the counter 2 steps.
for i in range(10, 15, 2):
print(i)
Results:
# => 10
# => 12
# => 14
Loop over collections
For loops allow us to iterate over a collection, taking one element at a time. Take for example, a list, and for every item in the list we print its square.
my_list = [1, 5, 2, 3, 5.5]
for el in my_list:
print(el**2)
Results:
# => 1
# => 25
# => 4
# => 9
# => 30.25
This kind of looping can work for tuples and sets, but as we have seen, dictionaries are a little different. Every ‘element’ in a dictionary consists of a key and a value. Therefore when we iterate over items in a dictionary, we can assign the key and value to different variables in the loop.
Note the use of the .items()
after the dictionary. We will explore this later.
my_dict = {"name": "jane", "age": 35, "loc": "France"}
for el_key, el_val in my_dict.items():
print("Key is:", el_key, " value is: ", el_val)
Results:
# => Key is: name and the value is: jane
# => Key is: age and the value is: 35
# => Key is: location and the value is: France
We could also loop over the keys in the dictionary using the .keys()
method instead of .items()
.
my_dict = {"name": "jane", "age": 35, "loc": "France"}
for the_key in my_dict.keys():
print(the_key)
Results:
# => name
# => age
# => loc
Or, the values using .values()
.
my_dict = {"name": "jane", "age": 35, "loc": "France"}
for the_value in my_dict.values():
print(the_value)
Results:
# => jane
# => 35
# => France
List comprehensions
We have seen previously how for
loops work. Knowing the syntax of a for
loop and wanting to populate a list with some data, we might be tempted to write:
x = []
for i in range(3):
x.append(i)
print(x)
Results:
# => [0, 1, 2]
While this is perfectly valid Python code, Python itself provides ‘List comprehensions’ to make this process easier.
x = [i for i in range(3)]
The syntax of a list comprehensions is:
[ <variable> for <variable> in <iterable> ]
We can also perform similar actions with a dictionary
[ <key>, <value> for <key>, <value> in <dictionary.items()> ]
using if
’s
Perhaps we only want to optionally perform an action within the list comprehension? Python allows us to do this with the inline if
statement we’ve seen in the previous lecture.
x = [i if i < 5 else -1 for i in range(7)]
print(x)
Results:
# => [0, 1, 2, 3, 4, -1, -1]
We add the inline <var> if <condition> else <other-var>
before the for
loop part of the comprehension.
There is another type of if
statement in a list comprehension, this occurs when we don’t have an else
.
x = [i for i in range(7) if i < 3]
print(x)
Results:
# => [0, 1, 2]
In this example, we’re only ‘adding’ to the list if the condition (\(i < 3\)) is true, else the element is not included in the resulting list.
multiple for
’s
If we like, we can also use nested for loops by simply adding another for loop into the comprehension.
x = [(i, j) for i in range(2) for j in range(2)]
print(x)
Results:
# => [(0, 0), (0, 1), (1, 0), (1, 1)]
In this example, we’re creating a tuple for each element, effectively each combination of 1 and 0.
List comprehensions with dictionary
Python doesn’t restrict us to list comprehensions, but we can do a similar operation to create a dictionary.
x = [2, 5, 6]
y = {idx: val for idx, val in enumerate(x)}
print(y)
Results:
# => {0: 2, 1: 5, 2: 6}
Here, every item in x
has been associated with its numerical index as a key thanks to the enumerate
function that returns both the index and value at iteration in the for loop.
For loop – Quick Exercise
- Create a list of elements:
- 2
- “NA”
- 24
- 5
- Use a
for
loop to iterate over this list. - In the body of the
for
loop, compute \(2x + 1\), where \(x\) is the current element of the list. - Store the result of this computation in a new variable \(y\), and then print y.
Note You cannot compute \(2x + 1\) of “NA”, therefore you will to use an if
statement to skip onto the next iteration if it encounters this. Hint try: type(...) =!=
str
While loop
A while
loop is another looping concept like for
but it can loop for an arbitrary amount of times. A while
loop looks to see if the condition is True
, and if it is, it will execute the body.
The syntax of the while loop is:
while <condition>:
<body>
i = 0
while i < 3:
print(i)
i = i + 1
Results:
# => 0
# => 1
# => 2
x = 0
y = 1
Here is another example:
while x + y < 10:
print("X is,", x, "and y is", y)
x = x + 1
y = y * 2
print("X ended as", x, ", while y is", y)
Results:
# => X is, 0 and y is 1
# => X is, 1 and y is 2
# => X is, 2 and y is 4
# => X ended as 3 , while y is 8
Functions
Functions are a re-usable set of instructions that can take some arguments and possible return something.
The basic structure of a function is as follows:
def <function_name>(args*):
<body>
(optional) return
args*
are 0 to many comma separated symbols.body
is to be indented by 4 spaces.
This is only the function definition however. To make it do something, we must ‘call’ the function, and supply the arguments as specified in the definition.
def say_hello(): # function definition
print("Hello, World!")
say_hello() # calling the function
We’ve already seen some functions provided by Python.
print
itself is a function with a single argument: what we want to print.
print("Hello, World!")
# ^ ^
# | |
# | user supplied argument
# |
# function name
set
is another function that takes a single argument: a collection of data with which to make a set:
set([1, 2, 2, 3, 4])
Example usage of a function
Let’s make a function that takes two numbers and adds them together:
def my_addition(a, b):
result = a + b
return result
x = 2
y = 3
z = my_addition(2, 3) # return 5 and stores in z
print(z)
Results:
# => 5
Quick exercise
- Create a function called
my_square
. This function should take one argument (you can call this argument what you like). - The body of the function should compute and return the square of the argument.
- Call this function with
5.556
. - Store the result of calling this function, and print it.
- What is the result?
Re-usability with Functions
Functions are better illustrated through some examples, so let’s see some!
name_1 = "john"
name_2 = "mary"
name_3 = "michael"
print("Hello " + name_1 + ", how are you?")
print("Hello " + name_2 + ", how are you?")
print("Hello " + name_3 + ", how are you?")
The above is pretty wasteful. Why? Because we are performing the exact same operation multiple times, with only the variable changed.
By abstracting the actions we want to perform into a function, we can ultimately reduce the amount of code we write. Be a lazy programmer!
name_1 = "john"
name_2 = "mary"
name_3 = "michael"
def say_hello(name):
print("Hello " + name + ", how are you?")
say_hello(name_1)
say_hello(name_2)
say_hello(name_3)
In this example, we’ve used the function as defined with the def
pattern to write the print
statement once. Then, we’ve called the function with each variable as its argument.
Named parameters
We’ve seen in previous examples that, when we create a function, we give each of the arguments (if there are any) a name.
When calling this function, we can specify these same names such as:
def say_hello(name):
print("Hello,", name)
say_hello("Micheal")
say_hello(name="Micheal")
Results:
# => Hello, Micheal
# => Hello, Micheal
By specifying the name of the parameter we’re using with the called function, we can change the order
def say_greeting(greeting, name):
print(greeting, name, "I hope you're having a good day")
say_greeting(name="John", greeting="Hi")
Results:
# => Hi John I hope you're having a good day
Optional/Default/Positional arguments
When we call a function with arguments without naming them, we are supplying them by position.
def say_greeting(greeting, name):
print(greeting, name, "I hope you're having a good day")
say_greeting(#first position, #section position)
The first position gets mapped to variable name of greeting
inside the body of the say_greeting
function, while the second position gets mapped to name
.
Sometimes when creating a function we may want to use default arguments, these are arguments that are used if the call to the function does not specify what their value should be. For example.
def say_greeting(name, greeting="Hello"):
print(greeting, name, "I hope you're having a good day")
say_greeting("John")
say_greeting("John", "Hi") # supply greeting as positional argument
Results:
# => Hello John I hope you're having a good day
# => Hi John I hope you're having a good day
Note if you supply a default argument in the function definition, all arguments after this default argument must also supply a default argument.
So, this won’t work:
def say_greeting(name="Jane", greeting):
print(greeting, name, "I hope you're having a good day")
say_greeting("John", "Hi")
Recap on arguments
# defining the function
def say_greeting(name, greeting) # no default arguments
def say_greeting(name, greeting="Hello") # greeting is a default argument
def say_greeting(name="Jane", greeting="Hello") # both arguments have a default
# calling the functions
say_greeting("John", "Hi") # both arguments are provided by position
say_greeting(name="John", greeting="Hi") # arguments are supplied by name
say_greeting(greeting="Hi", name="John") # the position of named arguments do not matter
Function doc-strings
To make it clear for a human to quickly understand what a function is doing, you can add an optional doc-string. This is a string that is added directly after the initial definition of the function:
def my_function(x, y):
"""I am a docstring!!!"""
return x + y
Some common use cases for docstrings are explaining what the parameters are that it expects, and what it returns.
If your explanation is a little longer than a line, a multiline docstring can be created as long as you’re using """
three quotation marks either side of the string
def my_function(x, y):
"""
This is my realllly long docstring
that explains how the function works. But sometimes
its best not to explain the obvious
"""
return x + y
Understanding scope
In this example we have two scopes which can be easily seen by the indentation. The first is the global scope. The second scope is the scope of the function. The scope of the function can reference variables in the larger scope. But once the function scope exits, we can no longer reference the variables from the function.
x = 10
def compute_addition(y):
return x + y
print(compute_addition(10))
print(x)
print(y) # does not work
Results:
# => 20
# => 10
Even though we can reference the global scope variable from the scope of the function, we can’t modify it like this:
x = 10
def compute_addition_2(y):
x = x + 5 # error local variable referenced before assignment
return x + y
print(compute_addition_2(10))
If we really wanted to reference a variable in a global scope and modify its value, we could use the global
keyword. Doing this makes the function output something different every time it is called. This can make it difficult to debug incorrect programs.
x = 10
def compute_addition_2(y):
global x
x = x + 5
return x + y
print(compute_addition_2(10))
print(x)
print(compute_addition_2(10))
Results:
# => 25
# => 15
# => 30
In almost all cases, avoid using global variables. Instead pass the variables as parameters. This can reduce a source of potential errors and ensure that if a function is called multiple times, the output can be more consistent and expected.
x = 10
def compute_addition_3(x, y):
x = x + 5
return x + y
print(compute_addition_3(x, 10))
print(x)
print(compute_addition_3(x, 10))
Results:
# => 25
# => 10
# => 25
Exercise
Library system
Use what you’ve learnt!
We’re going to create a library system to help locate and lookup information about books. For example, we want to know the author of book called ‘Moby Dick’.
To create this system, we are going to do it in stages. First, we will want to create our database of books:
Title | Author | Release Date |
---|---|---|
Moby Dick | Herman Melville | 1851 |
A Study in Scarlet | Sir Arthur Conan Doyle | 1887 |
Frankenstein | Mary Shelley | 1818 |
Hitchhikers Guide to the Galaxy | Douglas Adams | 1879 |
Our database is going to be a list of dictionaries. Where each dictionary is a row from this table. For example, one of the dictionaries will have the key “title” and a value “Moby Dick”.
Create this database and call it db
.
Locating Books
- Create a function called
locate_by_title
that takes the database to look through, and the title to look up as arguments. - This function should check each dictionary, and if the title is the same as what was searched for, it should return the whole dictionary.
- Test this function by calling the
locate_by_title
function withdb
and"Frankenstein"
. You should get{"title": "Frankenstein", "author": ...}
.
Note you should include docstrings to describe the arguments to the function, and what it will return.
Selecting a subset
Now that we can find books by the title name, we also want to find all books that were released after a certain data.
- Create a function called
books_released_after
that takes two arguments: the database to look through, and the year. - This function should look through the database, if it finds a book that was released after the year, it should add it to a list of books that is returned from this function.
- Test this function by calling
books_released_after
withdb
and1850
. This function call should return a list containing three dictionaries. The first entry should be ‘Moby Dick’ and the section should be ‘A Study in Scarlet’, etc.
Updating our database
Oh no! ‘Hitchhikers Guide to the Galaxy’ was released in 1979 not 1879, there must have been a typo. Let’s create a function to update this.
Create a function called
update
, that takes 5 arguments: 1) the database to update, 2) the key of the value we want to update 3) the value we want to update it to 4) the key we want to check to find out if we have the correct book and 5) the value of the key to check if we have the correct book.update(db, key="release year", value=1979, where_key="title", where_value="Hitchhikers Guide to the Galaxy")
Extended exercise
In the previous steps we created functions
locate_by_title
andbooks_released_after
. These two functions are similar in a way that they are selecting a subset of our database (just by different criteria).For this harder exercise, can we create a single function called
query
that allows us to do bothlocate_by_title
andbooks_released_after
.An example call to this
query
function may look like:results = query(db, where_key="title", where_value="Moby Dick", where_qualifier="exactly")
where_qualifier
should accept strings like"exactly"
,"greater than"
, and"less than"
.