Programming in Python
Please note this is still under active development. Please contact me if you spot any errors.
by Sam O'Neill
This is an introductory set of units for Python that have been created as the notes for the introductory Programming unit of the undergraduate computing course, 4CM523 - Programming.
It is designed to be reasonably comprehensive and should be read in order. Each unit has a number of lessons each with their own exercise.
I hope you enjoy the course
— Sam
Executing Code and Completing Exercises
If you are taking this course then you will be completing exercises and running code via Visual Studio Code. You will be shown in the labs how to do this. You may see references to Visual Studio Code and main.py in the text, if you prefer to use your own IDE or are comfortable in Python, by all means run the code as you wish.
You can also follow the instructions here.
Alternative ways to run code and do exercises are via the following online Python interpreter - Online GDB Python Compiler.
It is also recommended that you make use of the fantastic Python Tutor. This is an excellent way to view how the code runs step by step. You can also watch a number of excellent videos on their YouTube channel.
Introduction
We will be using the Python programming language in this course.
- Python is a commonly used beginner programming language since it is considered to be more easily readable and has a simpler syntax than other mainstream languages. Syntax refers to the rules of a programming language; similarly to how grammar refers to the rules of a human language.
- Python is also a relatively powerful language. It is used to support YouTube, Google, Instagram, Reddit, and other popular software programs.
- Python has consistently been in the top three most popular programming languages over the past two decades. Other languages that have been consistently popular include Java, C, and Javascript.
In this introductory course, you will be gaining a solid foundation in the Python programming language. The programming concepts you learn will also be helpful for any future programming languages you learn in the future.
How to use this book
You should work through each of the lessons and do the TASK at the end of the lesson.
I would recommend pasting the code snippets into Python Tutor. This is an excellent way to view how the code runs step by step.
You can also refer to the many other books, tutorials and videos found online. An especially useful quick reference is the W3Schools Python Tutorial.
Unit 1 - Getting Started
This unit introduces some things that we need to get up and running. You will learn how to run a basic program, about basic exceptions in Python and about the fundamental data types that Python has to offer - these crucial allow us to handle data in our programs, e.g. storing a number or a persons name.
Hello World
It is customary that the first program that you write in a programming language is the "Hello World" program.
So as not to break this great tradition, let's start our Python journey there!
Python lets you output text to the Terminal.
You can output a piece of text using the print()
function (more on functions later in the course).
Normally we will write out python programs in a source file (script) which is where we will write our code. You will need to create and save this file in Visual Studio Code. Make sure it has the .py
extension.
1. Hello World!
For now, we can output a piece of text to the terminal by putting the following line into a Python file.
print("Hello World!")
Once you have added this line, hit the Run button at the top of Visual Studio Code.
You should then see Hello World!
outputted to the terminal.
1.1 Wow! What Just Happened?
When you hit the "Run" button, Python reads your code from the source file and immediately executes it, showing the result in the terminal. Python is an interpreted language, which means that you don’t need to compile your code before running it.
If you would like to execute manually without the run button. Go to the Terminal and type:
python <NAME OF YOUR FILE>.py
This will then convert your code for execution and then run it.
1.2 Tasks
Throughout this course, you'll encounter tasks that you should complete. These are practical exercises designed to reinforce the concepts you've just learned. Some tasks will be more challenging than others, so focus on completing the easier ones before tackling the harder ones.
A TASK will always look like the one given below.
TASKs will normally appear at the bottom of the page
=== TASK ===
Create a Python program in a new file that outputs "Hello World! is cool!"
to the Terminal using the print()
function.
Comments
In programming, it is important to provide comments in your code to explain what you're doing. This is helpful for both yourself and others who might read your code later.
Comments are not interpreted by Python, so they don't affect how your code runs. Python supports two types of comments:
- Single Line Comments
- Multiple Single-Line Comments
1. Single Line Comments
Single-line comments start with a #
(hash symbol). Everything following the #
on that line will be ignored by Python. For example:
# This is a single-line comment (This won't run).
print("I am learning about comments")
Notice that comments are usually shown in a different color in your code editor.
2. Multiple Single-Line Comments
Python doesn't have a specific syntax for multiline comments. Instead, to comment out multiple lines, you need to place a #
at the start of each line. For example:
# This is a single-line comment.
# This is also a single-line comment.
print("I am learning about comments")
If you highlight multiple lines of code and press Ctrl + /
(or Cmd + /
on macOS), the editor will automatically add a #
at the start of each line, turning them into comments.
Note: While you might see triple quotes ('''
or """
) used for what looks like multiline comments, these are actually docstrings used to document functions or classes. They should not be used for regular comments.
3. A Note on Using Comments
There is some debate about how often to comment, and different programmers have different styles. Here's some basic advice:
3.1 Comment When It’s Useful
For example, this comment is not useful:
# This line prints out Hello World!
print("Hello World!")
Everyone can see what the code does, so this comment doesn’t add value. Only comment when it's not obvious what a piece of code does.
3.2 Be Professional
Your comments reflect on you, especially if others are reading your code. Avoid unprofessional comments, like:
# This code sucks!
print("Hello World!")
or:
# Workaround for Tyrion being a traitor and going off with that dragon lady!
print("Hello World!")
3.3 Keep Comments Brief and Clear
Make sure your comments are concise and to the point. If you need to write a longer explanation, split it into multiple lines. For example:
# The following is a comment that has the job of telling the person reading the source code that this prints Hello World!
print("Hello World!")
would be better as
# Prints Hello World!
print("Hello World!")
or:
# The following is a comment that has the following job.
# To tell the person reading the source code that this prints Hello World!
print("Hello World!")
=== TASK ===
Copy the following code into a new Python file.
# This is a single line comment in Python
print("Replace this line with TASK 1")
print("Hello")
print("World")
print("with Comments")
- Modify the code so that the first print statement outputs:
Single line comments start with #
. - Highlight lines 5-7 and press
Ctrl + /
(orCmd + /
on macOS) to comment out those lines.
Your final output in the terminal should look like this:
Single line comments start with #
Objects, Types, Operators, and Expressions
To do anything useful in our programs, Python will need to represent data such as numbers, words and booleans. We will learn more about these in Units 2 and 3.
For now we will introduce the most common 4 built-in data types.
1. Objects and Types
In Python, everything you work with is an object. An object can be a number, a word, or any other type of data. Each object has a specific type, which tells Python what kind of data it is and what operations can be done with it.
Python has several built-in data types, which it uses to represent data internally. We'll start by looking at four common ones:
Data Type | Description |
---|---|
int | An integer. This is a whole number, it can be positive, negative or zero. e.g. 5 |
float | A decimal number. e.g. 3.14 |
str | Text. It consists of individual characters. Strings are enclosed in single quotation marks ' or double quotation marks " . e.g. "Hello World" |
bool | The values True or False . Used to make decisions, more in Unit 3 |
A very useful function in python that we can use is type()
. The type()
function is a useful tool to check what type of data you're working with. Understanding the type of an object helps you know what operations can be performed on it and how Python will handle it in your program.
x = type(10)
print(x)
Here we are assigning the result of type(10)
to the variable x
. Basically this stores the result in memory in a name called x
. We can then print out what is stored in x
.
You could do this in one line like so.
print(type(10))
We will stick to using x
as it is more readable.
If you run either of these you will see the output:
<class 'int'>
This is python telling you that 10
is an object of type int
. For now, read class as type.
Try it for these other objects.
x = 10.3
print(x)
print(type(x))
x = "This is a string"
print(x)
print(type(x))
x = True
print(x)
print(type(x))
This first gets the type of the object, here a str
and then passes that to the print()
function.
2. Operators and Expressions
We can combine objects with operators to form expressions. When evaluated, these expressions produce a new object.
For example, we can combine the objects 10
and 5
with the +
operator,
x = 10 + 5 # new object 15 assigned to x
print(x) # prints out 15
print(type(x)) # prints out <class 'int'>
to create a new object 15
of type int
.
All built-in data types have operators that we can use to form expressions.
For example, we can test if two expressions are equal with the ==
comparison operator.
x = 10 + 5 == 3 * 5 # assign the result of comparing 10 + 5 and 3 * 5.
print(x) # prints out True
print(type(x)) # prints out <class 'bool'>
This will return True
. Python knows how to test whether two numbers are equal and returns you a new object of type bool
.
We will cover operators for numbers, strings, and bools in their respective lessons in this unit.
3. Help Function
The help()
function gets information about an object (it can also be used for other things.)
Try typing the following into the terminal or a Python file:
help(str)
You will need to click in the terminal and press Ctrl + c
to exit this.
You can also type python into the terminal to enter the Python Interative Shell and then type help(str)
and press Enter. You can open this by typing python
into a terminal.
=== TASK ===
Create a new Python file.
Edit the file to output the type of the following expressions. You will need to use the print()
and the type()
function.
Make sure you have read all of the above before attempting this (especially the end of Section 1).
12 + 5
5 + 3.0
12 / 5
12 + 5 == 5 * 3.0
The output of your program should be:
<class 'int'>
<class 'float'>
<class 'float'>
<class 'bool'>
Basic Exceptions
Every time that you try and run your code, Python will first try to parse your code, if it encounters something that it does not recognise, it will raise an exception.
This is a warning to the coder that they have done something invalid and the program will not run. Please read the exception and try to understand it.
The three main exceptions you may encounter for now will be:
SyntaxError
NameError
TypeError
1. SyntaxError
A SyntaxError
is perhaps the most common kind of complaint you get while you are still learning Python.
It means you have entered something that Python does not understand, this is commonly a spelling mistake or something you have missed.
NOTE: Always pay attention to the error message, it is telling you what is wrong with your code!
Try running the following code:
print(hello world)
You will get the following SyntaxError
because of some missing ""
around the string.
File "<stdin>", line 1
print(hello world)
^
SyntaxError: invalid syntax
2. NameError
A NameError
occurs when a local or global name is not found. This refers to variables, functions, and other things like modules and classes.
Basically, Python reserves particular words such as print
.
Try running the following code:
printf("Hello World!")
You will get the following NameError
because we have misspelled print
.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'printf' is not defined
3. TypeError
A TypeError
occurs when the data types of objects in an operation are invalid. For example, trying to divide a number by a string.
Try running the following code:
100/"5"
You will get the following TypeError
because we are trying to divide /
an int
by a str
. These two types don't play together well!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'int' and 'str'
=== TASK ===
- Copy the following code into a new Python file.
# You need to fix the following lines
# Run the code and then use the error messages to fix each line
# This line has a SyntaxError
print(hello world)
# This line has a NameError
printf("Hello World!")
# The following lines cause a TypeError
int1 = 100
str1 = "10"
print(int1 / str1)
- You will see a
SyntaxError
on line 5 because of some missing""
around the string.
print(hello world)
Fix this so that it prints out hello world
.
- Once you have completed this run the code. You will see a
NameError
on line 8 becauseprintf
is not a valid name.
printf("Hello World!")
Fix this so that it prints out Hello World
.
- Once you have completed this run the code. You will see a
TypeError
on line 13 because we are tring to divide/
anint
by astring
.
int1 = 100
int2 = "10"
print(int1 / int2)
Fix this so that it prints out 10.0
.
References
Indentation, WhiteSpace, and Blank Lines
Python is unusual in that it requires indentation for lines of code. This will make more sense when we do if
statements, while
loops, and for
loops.
For now, all you need to know is:
- Do not put whitespace (spacing or tabs) in front of a line of code.
- Python doesn't care about blank lines.
=== TASK ===
Copy the following code into a new Python file.
# This line is not indented properly. Fix this
print("Indentation matters in Python")
# You will notice that we have blank lines
# Python doesn't care about these!
# There is extra whitespace inside the print statement
# Python doesn't care about this
print("Hello World!" )
Run the code, and you will see an IndentationError
exception on line 2.
This is due to the whitespace at the beginning of the line.
Fix this.
NOTE: You may also see that the code has a red squiggly line. This is highlighting an error!
The Hello User Program
This is a mini-program and is presented to help you understand a concept. This is a very simple program that asks the user to input their name and their age.
It then says hello back to the user.
You will need to create a new file and paste this in to have a play.
name = input("Hello, what is your name?\n")
age = input("What is your age?\n")
print(f"Hello {name}, your age is {age}.")
Key Takeaways
We see 3 things here that are of interest:
- The
input()
function. This asks the user for some input via the terminal. - We assign (
=
) the input to a variable (name
andage
) - We print back out to the user using an f-string.
The next lesson on Simple I/O (Input/Output) will explain these in more detail.
Simple I/O (Input/Output)
For most of this course, we will work with the terminal. For your programs to be of more use, we need to have a way for the user to interact with them.
1. User Input
You can ask the user for input via the terminal using the input()
function.
For example,
input("Please enter something:\n")
will print out Please enter something
and then a blank line (this is because of the \n
escape character in the string).
The user can then enter something. Try this in a Python file.
2. Storing User Input
The above code does not store the input that the user enters. To do this you need to assign it to a variable. Update the Python file as follows:
input_string = input("Please enter something:\n")
print(f"You entered - {input_string}")
Now the input is stored in the variable input_string
and we can use it in our program.
You can think of input_string
as a box that stores the input from the user. We can then get the contents of the box at different points of our program.
The line,
print(f"You entered - {input_string}")
uses a Python f-string, which just lets you put the contents of the variable into the string. Here we put the contents of input_string
in between the curly braces {}
.
We will discuss variables and f-strings a lot more in the next unit.
3. Casting the Input
Whenever you get input from the user, it will be of type str
. Sometimes we wish to convert this to a number or other type. To do this we can cast the variable to another type. We can cast to an int
using the int()
function.
input_string = input("Please enter a number:\n")
x = int(input_string)
print(f"{x} + 5 = {x+5}")
The above code asks for a number, then casts the str
to an int
and then prints the result of adding 5
to the number.
Note that the line print(f"{x} + 5 = {x+5}")
places the contents of x
into the curly braces.
What happens if you don't enter a number? Copy the code into a Python file and have a play with this.
NOTE: We could have done the casting in one line.
x = int(input("Please enter a number:\n"))
print(f"{x} + 5 = {x+5}")
=== TASK ===
Create a new Python file.
Create a simple program that asks the user for a number and then prints out 10 times that number.
If the user enters 3
the program should work as follows:
Please enter a whole number:
3
3x10 = 30
If the user enters 10
the program should work as follows:
Please enter a whole number:
10
10x10 = 100
I have seen students try to solve this with an if
statement (we haven't even introduced this yet!). Please don't do this, your program should handle any number and print out the result of multiplying it by 10
. The two examples above are just two particular cases.
HINT: You will need to cast the input to an int
.
Unit 2 - Basic Elements of Python
This unit looks at the basic elements of Python. It is really important that you get a good handle on these elements. That does not mean you have to memorise them, but you should understand them.
Programmers regularly switch languages and look up things. Your secret programming weapon is Google! You will find almost unlimited resources on Python, but knowing the right words to use in a search will certainly help you.
The lessons in this unit are:
- Variables and Assignment
- Statements vs Expressions
- Numbers
- Strings
- Casting
- String Indexing and Slicing
- String Methods
Variables and Assignment
Variables are one of the most important things in programming languages. They allow us to assign a name to an object so we can store it for use later on in our program.
1. Assignment
We assign an object to a variable using the assignment operator =
. For example,
pi = 3.14
assigns 3.14
to the variable named pi
.
We can then use the variable in our programs. Consider the following program,
pi = 3.14
radius = 6
circumference = 2 * pi * radius
print(circumference)
this will print the value of the circumference of the circle with radius 6.
The point is that the line circumference = 2 * pi * radius
now used the variables pi
and radius
to calculate the value of the circumference
.
The following image illustrates the binding of the variables to the objects in memory. Note it isn't really how Python works internally, but it is good enough as a mental picture.
2. Reassignment
Python lets you reassign a variable to another object.
The following code,
pi = 3.14
radius = 6
circumference = 2 * pi * radius
print(f"The circle with radius = {radius} has circumference = {circumference}")
radius = 0
print(f"The circle with radius = {radius} has circumference = {circumference}")
will output,
The circle with radius = 6 has circumference = 37.68
The circle with radius = 0 has circumference = 37.68
because we reassigned the variable radius
to the value 0
, but we did not recalculate the circumference
.
The following depicts the reassignment of the variable radius
.
Image reproduced from Chapter 2: Introduction to Computation and Programming Using Python.
3. Objects in Memory
Python stores objects in memory and when you assign a variable it binds that variable to the location of the object in memory.
You can find the unique id of an object by using the id()
function. This refers to the object's memory address and will be different each time you run the program.
Input the following two lines into the terminal, remember to press enter after each line:
pi = 3.14
id(pi)
For now the id()
function is a bit of a novelty. We will see later on that it can prove essential in debugging programs when we are editing mutable (changeable) types like lists.
Note that two objects can have the same id if one object has been removed from memory (Python automatically cleans up objects that are no longer used). Essentially the unique id is being re-used.
=== TASK ===
This should be relatively simple, consider it a freebie.
Copy the following code into a new Python file.
pi = 3.14
radius = 6
circumference = 2 * pi * radius
print(f"The circle with radius = {radius} has circumference = {circumference}")
# Reassign radius to the value 15
# Reassign circumference to the circumference of the circule with radius 15
# In addition to the existing print statement, print out the new circumference.
- Reassign
radius
to the value15
- Reassign
circumference
to the circumference of the circule with radius15
- In addition to the existing
print
statement, print out the new circumference.
The output of the program should be:
The circle with radius = 6 has circumference = 37.68
The circle with radius = 15 has circumference = 94.2
Statements vs Expressions
So far you have already seen examples of both statements and expressions and your Python programs will be made up of both of these.
We will give a basic definition of the two, however, the real story is much more complicated.
1. Statements
Statements instruct Python to do something, that is the Python interpreter can execute the statement. To date, you have seen two types of statements: print and assignment.
For example,
print("Hello World!")
is a statement that instructs python to print the string "Hello World!"
and,
int1 = 1
is a statement that instructs Python to assign 1
to the variable int1
.
2. Expressions
Expressions differ from statements in that they evaluate to an object. They are a combination of objects and operators.
For example,
3 + 5
combines the integers (objects of type int
) 3
and 5
with the operator +
and evaluates to the integer 8
.
This example,
True and False
combines the boolean (objects of type bool
) True
and False
with the logical operator and
and evaluates to the boolean False
.
3. Combining Statements and Expressions
Most of your code will tend to be a statement made up of expressions.
For example, you can assign the result of an expression to a variable. Consider the following statement (assignment)
a = 3 + 5
Here 3 + 5
is an expression and the result, 8
, is assigned to the variable a
. The whole line is a statement that contains an expression.
=== TASK ===
A nice simple one.
Create a new Python file.
Print Statements instruct Python to do something
Print Expressions combine objects and operators and evaluate to an object
Print Statements can include expressions
Numbers
Python has three numeric types:
int
float
complex
Unless you are doing something specifically mathematical you will only use int
and float
. So we will cover just these in this lesson.
1. ints and floats
The following table provides a summary of int
and float
.
Data Type | Description |
---|---|
int | An integer. There is no upper or lower limit to how high or low it can be (Python 3). |
float | A decimal number. A double-precision floating-point number, equivalent to double in many other programming languages. |
1.1 int
An int
(integer) is simply a whole number such as 5
or -100
. If you assign a whole number to a variable, python will automatically know that the type of object is an int
.
Type the following two lines in the terminal:
a = 10
type(a)
You will see that the output confirms that a
is of type int
.
1.2 float
An float
(floating point number) is a number containing one or more decimals, such as 5.3
or -100.43
. If you assign a decimal number to a variable, python will automatically know that the type of object is an float
.
Type the following two lines in the terminal:
a = 10.6
type(a)
You will see that the output confirms that a
is of type float
.
You can find out the minimum and maximum float using
import sys
print(sys.float_info.min)
print(sys.float_info.max)
On the Repl.it systems it is
-2.2250738585072014e+308
to 1.7976931348623157e+308
. Which is approximately -2.23*(10**308)
to 1.80*(10**308)
That is astronomically big!
1.3 Dynamic Typing
You can combine an int
and a float
. Python will internally understand to convert the int
to a float
so that you can combine two floats.
For example, type the following into the terminal,
a = 1 + 3.3
print(a)
type(a)
the first line is adding an int
and a float
which results in an object of type float
with the value 4.3
.
2. Arithmetic Operators
Number objects can be combined with the following arithmetic operators.
Operator | Name | Example |
---|---|---|
+ | Addition | x + y |
- | Subtraction | x - y |
* | Multiplication | x * y |
/ | Division | x / y |
% | Modulus | x % y |
** | Exponentiation | x ** y |
// | Floor Division | x // y |
All of +
, -
, *
and /
will be familiar to you. However, %
, **
and //
may not be.
2.0.1 Modulus (%
)
The modulus operator is very useful for testing if a whole number is divisible by another whole number. It is an implementation of modulo (clock) arithmetic in mathematics.
If a number is divisible by another the output of x % y
will be 0
(x
and y
can be positive or negative).
10 % 3 # output is 1 (not divisible)
10 % 2 # output is 0 (divisible)
14 % 4 # output is 2 (not divisible)
14 % 7 # output is 0 (divisible)
You will see this in computational mathematics, for more info look at Modulus Operator - Real Python
It can also be used to find the remainder, but be careful, this doesn't work in all circumstances. I would always use math.remainder()
.
import math # imports extra maths stuff
print(math.remainder(10, 3)) # remainder of 10/3 is 1
print(10 % 3) # output is 1. (% WORKS)
print(math.remainder(-10, 3)) # remainder of 10/3 is 1
print(-10 % 3) # output is 2. (% DOES NOT WORK)
print(math.remainder(10, -3)) # remainder of 10/3 is -1
print(10 % -3) # output is -2. (% DOES NOT WORK)
print(math.remainder(-10, -3)) # remainder of 10/3 is -1
print(-10 % -3) # output is -1. (% WORKS)
So it works when both numbers are positive or both numbers are negative.
My advice is don't use it for the remainder.
2.0.2 Exponentiation (**
)
The exponentiation operator **
takes the number x
(called the base) to the power of y
(called the exponent).
2**3 # Evaluates to 8 (i.e. 2*2*2)
2**4 # Evaluates to 16 (i.e. 2*2*2*2)
5**2 # Evaluates to 25 (i.e. 5*5)
5**3 # Evaluates to 125 (i.e. 5*5*5)
2.0.3 Floor Division (//
)
The floor division operator //
takes the number x/y
and rounds it down.
For example,
10/4
is 2.5
rounded down is 2
.
So,
10//4 # Evaluates to 2
-10/4
is -2.5
rounded down is -3
.
So,
-10//4 #Evaluates to -3
2.1 Order of Operations
When using arithmetic operators you need to be aware of the order in which Python evaluates an operator. This is known as the operator precedence.
For example,
3 + 5 * 2
is 16
right? If you enter this into the terminal you will get 13
. Well done if you spotted this.
This is because the multiplication *
operator has higher precedence than the addition +
operator. Python is doing the following
# This is not code, we are manually evaluating to see how Python works with this expression
3 + 5 * 2 # (Evaluate 5 * 2)
3 + 10 # (Evaluate 3 + 10)
13 # (Final Object)
If you want to force the 3 + 5
to be evaluated first, then you need to use parentheses.
(3 + 5) * 2
This now evaluates to 16
. Try it in the terminal.
The following table gives the arithmetic operator precedence. Higher entries have higher precedence.
Operator | Name |
---|---|
() | Parentheses |
** | Exponentiation |
* , / , % , // | Multiplication, Division, Modulus, Floor Division |
+ , - | Addition, Subtraction |
2.2 Left-to-right Evaluation
You will notice from the table that some operators have the same precedence as each.
What happens then with the following:
5 - 2 + 1
If you try this in the terminal you will get the answer 4
. However, this could have been read as either 3 + 1
or 5 - 3
depending on whether you evaluated the -
or +
first.
Python follows the left-to-right convention. That is, if two operators are of the same precedence, then Python will evaluate the leftmost first. Hence:
# This is not code, we are manually evaluating to see how Python works with this expression
5 - 2 + 1 # (Evaluate 5 - 2)
3 + 1 # (Evaluate 3 + 1)
4 # (Final Object)
3. Comparison Operators
You can also compare two numbers and they will result in a bool
object. Either True
or False
.
Operator | Name | Example |
---|---|---|
== | Equal | x == y |
!= | Not equal | x != y |
> | Greater than | x > y |
< | Less than | x < y |
>= | Greater than or equal to | x >= y |
<= | Less than or equal to | x <= y |
For example, the following expressions evaluate to:
Expression | Result |
---|---|
3 < 5 | True |
3 > 3 | False |
3 >= 3 | True |
3 == 5 | False |
3 != 5 | True |
3.1 Order of Precedence
All the comparison operators given above have lower precedence than the arithmetic operators.
Operator | Name |
---|---|
() | Parentheses |
** | Exponentiation |
* , / , % , // | Multiplication, Division, Modulus, Floor Division |
+ , - | Addition, Subtraction |
== , != , < , > , <= , >= | Comparison Operators |
Therefore something like the following expression,
3 + 5 == 3 * 5
is evaluated as follows:
# This is not code, we are manually evaluating to see how Python works with this expression
3 + 5 == 3 * 5 # Evaluate 3 + 5
8 == 3 * 5 # Evaluate 3 * 5
8 == 15 # Evaluate 8 == 15
False
Note: I would always want to convey my intention and not rely on the order of precedence, it is easy to forget. So I would rewrite the above as:
(3 + 5) == (3 * 5)
This is identical, but it tells the reader more explicitly that the stuff in the parentheses is evaluated first.
=== TASK ===
Copy the following code into a new Python file.
# DO NOT TOUCH THE FOLLOWING LINES 4, 5 and 6
# THESE ARE USED FOR THE INPUT.
a = int(input("Please enter a number: ")) # leave this line alone
b = int(input("Please enter a number: ")) # leave this line alone
c = int(input("Please enter a number: ")) # leave this line alone
# TASK 1.
# print out the type of 10.3 + 5
# TASK 2.
# print(a + b * c) # Correct this so that the ``+`` is evaluated first
# print(a**b+c) # Correct this so that the exponent is b+c
# TASK 3.
# print the output of a < b
# TASK 4.
# print the output of (a < b) == (a <= b)
This reads in three whole numbers and stores them in variables a
, b
and c
. Try running the code and it will ask you for three numbers.
-
Print the type of
10.3 + 5
-
Fix the following two lines:
print(a + b * c) # Correct this so that the ``+`` is evaluated first
print(a**b+c) # Correct this so that the exponent is b+c
-
Print the result of
a < b
-
Print the result of
(a < b) == (a <= b)
If you enter 2
, 3
and 2
into your program then a=2
, b=3
and c=2
. If you have made the above changes correctly, your program should output:
<class 'float'>
10
32
True
True
References
Precedence and Associativity of Operators in Python
Strings
This lesson introduces you to the basics of strings in Python.
Python has a built-in datatype str
known as a string, you can think of this as a piece of text.
1. Basic Printing of Strings
Strings can be printed to the terminal using the python print()
function (we will explore functions in detail later in the course).
For example:
print("hello world!")
Prints hello world!
to the terminal.
Whatever is contained between the brackets will be printed out to the terminal using double quotes ""
.
2. Quotes
In Python strings can be enclosed in either single quotes '...'
or double quotes "..."
For example:
print('hello world!')
Also prints hello world!
to the terminal as per section 1.
NOTE: Although we can use both, please stick to double quotes as most languages represent strings with double quotes!
3. Escape Characters
My first program was "Hello World!" in Python
Try the following in the terminal.
print("My first program was "Hello World!" in Python")
You will see a SyntaxError: invalid syntax
, this is the Python interpreter telling you that it does not understand the code you have entered. If you look closely you will see that it is pointing to the H
, the character after the second ""
. This is because it is illegal to put a double quote character "
within a string.
To fix this we need to use an escape character, in Python, this is the \
(backslash) followed by a particular character.
Amend the previous line of code to include backslashes for the quotes in the string.
print("My first program was \"Hello World!\" in Python")
If you run this you will now get the desired output My first program was "Hello World!" in Python
.
There are a number of special escape characters in Python, you can look them up. We will just look at the following:
Name | Escape Character |
---|---|
tab | \t |
new line | \n |
backslash | \\ |
single quotation mark | \' (You only need to use this when the string in enclosed is single quotation marks.) |
double quotation mark | \" (You only need to use this when the string in enclosed is double quotation marks.) |
For example,
print("My first program was \"Hello World!\" in Python\n\nPython is cool!")
prints out :
My first program was "Hello World!" in Python
Python is cool!
4. Multiline Strings
Multiline strings let us write multiple lines in a string without escape characters or multiple print statements. In Python, they start with '''
or """
and end the same way.
numbers = '''3
5
6
7
'''
print(numbers)
The above will print out the following to the terminal.
3
5
6
7
If you have an escape character in your multiline string and you want this to appear in the terminal you should escape it with another \
.
For example,
print('''This is a multiline string.
The tab escape character in python is \t.
''')
will result in the output:
This is a multiline string.
print('''This is a multiline string.
The tab escape character in python is .
''')
i.e. it will put a tab into the output. To get \t
to show up you need to escape it with another \
as follows:
print('''This is a multiline string.
The tab escape character in python is \\t.
''')
NOTE: This is the same in a normal string. To output an escape character as text you should escape it!
5. Concatenating Strings
To concatenate, or combine, strings you can use the +
operator.
For example,
str1 = "Hello"
str2 = "World!"
print(str1 + str2)
results in the output:
HelloWorld!
To get the standard Hello World!
we could do the following.
str1 = "Hello"
str2 = "World!"
print(str1 + " " + str2)
Note that you can also just concatenate strings within the print function:
print("Hello" + " " + "World")
6. Formatting Strings
We can also combine strings using the format()
method.
The format()
method takes the passed arguments and puts them in the string where the placeholders {}
are:
For example,
str1 = "string 1"
str2 = "string 2"
print("Hello {} and hello {}.".format(str1, str2))
results in the output:
Hello string 1 and hello string 2.
We can also achieve this using index numbers starting at {0}
str1 = "string 1"
str2 = "string 2"
print("Hello {0} and hello {1}.".format(str1, str2))
This is especially useful if you wish to repeat strings stored in variables.
For example,
str1 = "string 1"
str2 = "string 2"
print("Hello {0} and hello {1}.\nBye {0} and bye {1}.".format(str1, str2))
results in the output:
Hello string 1 and hello string 2.
Bye string 1 and bye string 2.
7. f-Strings
Since Python 3.6 you can use f-Strings (formatted string literals), we will use these throughout the course, but you should understand how to use the others in case someone else has written code using them.
We can achieve the previous output by doing the following:
str1 = "string 1"
str2 = "string 2"
print(f"Hello {str1} and hello {st2}.\nBye {str1} and bye {str2}.")
which again results in the output:
Hello string 1 and hello string 2.
Bye string 1 and bye string 2.
Note that in front of the string we put an f
. This tells python that anything in curly braces should be evaluated. So in the example above, the values of str1
and str2
are substituted into the string.
=== TASK ===
Create a new Python file.
-
Print
Programming in Python
to the terminal. -
Print
Programming in Python with single quotes
to the terminal using single quotes''
. -
Print the following to the terminal.
I know how to put "quotes" into a string.
And put in new lines!
- Using a multiline string print
I am using a multiline string to:
- print multiple lines
- without the use of the escape character \n
to the terminal.
- Create the following strings and store them in the variables
str1
andstr2
str1 = "string 1"
str2 = "string 2"
Print Hi, I am string 1 and I am string 2!
to the terminal using string concatenation.
- Print
string 1 is before string 2 and string 2 is after string 1
to the terminal using f-strings.
The entire output of the program should be as follows:
Programming in Python
Programming in Python with single quotes
I know how to put "quotes" into a string.
And put in new lines!
I am using a multiline string to:
- print multiple lines
- without the use of the escape character \n
Hi, I am string 1 and I am string 2!
string 1 is before string 2 and string 2 is after string 1
Casting
It is very common that you will need to convert one data type to another.
Type the following into the terminal:
print("The meaning of life is " + 42)
You will the following TypeError
:
TypeError: can only concatenate str (not "int") to str
This is because you are trying to add a str
and an int
. Python does not know how to do this. To correct this you need to do something called casting.
The following code casts (converts) the int
to a str
using the constructor function str()
:
print("The meaning of life is " + str(42))
If you try this you will see that it works.
Type str(42)
into the terminal on its own and you will see that it returns the str
'42'
(note it uses single quotes, but that is the same as double quotes in Python!).
Note that we could have done the following:
print(f"The meaning of life is {42}")
This is because the python f-string knows how to convert the number 42 to play nicely with the string. Try it in the terminal.
1. How to Cast
Casting in python is therefore done using constructor functions:
Function | Description |
---|---|
int() | Constructs an integer number from an integer, a float (by removing all decimals), or a string (providing the string represents a whole number) |
float() | Constructs a float number from an integer, a float, or a string (providing the string represents a float or an integer) |
str() | Constructs a string from a wide variety of data types, including strings, integers, floats, and booleans |
bool() | Constructs a boolean from a wide variety of data types, including strings, integers, floats, and booleans |
1.1 Examples
Try these in the terminal:
int(1) # Creates an int with value 1
int("2") # Creates an int with value 2
int(4.3) # Creates an int with value 4
int(True) # Creates an int with value 1
int(False) # Creates an int with value 0
float(1) # Creates a float with value 1.0
float("2") # Creates a float with value 2.0
float("3.142") # Creates a float with value 3.142
float(4.3) # Creates a float with value 4.3
float(True) # Creates a float with value 1.0
float(False) # Creates a float with value 0.0
str(1) # Creates a str with value '1'
str("3.142") # Creates a str with value '3.142'
str(4.3) # Creates a str with value '4.3'
str(True) # Creates a str with value 'True'
str(False) # Creates a str with value 'False'
bool()
can throw up some unexpected results unless you understand what it is doing.
You might think that bool("0")
would result in False
, it doesn't!
Python treats everything as True
other than False
, 0
, an empty string ""
and some other things we have yet to encounter, empty lists, dictionaries, tuples, and the None
keyword which represents no value at all (more on that later).
bool(1) # Creates a bool with value True
bool("3.142") # Creates a bool with value True
bool(4.3) # Creates a bool with value True
bool("0") # Creates a bool with value True
bool(True) # Creates a bool with value True
bool(False) # Creates a bool with value False
bool(0) # Creates a bool with value False
bool("") # Creates a bool with value False
bool([]) # Creates a bool with value False
bool({}) # Creates a bool with value False
bool(()) # Creates a bool with value False
bool(None) # Creates a bool with value False
=== TASK ===
Create a new Python file and write a program that outputs the following to the terminal for a given X and Y.
The result of multiplying X by Y is X*Y
For example, for 2.1 and 3:
Please enter a number:
2.1
Please enter another number:
3
The result of multiplying 2.1 by 3.0 is 6.3
For example, for 5.2 and 3.4:
Please enter a number:
5.2
Please enter another number:
3.4
The result of multiplying 5.2 by 3.4 is 17.68
References
The What's My Age Again (in Hours)? Program
The following program is named after the pop-punk classic by Blink 182 - What's My Age Again off the album Enema of the State.
The program asks the user for the hour, day, month, and year of their birth.
It then outputs the person's age in hours.
Here is a plan for our program.
In pseudocode:
Ask the user for the hour, day, month and, year of birth
Compute the difference between the current date and time and the user's birth
Convert the difference into hours
Print out the user's age in hours
Please try to understand how this program works and match it with the pseudocode above.
Copy and paste this code into a new file to play around with it.
1. The Complete Program
import datetime
hour = int(input("What hour (24hr) were you born?\n"))
day = int(input("What day (number) of the month were you born?\n"))
month = int(input("What number month were you born?\n"))
year = int(input("What year were you born?\n"))
# create a new datetime object with user input
birth_datetime = datetime.datetime(year, month, day, hour)
# get current datetime
current_datetime = datetime.datetime.now()
# compute the difference
diff = current_datetime - birth_datetime
# convert days (*24) and seconds (/3600) to hours and sum
hours = diff.days*24 + diff.seconds/3600
print(f"Your age in hours is {round(hours)}.")
How would you estimate that the output is actually correct?
You should try and do a calculation to convince yourself that this works properly.
String Methods
Python has a number of built-in methods that you can use on strings to do simple transformations.
You can see a comprehensive list via the link in the References.
1. What is a Method?
A method is something you can call on an object that does something with that object. This will make more sense when we get to object-orientation later in the course.
For now, if we have a string we can call a method using the .
notation and we will get back a new string.
For example, type the following into the terminal,
"hello world".upper()
returns the new string:
"HELLO WORLD"
Here we called the upper()
method on the str
object "hello world"
and it gave us back the str
"HELLO WORLD"
.
1.1 Be Careful
String methods return new strings, consider the following code:
a = "hello world" # (assign "hello world" to variable a)
a.upper() # (call upper() on variable a)
print(a) # (print a)
will output
hello world
This is because it returns a new string, it does NOT change the original string. So a
is left as "hello world"
.
We would either have to do the following to print out the upper case version,
a = "hello world" # (assign "hello world" to variable a)
print(a.upper()) # (print out the result of calling upper() on variable a)
or,
a = "hello world" # (assign "hello world" to variable a)
b = a.upper() # (call upper() on variable a and assign result to variable b)
print(b) # (print b)
1.2 Familiarise Yourself
Take a look at the following link - W3Schools - Python String Methods and try some of these out. You will need some of them for the TASK.
=== TASK ===
Copy the following into a new Python file.
sentence = input("Please enter a sentence:\n")
print(sentence)
Amend the code so that the inputted sentence is then printed out as
- upper case
- lower case
- first character of each word is upper case
For example,
Please enter a sentence:
The quick brown fox jumps over the lazy dog
THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
the quick brown fox jumps over the lazy dog
The Quick Brown Fox Jumps Over The Lazy Dog
References
W3Schools - Python String Methods
String Indexing and Slicing
A string is a sequence of characters representing Unicode characters.
For example the string "Helloworld"
can be thought of like this.
Unlike many programming languages, Python does not have a character type and a character is just a string (str
) of length 1
.
1. String Indexing
You can access elements of the string using square brackets []
and the index of the position you wish to access.
Note indexing starts at 0
. So the first character has an index of 0
.
For example,
str1 = "String Indexing"
print(str1[0]) # prints the first character "S"
print(str1[4]) # prints the 5th character "n"
1.1 IndexError
If you try the following:
str1 = "String Indexing"
print(str1[15]) # IndexError
You will get an exception as there is no 16th character (index 15 tries to access the 16th character).
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: string index out of range
1.2 Negative Indexing
We can also access the characters of the string from the end.
For example,
str1 = "String Indexing"
print(str1[-1]) # prints the last character "g"
print(str1[-2]) # prints the second to last character "n"
1.3 Length of a String
The len()
function will return the length of the string.
str1 = "String Indexing"
print(len("String Indexing")) # prints 15
2. String Slicing
We can easily get substrings from a given string by using string slicing.
For example, we can get "Index"
from the string "String Indexing"
.
The following gets the characters from position 7 to position 11 (12 not included)
str1 = "String Indexing"
print(str1[7:12]) # prints Index
Basically you specify the start index and the end index (not included), separated by a colon, to return a part of the string.
2.1 Start to a Given Position
You can get all characters from the start to a given position by:
str1 = "String Indexing"
print(str1[0:6]) # prints String
## OR
print(str1[:6]) # prints String
The second way is very common and is shorthand for the start index 0
.
2.2 A Given Position to the End
You can get all characters from a given position to the end of the string using.
str1 = "String Indexing"
print(str1[4:15]) # prints ng Indexing
## OR
print(str1[4:]) # prints ng Indexing
The second way is quite common and it means we don't need to know the length of the string!
Otherwise, in general you would need to do the following
str1 = "String Indexing"
print(str1[4:len(str1)]) # same as str1[4:15] or str1[4:]
2.3 Negative Slicing
You can also slice using negative indexing.
str1 = "String Indexing"
print(str1[-3:]) # prints out the last 3 characters - "ing"
print(str1[-4:-2]) # prints out character 11 and 12. Same as str1[11:13]
=== TASK===
Copy the following into a new Python file.
# DO NOT EDIT THESE TWO LINES
firstname = input("Please enter a first name: \n") # leave this alone!
surname = input("Please enter a surname: \n") # leave this alone!
# -------------------------------------------------------
# Edit the following line so that it prints out the first character of the first name
print(f"The first character of the first name is {firstname}")
# Edit the following line so that it prints out the last character of the surname (negative indexing)
print(f"The last character of the surname is {surname}")
# Edit the following line so that it prints the initials of the person. e.g. Mary Smith would result in M.S
print(f"The person's initials are {firstname}.{surname}")
# Edit the following line so that it prints the first 3 characters of the first name. For example Mary would print out Mar
print(f"The first 3 characters of the first name are {firstname}")
# Edit the following line so that it prints the last 4 characters of the surname
print(f"The last 4 characters of the surname are {surname}")
- Edit the following line so that it prints out the first character of the first name
print(f"The first character of the first name is {firstname}")
- Edit the following line so that it prints out the last character of the surname (negative indexing)
print(f"The last character of the surname is {surname}")
- Edit the following line so that it prints the initials of the person. e.g. Mary Smith would result in M.S
print(f"The person's initials are {firstname}.{surname}")
- Edit the following line so that it prints the first 3 characters of the first name. For example, Mary would print out Mar
print(f"The first 3 characters of the first name are {firstname}")
- Edit the following line so that it prints the last 4 characters of the surname (note we assume for simplicity that the surname contains at least 4 characters, what happens if it is less than 4?)
print(f"The last 4 characters of the surname are {surname}")
An example of the correct program for the input Mary Smith
is given below.
Please enter a first name:
Mary
Please enter a surname:
Smith
The first character of the first name is M
The last character of the surname is h
The person's initials are M.S
The first 3 characters of the first name are Mar
The last 4 characters of the surname are mith
Unit 3 - Branching and Decisions
So far in this course, we have written straight-line programs. These execute one statement after another.
These aren't particularly interesting programs.
Branching programs provide more complexity to our programs and make them more interesting.
Generally, we will look to use a conditional statement which has three parts:
- boolean test - an expression that evaluates to
True
orFalse
True
block - a block of code that is executed if the test evaluates toTrue
False
block - a block of code that is executed if the test evaluates toFalse
The following image demonstrates the program flow of a basic branching statement.
You can see that the code enters the conditional statement (dotted box) and encounters the Test, based on whether it evaluates to True
or False
it executes either the True
block or the False
block.
Image reproduced from Chapter 2: Introduction to Computation and Programming Using Python.
If ... Else Statement
As stated in the overview of this unit. A conditional statement has three parts:
- Boolean test - an expression that evaluates to True or False
- True block - a block of code that is executed if the test evaluates to True
- False block - a block of code that is executed if the test evaluates to False
1. Structure of a conditional statement
In Python, a conditional if
statement has the following form:
if <boolean expression>:
# If the boolean expression is True, run this block of code
else:
# If the boolean expression is False, run this block of code
You can see how this mirrors the diagram from the overview of this unit, if the boolean expression is True
, execute the True
block, else it is False
, execute the False
block.
Example 1.1 - Just an if statement
The following example tests if a variable x
is less than5
and only prints out if x
is less than 5
print("code before if statement")
# store 4 in the variable x
x = 4
# Test the variable using the boolean expression x < 5?
if x < 5:
print(f"{x} is less than 5")
print("code after if statement")
The whole output of the program is as follows:
code before if statement
4 is less than 5
code after if statement
If we change x = 10
then the program will not execute the statement print(f"{x} is less than 5")
and the output will be:
code before if statement
code after if statement
Copy and paste the following program into a Python file and experiment by entering different numbers.
# get some input from the user and store as an int
input_string = input("Please enter a whole number\n")
x = int(input_string)
print("code before if statement")
if x < 5:
print(f"{x} is less than 5")
print("code after if statement")
Example 1.2 - if ... else statement
The following example tests if a variable x
is less than 5
and prints out information for both cases.
print("code before if statement")
# Test the variable using the boolean expression x < 5?
if x < 5:
print(f"{x} is less than 5")
else:
print(f"{x} is greater than or equal to 5")
print("code after if statement")
In this example x = 7
which is greater than 5
, therefore x < 5
evaluates to False
and the program prints 7 is greater than or equal to 5
(the else
block)
The whole output of the program is as follows:
code before if statement
7 is greater than or equal to 5
code after if statement
Try copying the code below into a Python file and running the program. Think about the following:
- What happens with different
int
values ofx
? - What happens if
x
is not anint
, for example of typestr
?
# get some input from the user and store as an int
input_string = input("Please enter a whole number\n")
x = int(input_string)
print("code before if statement")
# Test using the boolean expression x < 5?
if x < 5:
# run this code if boolean expression is True
print(f"{x} is less than 5")
else:
# run this code if boolean expression is False
print(f"{x} is greater than or equal to 5")
print("code after if statement")
2. Indentation
You will notice that the code is indented, this is how Python determines blocks of code. Indentation is determined by the programmer, I would stick to 2 or 4 spaces. Repl.it defaults to 2 spaces.
It can be done using a different number of spaces or the tab character, but you should be consistent in your program. Try the code without the indentation. What happens?
Example 2.1 - No indentation
print("code before if statement")
x = 4
if x < 5:
print(f"{x} is less than 5")
else:
print(f"{x} is greater than or equal to 5")
print("code after if statement")
You will see that you get the following error - IndentationError: expected an indented block
.
3. Compound Boolean Expressions
The conditional test for the if
statement can be a more complicated expression as long as it evaluates to True
or False
.
For example, the following program tests to see if a number is between 1 and 10:
Example 3.1 - Logical And
# get some input from the user and store as an int
x = int(input("Enter a whole number:\n"))
if (x > 0) and (x < 11):
print("Number is between 1 and 10")
else:
print("Number is not between 1 and 10")
Here the boolean expression (test) is (x > 0) and (x < 11)
which requires that both (x > 0)
and (x < 11)
need to be True
for the whole expression to be True
.
Example 3.2 - Logical Not
# ask the user for a number and cast it to an int
x = int(input("Enter a whole number:\n"))
if not x < 10 :
print("Number is above 10")
Here the boolean expression (test) is not x < 0
. This will first evaluate x < 0
and then take a not
of the result. e.g. if x = 11
then x < 10
is False
, therefore not x < 10
is True
.
Try out these programs and make sure you understand what they do.
=== TASK ===
You can test if a number is even or odd using the modulus operator %
.
For example, 4 % 2 = 0
evaluates to 0
because 2
divides 4
with no remainder.
However, 7 % 2 = 1
evaluates to 1
because 2
divides 7
with remainder 1
.
We can use this to evaluate if a number is odd or even. The expression x % 2 == 0
evaluates to True
if x
is even and False
if it is odd.
The expression x % 2 == 0
compares the left side x % 2
to the right side 0
to see if they are equal.
For example,
x | x % 2 | x % 2 == 0 |
---|---|---|
4 | 0 | True |
7 | 1 | False |
I suggest you try some even and odd examples out in the terminal if you don't understand this.
E.g. Try:
# test 4 to see if it is even
4 % 2 == 0 # will print out True as 4 % 2 evaluates to 0
# test 7 to see if it is even
7 % 2 == 0 # will print out False as 7 % 2 evaluates to 1
Write a program that asks a user for a number and then prints out whether it is even or odd.
Your program should work as follows:
Please enter a whole number:
7
Your number is odd!
Please enter a whole number:
4
Your number is even!
Note that to pass the tests you must have exactly the output above, apart from the numbers which will differ depending on what the user inputs.
MP: The Really Rubbish Password Program
This is an example mini-program to demonstrate the use of an if
... else
statement.
The aim is to ask the user for a password and if the password matches the secret password then the user gets into the system, if not the user is denied access.
Feel free to play around with the program as much as you like.
Planning our program in pseudocode.
Set the secret password
Ask the user to enter their password
if the input matches the secret password
grant access
else
do not grant access
1. The Complete Program
The complete program is given below and then explained in the subsequent sections.
# The Really Rubbish Password Program
# This is the stored password for the user
secret_password = "secret"
print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth\n")
password = input("Please enter your password:\n")
if password == "secret":
print("\nAccess Granted!")
else:
print("\nAccess Denied!")
input("\n\nPress the enter key to exit.")
2. Breaking Down the Program
The following section explains the two main parts of the program.
2.1 Getting the User Input.
The first part of the code prompts the user for their password and stores it in the variable password
.
# The Really Rubbish Password Program
# This is the stored password for the user
secret_password = "secret"
print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth\n")
password = input("Please enter your password:\n")
2.2 Testing the Password
The second part of the code then uses the variable password
to check if the password is correct. If the condition password == "secret"
is True
then access is granted otherwise (else
) access is denied.
if password == "secret":
print("\nAccess Granted!")
else:
print("\nAccess Denied!")
input("\n\nPress the enter key to exit.")
References
Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.
Nested if Statements
It is possible to nest if statements within other if statements. This can be useful for testing multiple conditions but allowing us to run code for each conditional.
1. Nested if Statement
We can nest if statements as follows:
1.1 Example - Nested if Statement
x = 101
if x > 100:
print("Above 100, ", end="")
if x > 150:
print("and also above 150!")
else:
print("but not above 150!")
As x = 101
the first program will first execute the statement print("Above 100, ", end="")
and then execute the statement print("but not above 150!")
. Try different values of x by pasting the code above into a Python file.
If we compare this with:
x = 101
if (x > 100) and (x > 150):
print("Above 100, and also above 150!")
The second program cannot do this and only prints out "Above 100, and also above 150!
if x
is greater than 150
.
If x = 99
or x = 130
the program will print nothing.
You should make sure you understand the program flow. The two program flows are depicted in the diagram below.
2. Indentation
You should note that in the example given in 1.1, the blocks of code are given by indentation. If we just examine the structure of the example it looks as follows:
# main block of code, anything aligned with this is in this block
if x > 100:
# True block of code
print("Above ten,")
if x > 150:
# True block of code
print("and also above 150!")
else:
# False block of code
print("but not above 150!")
This is also illustrated by the diagram below. You should note that the indentation defines each of the blocks of code.
=== TASK ===
Using nested if statements, write a program that asks the user for a whole number. Your program should do the following:
- If the number is divisible by 3 and 5 it should print out
Your number is divisible by 3 and 5.
- If the number is divisible by 3 and NOT 5 it should print out
Your number is divisible by 3 and NOT by 5.
- If the number is NOT divisible by and by 5 it should print out
Your number is NOT divisible by 3 and is divisible by 5.
- If the number is NOT divisible by and by NOT 5 it should print out
Your number is NOT divisible by 3 and 5.
Your program should match the examples below. I have given examples for each output.
Please enter a number:
15
Your number is divisible by 3 and 5.
Please enter a number:
12
Your number is divisible by 3 and NOT by 5.
Please enter a number:
20
Your number is NOT divisible by 3 and is divisible by 5.
Please enter a number:
22
Your number is NOT divisible by 3 and 5.
HINT: We learned how you can test if a number is divisible by 2 in the Lesson: If ... Else Statement earlier in this unit.
Elif Statement
Sometimes we may wish to use an alternative test should our previous test evaluate as False
.
elif
which is short for else if, lets us do exactly that.
1. Using the Elif Statement
We can test another condition after the first condition as follows:
1.1 Example - if ... elif
Remember from Nested If Statements that we can do the following:
# change these to experiment with the if..elif block
x = 4
y = 5
if x < y:
# block of code
print("x is less than y")
elif x > y:
# block of code
print("x is greater than y")
Here depending on the values of x
and y
one (and only one) of the print
statements is executed.
If x
is less than y
then x < y
is True
and the first block is executed.
If x
is greater than y
then x < y
is False
and the elif
part is checked. As x > y
is True
, the second block is executed.
What about if x
and y
are equal?
Copy the code above into a Python file and experiment with different values of x
and y
.
1.2 Example - if ... elif ..else
We can also include an else
statement, now our program will output when x
and y
are equal.
# change these to experiment with the if..elif..else block
x = 4
y = 5
if x < y:
# block of code
print("x is less than y")
elif x > y:
# block of code
print("x is greater than y")
else:
# block of code
print("x is equal to y")
You should think about this and realise that there are three possibilities.
x
is less thany
x
is greater thany
x
is equal toy
If the first two are False
, then it must be that x
is equal to y
. We don't need a test, we can just the else
statement.
1.3 The Connection with if ... else
The program from the last section:
# change these to experiment with the if..elif..else block
x = 4
y = 5
if x < y:
# block of code
print("x is less than y")
elif x > y:
# block of code
print("x is greater than y")
else:
# block of code
print("x is equal to y")
Can be rewritten in terms of just if
and else
statements:
# change these to experiment with the if..elif..else block
x = 4
y = 5
if x < y:
# block of code
print("x is less than y")
else: # You can see from these two lines where elif gets it's name, it is doing the same as else if!
if x > y:
# block of code
print("x is greater than y")
else:
# block of code
print("x is equal to y")
1.4 Example - Multiple elif statements
Another example is testing to see if the first letter of someone's name begins with a vowel.
input_name = input("Please enter your name:\n")
# convert the name to lowercase
name = input_name.lower()
if name[0] == "a":
print("The name begins with an a")
elif name[0] == "e":
print("The name begins with an e")
elif name[0] == "i":
print("The name begins with an i")
elif name[0] == "o":
print("The name begins with an o")
elif name[0] == "u":
print("The name begins with an u")
else:
print("The name does not begin with a vowel")
I suggest trying each of these programs out in Python.
Note that this is not the most efficient way to do this, we can either use the newer match statement available since Python 3.10 (version). We could also do a similar thing using lists
=== TASK ===
Write a program that outputs whether a number is positive, negative, or zero. Your program should accept numbers of type float
.
- If the number is positive it should output
Your number is positive!
- If the number is negative it should output
Your number is negative!
- If the number is zero it should output
Your number is zero!
For example, your program should output the following given these inputs:
Please enter a number:
2.3
Your number is positive!
Please enter a number:
-3.3
Your number is negative!
Please enter a number:
0
Your number is zero!
The Mood Face Program
This is an example mini-program to demonstrate the use of an elif
statement.
The program generates a random number between 1 and 3 and then prints out a given mood face.
Work through the three sections.
1. The Random Mood Program
# Mood Computer
# Demonstrates the elif clause
import random
print("Right now I am feeling...")
mood = random.randint(1, 3)
if mood == 1:
# happy
print( \
"""
-----------
| |
| O O |
| < |
| |
| . . |
| `...` |
-----------
""")
elif mood == 2:
# neutral
print( \
"""
-----------
| |
| O O |
| < |
| |
| ------- |
| |
-----------
""")
elif mood == 3:
# sad
print( \
"""
-----------
| |
| O O |
| < |
| |
| .'. |
| ' ' |
-----------
""")
input("\n\nPress the enter key to exit.")
You'll note that because the multiline strings do not have indentation, the lines,
print( \
"""
-----------
| |
| O O |
| < |
| |
| . . |
| `...` |
-----------
""")
are actually really one line of code, but the multiline string is split across the 10 lines.
2. Refactoring the Program
If you look carefully you will notice that all the faces have the same first 5 lines. and the same last line.
-----------
| |
| O O |
| < |
| |
-----------
Therefore we can just print this out once and then print out the bottom 3 lines depending on the mood.
# Mood Computer
# Demonstrates the elif clause
import random
print("Right now I am feeling...")
# print the first five lines of the face
print("""-----------
| |
| O O |
| < |
| |""")
mood = random.randint(1, 3)
# use elif to print mood lines of the face
if mood == 1:
# happy
print("| . . |")
print("| `...` |")
elif mood == 2:
# neutral
print("| ------- |")
print("| |")
elif mood == 3:
# sad
print("| .'. |")
print("| ' ' |")
# print the bottom line of the face
print("-----------")
input("\n\nPress the enter key to exit.")
This is exactly the same program but slightly shorter.
3. Accept User Input
We make one final change to the program to ask the user for a number between 1
and 3
instead of randomly generating the number.
# Mood Computer
# Demonstrates the elif clause
mood = int(input("Please enter a number between 1 and 3:\n"))
# use elif to print mood lines of the face
if (mood < 1) or (mood > 3):
print("Illegal mood value!")
else:
print("Right now I am feeling...\n")
# print the first five lines of the face
print("""-----------
| |
| O O |
| < |
| |""")
if mood == 1:
# happy
print("| . . |")
print("| `...` |")
elif mood == 2:
# neutral
print("| ------- |")
print("| |")
elif mood == 3:
# sad
print("| .'. |")
print("| ' ' |")
# print the bottom line of the face
print("-----------")
input("\n\nPress the enter key to exit.")
Note now if you enter a number that isn't 1,2
or 3
you will get back,
Illegal mood value!
References
Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.
Booleans
This lesson introduces you to the built-in data type bool
. Booleans are essential in computer science and take on either a value of True
or False
.
True
and False
are keywords in Python and are used to represent their respective boolean values. You can assign them to variables, for example:
my_bool = True
type(my_bool)
The above code assigns the value True
to my_bool
and then checks the type. Try this in the terminal and you will see that my_bool
is now an object of type <class 'bool'>
.
1. Boolean Expressions
You can compare two objects of the same type using the comparison operators listed in the table below. These expressions will return either True
or False
and are known as boolean expressions.
1.1 Comparison Operators
Operator | Name | Example |
---|---|---|
== | Equal | x == y |
!= | Not equal | x != y |
> | Greater than | x > y |
< | Less than | x < y |
>= | Greater than or equal to | x >= y |
<= | Less than or equal to | x <= y |
1.2 Comparing Numbers
For example, the following expressions compare int
objects and evaluate to:
Expression | Result |
---|---|
3 < 5 | True |
3 > 3 | False |
6 >= 6 | True |
3 == 5 | False |
3 != 5 | True |
1.3 Comparing Strings
Interestingly you can also compare other objects such as str
. The following,
"held" < "helm"
evaluates to True
and,
"help" < "helm"
evaluates to False
.
Python knows how to compare the order of strings! It looks at each character in turn and compares their order in the alphabet. Try experimenting. For example, what do the following two expressions return?
"hel" < "helm"
"hello" < "helm"
Try to reason about the answers that you get.
1.4 Comparing Booleans
Believe it or not, you can also compare the order of Booleans.
True < False # (Evaluates to False)
False < True # (Evaluates to True)
This is because Python also represents True
as the bit value 1
and False
as the bit value 0
. Now the above should make sense!
1.5 Comparing Different Types
Do you know how to order the str
"hello"
and the int
10
? I don't and neither does Python.
"hello" < 10
results in the following exception:
TypeError: '<' not supported between instances of 'str' and 'int'
Two objects you can mix are numbers (int
and float
),
4 < 5.6 # (Evaluates to True)
and numbers and booleans (bool
),
4 < True # (Evaluates to False)
because True
is also represented by 1
.
2. Logical Operators
Boolean expressions can be combined with logical operators to create larger boolean expressions:
Operator | Description | Example |
---|---|---|
and | Returns True if both statements are True | x < 5 and x < 10 |
or | Returns True if one of the statements is True | x < 5 or x < 10 |
not | Reverse the result, returns False if the result is True | not(x < 5 and x < 10) |
2.1 Order of Precedence and Left-to-Right
All the logical operators given above have lower precedence than the arithmetic operators and comparison operators.
You will also notice that the order of precedence (high to low) for the logical operators is not
, and
and then or
. This means you evaluate not
first, then and
, then or
.
You also evaluate left-to-right.
Operator | Name |
---|---|
() | Parentheses |
** | Exponentiation |
* , / , % , // | Multiplication, Division, Modulus, Floor Division |
+ , - | Addition, Subtraction |
== , != , < , > , <= , >= | Comparison Operators |
not | Logical NOT |
and | Logical AND |
or | Logical OR |
2.1 Evaluating a Complicated Boolean Expression
The table gives quite a complicated expression for the not
example.
not(x < 5 and x < 10)
Let's look at this for x = 4
,
# This is not code, we are manually evaluating to see how Python works with this expression
not(x < 5 and x < 10) # (Substitute x = 4)
not(4 < 5 and 4 < 10) # (Evaluate 4 < 5)
not(True and 4 < 10) # (Evaluate 4 < 10)
not(True and True) # (Evaluate True and True)
not(True) # (Evaluate not True)
False
and for x = 6
,
# This is not code, we are manually evaluating to see how Python works with this expression
not(x < 5 and x < 10) # (Substitute x = 4)
not(6 < 5 and 6 < 10) # (Evaluate 6 < 5)
not(False and 6 < 10) # (Evaluate False and ....)
not(False) # (Evaluate not False)
True
False and 6 < 10
evaluated to False
because and
requires both expressions to be True
. As the first is False
, why bother evaluating the second?
This is known as short-circuit evaluation or McCarthy evaluation. Named after the great John McCarthy.
2.2 Truth Tables
We can consider the result of combining two bool
variables p
and q
. The following are known as truth tables and display the result for different combinations of p
and q
using and
and or
.
2.2.1 Truth Table for and
p | q | p and q |
---|---|---|
True | True | True |
True | False | False |
False | True | False |
False | False | False |
2.2.2 Truth Table for or
p | q | p or q |
---|---|---|
True | True | True |
True | False | True |
False | True | True |
False | False | False |
=== TASK ===
For this program you should fix the single line instructed.
Copy the following code into a new Python file.
# DO NOT TOUCH THESE LINES. THEY ARE USED FOR THE INPUT
is_raining = bool(int(input()))
no_hat = bool(int(input()))
#######################################################
# You should fix this line to by forming an expression using is_raining and no_hat to produce the correct result for takes_umbrella.
# e.g takes_umbrella = is_raining or no_hat
# Note you only have to fix this line. No if statements etc.. required!
takes_umbrella = True
print(takes_umbrella)
Sam doesn't like getting his hair wet and sometimes wears a hat.
- On days that it is raining and Sam is not wearing a hat, Sam takes his umbrella.
- On days that it is raining and Sam is wearing a hat, Sam does not take an umbrella.
- If it is not raining Sam does not take an umbrella.
We use two variables is_raining
and no_hat
to represent whether it is raining and if Sam is wearing a hat.
- If it is raining
is_raining = True
- If Sam is NOT wearing a hat
no_hat = True
.
Using a third variable takes_umbrella
determine if Sam should take his umbrella by combining is_raining
and no_hat
.
For example, on days that it is raining and Sam is not wearing a hat the variables will have the following values:
is_raining = True
no_hat = True
takes_umbrella = True
is_raining
and no_hat
have been set up for you. Combine them with logical operators to get the correct value of takes_umbrella
.
HINT: You should consider the truth table and fill in the missing entries. This will then give you a hint to what the expression should be.
is_raining | no_hat | takes_umbrella |
---|---|---|
False | False | ? |
False | True | ? |
True | False | ? |
True | True | True |
W3Schools - Python Comparison Operators
Unit 4 - Iteration (Loops)
In the previous module, we looked at how we could write more complicated problems using conditionals (if statement, etc...).
In addition, we can make our programs repeat particular steps by looping (iteration).
Python has two basic loop statements:
while
loopsfor
loops
Iterations provide more complexity to our programs and make them more interesting.
Generally an iteration (loop) has two parts:
- Boolean test - an expression that evaluates to
True
orFalse
- Loop body - a block of code that is executed if the test evaluates to
True
The following image demonstrates the program flow of a basic iteration program.
You can see that the code enters the iteration (dotted box) and encounters the Test, if this evaluates to True
it runs the loop body and then repeats the test. If it evaluates to False
the program continues.
Image reproduced from Chapter 2: Introduction to Computation and Programming Using Python.
While Loops
A while loop lets us execute a series of statements as long as the condition (boolean expression) remains True
.
A while loop has the basic structure:
while <condition>:
# Do something until the condition is False
1. While Loop Using a Counter
A good portion of the while
loops that you write will involve a counter variable.
The following example demonstrates a basic while loop that prints out the numbers 1
- 5
:
i = 1 # Define a variable i and bind the value 1 to it
while i < 6:
print(i) # print out the value of i
i += 1 # This increments i by 1. i.e. i = i + 1
print("Program has ended!")
Note the indentation here! The indented block is the loop body (the code that is executed inside the loop).
The above code first defines a counter variable i = 1
. It then defines a while
loop that tests whether i < 6
. If this is True
it prints out the value of i
and then adds 1
to i
.
Note it is traditional to use i
for a counter variable.
The image below demonstrates how the code runs:
2. While Loop Using a Boolean Variable
Consider the following program which just keeps asking the user for a number until they enter a 0
. Then the program ends.
# loop until the user enters 0
input_string = input("Please enter a number:\n")
num = int(input_string)
while num != 0:
input_string = input("Please enter a number:\n")
num = int(input_string)
print("Program has ended!")
We can rewrite this program by using a bool
variable (program_over
) which keeps track of whether the while
loop should continue or stop.
After a number is entered, we use an if
statement to test if the number entered was 0
, if it was, then we set program_over
to True
. This will then stop the while
loop and the program will end.
# loop until the user enters 0
program_over = False
while not program_over:
input_string = input("Please enter a number:\n")
num = int(input_string)
if num == 0:
program_over = True
print("Program has ended!")
3. Non-terminating While Loop
Now consider the code if you forget to increment (add to) i
.
i = 1 # Define a variable i and bind the value 1 to it
while i < 6:
print(i) # print out the value of i
This loop will repeat forever! i
will never change its value from 1
and therefore the condition i < 6
will forever remain True
.
- What will be the output?
- Try it in Python. To stop (exit) the program in the terminal press
Ctrl + c
.
4. Nested While Loops
Just like if
statements, we can nest loops.
The following program shows an outer and an inner loop. This prints out the multiplication table.
i = 1
while i < 13:
j = 1
while j < 13:
print(f"{i} x {j} = {i*j}")
j += 1
i += 1
The outer loop has a counter variable i
and the inner loop has a counter variable j
. Below is a flow diagram of the program.
The following is of an animation of a similar program (below), but the conditions are changed to allow us to visualise it quicker.
i = 1
while i < 3:
j = 1
while j < 3:
print(f"{i} x {j} = {i*j}")
j += 1
i += 1
=== TASK ===
Use a while loop to print out the even numbers starting at 2 and up to and including 100.
2
4
6
.
.
.
100
Note that the dots represent numbers between 6 and 100. It saves us writing it all out!
HINT: You only need a single while loop.
The Number Guessing Game
This is an example mini-program to demonstrate the use of while
loops and if
.. else
statements.
The basic aim is a simple game to ask the user to guess a number between 1 and 10.
Feel free to play around with the program as much as you like.
Note: You will need to set the secret_guess
to 4
.
1. Number Guessing Game
Planning the program is important. The basic pseudocode for this program is:
pick a random number
while the player hasn’t guessed the number or chosen to exit
let the player guess
if player guessed correct
congratulate the player
The program in Python is given below.
# The Number Guessing Game
import random
# randomly generate a number between 1 and 10
secret_guess = random.randint(1,10)
# to pass the test uncomment this line
# secret_guess = 4
print("Welcome to the Number Guessing Game!")
print("You need to try and guess the number between 1 and 10...")
print("If you wish to exit the game enter 0..")
guess = int(input("Please enter a guess:\n"))
while guess != secret_guess and guess != 0:
print("That is not correct, please try again.")
guess = int(input("Please enter a guess:\n"))
if guess != 0:
print(f"Well done the correct answer is {secret_guess}")
This program generates a random number and then asks the user to input a guess.
The guess is then tested in the while
condition and if it is not the correct number and not 0
we repeat the step. If it is the correct number we exit the while
.
2. Adding Better Feedback
If you try the program above you will get pretty frustrated by the lack of feedback.
To improve the game we can let the user know if their guess is too low or too high.
This is the subject of exercise 4.5.
The Less Rubbish Password Program
This is an example mini-program to demonstrate the use of an if
... else
statement and a while
loop.
We previously created a rubbish password program using the following code.
# The Really Rubbish Password Program
# This is the stored password for the user
secret_password = "secret"
print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth\n")
password = input("Please enter your password:\n")
if password == secret_password:
print("\nAccess Granted!")
else:
print("\nAccess Denied!")
input("\n\nPress the enter key to exit.")
Aside from this being very insecure, a major issue is that the user only gets one guess at entering their password.
1. Improving with a Loop
To improve our program we can add a while
loop that repeats the block of code.
password = input("Please enter your password:\n")
if password == "secret":
print("\nAccess Granted!")
else:
print("\nAccess Denied!")
To do this we add a boolean variable named access
which will track whether the user has access to the system. We then set this to True
within the if
statement when access is granted.
1.1. The Complete Program
# The Less Rubbish Password Program
# This is the stored password for the user
secret_password = "secret"
access = False
print("Welcome to NOSA Inc.")
print("Did you know that the Moon is an average of 238,855 miles away from Earth")
while not access:
password = input("\nPlease enter your password:\n")
if password == "secret":
print("\nAccess Granted!")
access = True
else:
print("\nAccess Denied!")
input("\n\nPress the enter key to exit.")
Ranges and Lists
Before we come onto for
loops we must understand ranges and lists.
1. List
A list
in python is used to store multiple values in a single variable.
For example:
list1 = [1,2,3]
list2 = ["Citreon", "Ford", "Audi", "Mercedes"]
list3 = ["Citreon", "Ford", 67, True]
You can access an element in a list
using indexing. Each member of the list
has an index number starting from 0
.
For example the code,
list3 = ["Citreon", "Ford", 67, True]
print(list3[0])
print(list3[2])
results in the output:
Citreon
67
Try creating your own list
in the and printing some of its elements.
For now, we will not discuss more about lists until later in the course. It suffices to know that they store multiple values and are heterogeneous (a fancy way of saying they can store different types).
2. Ranges
A range
allows us to create a sequence of numbers using the range()
function.
range(10) # This will create a range of numbers 0,1,2,3,4,5,6,7,8,9
range(3, 10) # This will create a range of numbers 3,4,5,6,7,8,9
range(2, 10, 2) # This will create a range of numbers 2,4,6,8
Ranges can take either:
- an end value
range(end)
- a start and end value
range(start, end)
- a start, end, and step value
range(start, end, step)
In some ways a range
behaves like a list
, however, to save space, Python doesn't create the list
.
If you type the following into the terminal, you will see that the object type of a range
is a range
!
a = range(10)
print(type(a)) # prints <class 'range'>
So think of it as a convenient way to store a list
of numbers without actually storing them!
By default start
is 0
and step
is 1
.
So range(10)
is the same as both range(0,10)
and range(0,10,1)
.
2.1 Convert a Range to a List
If you try and print a range
you won't get the numbers. Try the following in the terminal.
a = range(10)
print(a)
and you will get the output:
range(0, 10)
You can convert the range to a list using the list()
function.
a = list(range(2, 10)) # converts the range to a list [2,3,4,5,6,7,8,9]
print(a)
If you run the following code in the terminal or a Python file you will get the output:
[2,3,4,5,6,7,8,9]
The list()
function is forcing Python to build the list in memory.
To prove this to ourselves let's check the size of range(10**6)
. Note 10**6 = 1000000
import sys
a = range(10**6)
print(sys.getsizeof(a))
This returns 48
which is 48 bytes of memory. Try a range
of any size and you will always get 48
.
Now let's convert it to a list
and print the size:
import sys
a = list(range(10**6))
print(sys.getsizeof(a))
This returns a whopping 8000056
(8 million) bytes, which is approximately 166667
times as large!
=== TASK ===
Using the range()
and list()
functions, print out the odd numbers from 1
up to and including 99
.
Your output should look like this:
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99]
HINT: Make sure you have read Section 2.1
For Loops
The other main way of looping Python is by using a for
loop.
1. Structure of a For Loop
A for
loop in python is structured as follows:
for x in <iterable>:
## do something
Both ranges and lists are iterables which means we can loop over them.
1.1 For Loop with a List
A list is a collection of objects. They don't have to be of the same type.
The following 3 examples show a for loop that loops over each item in the list and prints out that item.
Example 1.1.1 - List of numbers
for x in [1,6,-23]:
print(x)
1
6
-23
The following animation shows how this works.
Example 1.1.2 - List of strings
The following loops over a list of strings,
for x in ["Citreon", "Ford", "Audi", "Mercedes"]:
print(x)
and outputs.
Citreon
Ford
Audi
Mercedes
We can also do this by first assigning the list to a variable and then using this in our for loop.
cars = ["citreon", "ford", "audi", "jaguar"]
for car in cars:
print(car)
Example 1.1.3 - List of different types
The following loops over a list of different types of objects,
for x in ["Citreon", "Ford", 67, True]:
print(x)
and outputs.
Citreon
Ford
67
True
1.2 For Loop with a Range
It is common to use the range()
function to automatically generate a sequence of numbers. Do you really want to type out the entire list?
It is also faster and more efficient because it does not have to build the list in memory.
Example 1.2.1
Here range(10)
generates the numbers 0,1,2,3,4,5,6,7,8,9
. You can think of it like the list [0,1,2,3,4,5,6,7,8,9]
.
The program then prints out double of each item in the list.
for i in range(10):
print(2*i)
0
2
4
6
8
10
12
14
16
18
Example 1.2.2
Here range(3,10)
generates the numbers 3,4,5,6,7,8,9
. You can think of it like the list [3,4,5,6,7,8,9]
.
The program then prints out each item in the list.
for i in range(3,10):
print(i)
3
4
5
6
7
8
9
Example 1.2.3
Here range(3,20,2)
generates numbers using a step size of 2 - 3,5,7,9,11,13,15,17,19
. You can think of it like the list [3,5,7,9,11,13,15,17,19]
.
The program then prints out each item in the list.
for i in range(3,20,2):
print(i)
3
5
7
9
11
13
15
17
19
1.3 For Loop with Underscore
If you are not using the counter variable in the for
loop, it is convention to use an _
(underscore).
for _ in range(10):
print("Hello")
The above will print out Hello
10
times.
2. Nested For Loops
Just as we did with while
loops, we can nest for loops. Here we use an outer for
loop and an inner for
loop to print out the multiplication tables.
for i in range(1,13):
for j in range(1,13):
print(f"{i} x {j} = {i*j}")
The animation below demonstrates how this program works, note that instead of printing out the whole table we have limited this to just 1
and 2
.
for i in range(1,3):
for j in range(1,3):
print(f"{i} x {j} = {i*j}")
If we compare this to the same program using a while
loop, you can see that the for
loop version is less verbose and easier to read.
i = 1
while i < 13:
j = 1
while j < 13:
print(f"{i} x {j} = {i*j}")
j += 1
i += 1
=== TASK ===
You can find the length of a string by using the len()
function.
For example:
string1 = "for loop"
len_string = len(string1)
print(len_string)
# note you could do this in one line print(len(string1))
This will output the length of the str
"for loop"
. That is, 8
.
Copy the following into a new Python file:
# Do not touch this line - It is just there to set up the string list from the input
# string_list will contain a list of strings that have been entered.
string_list = input("Please enter a list of numbers separated by a comma.\ne.g. Citreon,Ford,Audi,Mercedes\n").split(',')
print(string_list)
This code will accept a list of comma-separated strings. e.g. Citreon,Ford,Audi,Mercedes
.
If you try running the program and you enter a list of comma-separated strings they will be printed back to the terminal.
['Citreon','Ford','Audi','Mercedes']
The strings inputted are stored in the list
named string_list
.
Amend the program so that it prints out the following for each inputted string.
The length of the string {string} is {length of string}.
For example, for the string "Ford"
the program would print:
The length of the string Ford is 4.
For the input one,two,three,four,five
, the whole program acts as follows:
Please enter a list of numbers separated by a comma.
e.g. Citreon,Ford,Audi,Mercedes
one,two,three,four,five
The length of the string one is 3.
The length of the string two is 3.
The length of the string three is 5.
The length of the string four is 4.
The length of the string five is 4.
For the input Citreon,Ford,Audi,Mercedes
, the whole program acts as follows:
Please enter a list of numbers separated by a comma.
e.g. Citreon,Ford,Audi,Mercedes
Citreon,Ford,Audi,Mercedes
The length of the string Citreon is 7.
The length of the string Ford is 4.
The length of the string Audi is 4.
The length of the string Mercedes is 8.
Note that to pass the tests you must have exactly the output above, apart from the input which will differ depending on what the user inputs.
Break and Continue
break
and continue
are keywords in python that allow you to stop the loop or skip the current iteration.
Many times programmers use break
and continue
when they don't need to. Generally break
and continue
can lead to funny flows in your program and bugs. If you can avoid using them then do!
1. Break
The break
keyword allows us to exit ("break") out of a loop early. We can do this with both while
and for
loops.
Example 1.1
The following program will stop short of printing the numbers 0,1,2,3,4,5,6,7,8,9
and only print 0,1,2,3,4
.
If you need to convince yourself, copy, paste and run this code in Python.
i = 0
while i < 10:
if i == 5:
break
print(i)
i += 1
The program tests each value of i
and if it is equal to 5
the program exits the loop.
We can write this without the break
.
i = 0
while i < 5:
print(i)
i += 1
Example 1.2
Here is the same program with a for
loop
for i in range(0,10):
if i == 5:
break
print(i)
We can write this without the break
.
for i in range(0,5):
if i < 5:
print(i)
2. Continue
The continue
keyword allows us to skip the current iteration of a loop. We can do this with both while
and for
loops.
Example 2.1
The following program will not print out the number 5
, but all other numbers.
So the output will be the numbers 0,1,2,3,4,6,7,8,9
. Notice that 5
is missing.
If you need to convince yourself, copy, paste and run this in Python.
i = -1
while i < 9:
i += 1
if i == 5:
continue
print(i)
The program tests each value of i
and if it is equal to 5
the program exits the loop.
We can write this without the continue
.
i = 0
while i < 10:
if not i == 5:
print(i)
i += 1
Example 2.2
Here is the same program with a for
loop
for i in range(0,10):
if i == 5:
continue
print(i)
We can write this without continue
.
for i in range(0,10):
if not i == 5:
print(i)
3. No Do...While
To date in Python, we have seen a while loop that will check a condition and execute a code block until the condition becomes False.
while <condition>
# Do something
In many languages it is possible to do the following:
do
run a block of code
while <condition>
This guarantees that the code block runs at least once as the while
One of the major reasons Python does not support such a statement is that it breaks the indentation rule that a code block should be indented. Here it isn't!
3.1. Try Not To Use Break
It is quite common to see the following in Python.
while True:
# do something
if <condition>:
break
The following essentially emulates a do...while loop. Here is an example.
# The Number Guessing Game
import random
# randomly generate a number between 1 and 10
secret_guess = random.randint(1,10)
print("Welcome to the Number Guessing Game!")
print("You need to try and guess the number between 1 and 10...")
while True:
guess = int(input("Please enter a guess:\n"))
print("That is not correct, please try again.")
if guess == secret_guess:
break
print(f"Well done the correct answer is {secret_guess}")
However, the use of a break
statement here relies on the person reading this to implicitly understand that the programmer is intending to use a do...while.
There are also other reasons we would not want to do this in other languages which are compiled to do with the fetch-decode-execute cycle.
We can easily rewrite this with an additional boolean.
# The Number Guessing Game
import random
# randomly generate a number between 1 and 10
secret_guess = random.randint(1,10)
print("Welcome to the Number Guessing Game!")
print("You need to try and guess the number between 1 and 10...")
game_over = False
while not game_over:
guess = int(input("Please enter a guess:\n"))
print("That is not correct, please try again.")
if guess == secret_guess:
game_over = True
print(f"Well done the correct answer is {secret_guess}")
Now, these are almost identical, however, it is now more clear that the while
will end when gameover
is set to True
. This conveys the programmers' intention.
=== TASK ===
Print out the numbers 1
to 100
but leave out the meaning of life 42
.
Use a continue
statement.
The Sum Numbers Program
This is an example mini-program.
The aim is to ask the user for a series of numbers. If the user enters 0
, the program then outputs the sum of the inputted numbers
Feel free to play around with the program as much as you like.
The first program uses a break
to exit the while
loop. Notice that the while
condition is always True
. So the break
is essential to exit.
# The Sum Numbers Program
print("Welcome to The Sum Number Program")
total = 0
while True:
input_string = input("Please enter a number. Enter 0 to stop\n")
if input_string == "0":
break
x = float(input_string)
total += x # note this is shorthand for: total = total + x
print(f"Sum of numbers entered is {total}")
We can also do this without a break
by using a bool
named program_over
. You will notice that this is a little more verbose.
print("Welcome to The Sum Number Program")
program_over = False
total = 0
while not program_over:
input_string = input("Please enter a number. Enter 0 to stop\n")
if input_string == "0":
program_over = True
else:
x = float(input_string)
total += x
print(f"Sum of numbers entered is {total}")
NOTE: Please avoid using break
if you can. It can lead to code that becomes hard to reason about or follow.
While this might seem irrelavant to this program, imagine a much larger program with a more complicated flow and the use of lots of breaks and continues. This can become hard to reason about.
The Check Valid Input Program
The following mini-program demonstrates the use of try
..except
.
We will look at exceptions in more detail later on; however, it is important to see try
and except
early on.
1. A Program That Fails
Consider the following program.
num_string = input("Please enter a number:\n")
# try to convert the number using float()
# this will fail if the user doesn't enter anything valid. e.g. hello
x = float(num_string)
print(f"{x} + 5 = {x+5}")
If you enter something which is not a number, Python will throw an exception, in particular, a ValueError
.
2. Validating the User Input
We can fix this issue by catching this exception using the try
...except
statement.
input_valid = False
while not input_valid:
num_string = input("Please enter a number:\n")
try:
num = float(num_string)
input_valid = True
except ValueError:
print("Input is not a valid number.\n")
print(f"{num} + 5 = {num+5}")
Here the program attempts to convert num_string
to a float
.
If this throws an exception, then we trigger the except
block and print out Input is not a valid number.
. Note that the line input_valid = True
is not executed and thus input_valid
remains set to False
. Therefore, we ask the user for another number.
This will continue until the user enters input that does not trigger the exception and input_valid
is set to True
.
Unit 5 - Functions
Functions are such a huge part of programming that we will be spending two units (units 5 and 8) on this subject.
In this unit we will be discussing:
- What a function is
- Why do we use functions
- Arguments vs Parameters
- Return Values
- Variable Scope
- Passing by Assignment
A function consists of three parts:
- The input(s) to the function
- The function itself
- The output from the function
You can think of a function as a machine (black box) that takes some input(s), does something, and then produces an output.
Once you define a function, you can reuse it. This is the principal reason for their existence. Don't Repeat Yourself! (DRY)
I have included 5 examples of functions. For now, I want you to understand the idea as a concept. We will see examples 1, 2, 4, and 5 in Python very soon!
1. Example Function - Is a Number Odd?
The first function takes in 1 input (an int
) and produces 1 output (a bool
).
This function is a machine that tells us whether the input is odd.
For now, we don't care how it works, we just assume it does.
The following image depicts the function on the left and then two examples of it being used on the right.
2. Example Function - Add Two Numbers
The second function takes two inputs which are numbers (float
) and outputs the sum of those two numbers.
Key point, a function can take multiple inputs!
3. Example Function - What Colour is a Shape?
The third function might look odd as in python we do not have a Shape
type (yet!). Later in the course, we will make one.
It serves to demonstrate that a function is just a machine that does something to the input and produces an output.
4. Example Function: Print Hello Name
The final two examples are quite common. The first has an input, but no output. It takes in a person's name (e.g "Emily"
) and prints "Hello Emily"
to the terminal.
We will talk about this and its relationship to Python's None
.
5. Example Function: Print Hello World
The final example has no input and no output. It is just a machine that runs a piece of code.
In this case, it just prints "Hello World"
.
Passing Unit Tests
This is very important
To date, we have been using input/output tests. From now on we will be mainly using unit tests that import functions from your files.
These allow us to test individual functions. We will discuss this properly later on in the module.
There are two main points here:
- You should only write code in either a function block. e.g. the
add()
function. - You should write all other code that runs your program and calls the functions in the
if __name__ == "__main__"
block.
Any code outside of this can cause problems with the tests!
Function Basics
As we mentioned in the overview a function consists of three parts:
- The input(s) to the function
- The function itself
- The output from the function
You can think of a function as a machine (black box) that takes some input(s), does something, and then produces an output.
Once you define a function, you can reuse it. This is the principal reason for their existence. Don't Repeat Yourself! (DRY)
In essence it is a way of packaging up bits of code for reuse or to make your program more readable.
1. Defining a Function in Python
We define a function using the keyword def
, a function name, and the function parameters. We then write the code the function performs in the function body.
def function_name(<formal parameters>):
<function body>
The easiest way to demonstrate a function is by an example.
Our first function will just add 5 to the input
def add_five(x):
return x + 5
Note that the return
keyword is how you return (output) something from a function. Here the function add_five()
returns the result of adding 5
to the input x
.
2. Calling a Function in Python
add_five()
is now a function that you can call with different values of x
. e.g. add_five(10)
Try running the following code,
def add_five(x):
return x + 5
result = add_five(10) # call function with 10 and bind result
print(result) # this prints the value of result
and you will get
15
as the output.
You can visualise this yourself on Python Tutor.
Note that we could have called this function twice.
def add_five(x):
return x + 5
result_one = add_five(10) # call function with 10 and bind result
result_two = add_five(7) # call function with 7 and bind result
print(result_one) # this prints the value in result_one
print(result_two) # this prints the value in result_two
and you will get
15
12
as the output.
2.1 Define a Function Before You Call It
Now try the following,
result = add_five(10)
print(result)
def add_five(x):
return x + 5
and you will get NameError
. This is because the function has not yet been defined, i.e. it is below the function call. Python does not know about that name.
2.2 Order of Precedence
We can also do more interesting things like:
def add_five(x):
return x + 5
result = add_five(10)*add_five(7) # same as 15*12
print(result) # prints 180
Here Python will evaluate add_five(10)
and then evaluate add_five(7)
, then multiply them together.
This is because other than parentheses ()
, function calls have higher precedence than everything else in Python.
2.3 Multiple Return Statements
Consider the following function.
def greater_than_five(x):
if x > 5:
return True
else:
return False
if __name__ = "__main__":
print(greater_than_five(3)) # prints False
print(greater_than_five(7)) # prints True
Here we use an if
statement to test whether x
is greater than 5
, if it is the function returns True
, otherwise it returns False
.
This is a key point, although your function can only return one output, it can have more than one return
statement in your function.
Note that we could have done this with one return
statement by returning the result of the boolean expression.
def greater_than_five(x):
return x > 5
if __name__ = "__main__":
print(greater_than_five(3)) # prints False
print(greater_than_five(7)) # prints True
Again which one you use will be your preference and which one you are more comfortable with. Both ways are fine. If the second one isn't for you, then use the first one.
3. if __name__ = "__main__":
You will have noticed in the last couple examples used the if __name__ = "__main__":
block. This is not needed to call your functions, but as stated in the 5.1 - MP: Passing Unit Tests you should write any code and calls to your functions inside this block.
The reason is that the unit tests import your code and anything outside the if __name__ = "__main__":
will get run due to the import. This can mess up the unit tests!
=== TASK ===
Create a function that tests whether a number is odd or even.
Copy the following into a new Python file.
def odd_test(x):
# delete pass and replace with code to implement the function
pass
if __name__ == "__main__":
print(odd_test(3)) # should output True
print(odd_test(8)) # should output False
Your function odd_test
and take in one input.
It should return a True
or False
depending on the number.
For example:
odd_test(3)
should return True
and odd_test(8)
should return False
.
More About Functions
HINT: I would copy and past examples into a Python interpreter to make sure you understand them.
This is one of the most important lessons on functions. Please make sure you understand all 3 of the following.
In this lesson we will cover the following:
- Different types of functions
- Parameters vs arguments
- Function composition
All of these are important concepts and will help you understand later material.
1. Types of Function
1.1 Function with Multiple Parameters
We are not limited to just one parameter, we can have as many as we like. The following two functions take two parameters.
The first returns the sum of the two inputs.
def add(x, y):
return x + y
if __name__ == "__main__":
result = add(3, 13)
print(result) # prints 16
The seconds returns the maximum of the two inputs.
def maximum(x, y):
if x > y:
return x
else:
return y
if __name__ == "__main__":
result = maximum(3, 5)
print(result) # prints 5
You will note that this function has two uses of return
for the two possible cases.
1.2. Function with No Return Value
Here we define a function with a single parameter name
(the input of the function).
def print_hello(name):
print(f"Hello {name}")
This is interesting as it takes an input name
and does something with the input. However, it does not have a return
statement.
Be careful though, this function does return something, it returns the None
keyword which represents no value.
Run the following code and you will see this for .
def print_hello(name):
print(f"Hello {name}")
if __name__ == "__main__":
# call the function and bind the return value to a
a = print_hello("Jimi")
print(a)
You will notice that it outputs,
Hello Jimi
None
because a
stores the return value of print_hello()
which is None
.
1.3. Function with No Input and No Return Value
The final function has no input and no return
statement. Its sole purpose is to print two lines.
def print_hello_world():
print("Hello World")
print("Welcome to another day on planet earth!")
if __name__ == "__main__":
print_hello_world()
print_hello_world()
The code above will output,
Hello World
Welcome to another day on planet earth!
Hello World
Welcome to another day on planet earth!
as the function is called twice.
1.4. Some Familiar Functions
You have already met a bunch of Python functions.
Function | Input | Returns | Purpose | Example |
---|---|---|---|---|
print() | str to output to the terminal. | None | Prints the input to the terminal. | print("Hello World") |
input() | str to output to the terminal. | str entered by the user. | Prints the input to the terminal and returns what the user inputted as a string | input("Please enter your name:\n") |
int() | str representation of a whole number, e.g."5" | int conversion of input. e.g. 5 | Converts a str to an int if possible. If not throws ValueError | int("5") |
1.5 The NotImplementedError
Sometimes it is convenient to define a function, but not write the code yet. This is especially useful when planning a program. It is also useful to make sure that if this function is called it doesn't result in a silent bug.
To stop this, we can define a function that raises a NotImplementedError
. This as the name suggests means that you have not implemented the function.
Here we set up a program for computing the total and length of a list. The length()
function has not been implemented and raises a NotImplementedError
.
def total(num_list):
""" compute the total of the numbers in the list"""
tot = 0
for x in num_list:
tot += x
return tot
def length(num_list):
""" count the number of numbers in the list"""
raise NotImplementedError("The length function has not been implemented yet!")
if __name__ == "__main__":
num_list = [4,3,5,7,4]
print(f"The total of the list is {total(num_list)}") # This line will run
print(f"The number of numbers in the list is {length(num_list)}") # This line will fail
If you try this in a Python file you will get the output.
The total of the list is 23
Traceback (most recent call last):
File "main.py", line 14, in <module>
print(f"The number of numbers in the list is {length(num_list)}") # This line will fail
File "main.py", line 10, in length
raise NotImplementedError("The length function has not been implemented yet!")
NotImplementedError: The length function has not been implemented yet!
This is because when we call the length()
function it raises the NotImplementedError
with the message The length function has not been implemented yet!
.
Note thought that Python actually ran the code before it and printed out the list total.
1.6 The Pass Statement
Another way to do this is by using the pass
statement. When a programmer doesn't know what to write or is creating a skeleton program they will sometimes put pass
. pass
will mean the code is valid and can therefore be run.
def total(num_list):
""" compute the total of the numbers in the list"""
tot = 0
for x in num_list:
tot += x
return tot
def length(num_list):
""" count the number of numbers in the list"""
pass
if __name__ == "__main__":
num_list = [4,3,5,7,4]
print(f"The total of the list is {total(num_list)}") # This line will run
print(f"The number of numbers in the list is {length(num_list)}") # This line will run but prints - The number of numbers in the list is None
The length()
function has a pass statement and will now return None
.
Try this in a Python Tutor
2. Parameters vs Arguments
It is important to understand the difference between the parameters of a function and arguments. These words are often confused or used interchangeably.
Parameters
Parameters are the names that are in the function definition in between the parentheses ()
.
Arguments
Arguments are the names that are used in the function call.
The following image shows both the function definition and the function call.
3. Function Composition
Function composition is the act of applying a function to the result of another function.
The name is taken from the mathematical concept of function composition.
Consider the following code.
def add5(x):
return x + 5
def mul2(x):
return 2*x
if __name__ == "__main__":
x = 10 # binds 10 to x
x = add5(x) # binds 15 to x, i.e. 10 + 5 = 15
x = mul2(x) # binds 30 to x, i.e. 2*15 = 30
print(x) # prints 30
We can write this using function composition, note that we evaluate the innermost function first. So work inner to outer.
The example below first evaluates add5()
and then evaluates mul2()
with the result of add5()
.
x = 10 # binds 10 to x
x = mul2(add5(x)) # first computes add5(10) = 15, then computes 2*15 = 30 and binds to x
print(x) # prints 30
We can go further and compose three functions. Now we pass the result of the first two to the print()
function.
x = 10
print(mul2(add5(x))) # first computes add5(10) = 15, then computes 2*15 = 30 and then prints 30
You could even just do the following.
print(mul2(add5(10))) # does the whole lot in one line!
OK great, we did it in one line!
However, take a look at the readability of the code. The examples which use function composition here are not exactly very readable, in fact, it obfuscates things.
The first example is clear. Assign 10
to x
, then add 5
to x
, then multiply x
by 2
and finally print x
.
There are many examples of complex programs that we can write in python with one-liners. This is fine if it is just for your eyes only.
I would suggest for readability you avoid it unless it is necessary or makes sense and is readable.
3.1 Function Composition Isn't Commutative
It is also important to know that function composition isn't commutative, that is just a fancy way of saying that if you change the order of evaluation it doesn't have the same result.
For example,
3 * 5 == 5 * 3 # True, because multiplication doesn't care about the order (commutative)
3 / 5 == 5 / 3 # False, division cares about the order (not commutative)
The following example shows the add5()
and mul2()
functions composed in the two possible ways give two different results.
def add5(x):
return x + 5
def mul2(x):
return 2*x
if __name__ == "__main__":
x = 10
y = mul2(add5(x))
z = add5(mul2(x))
print(y) # prints 30
print(z) # prints 25
You can try this in Python Tutor and see that y
and z
are different values because the order of composition was changed.
=== TASK ===
Copy the following into a new Python file. You will need to remove the raise NotImplementedError
and replace it with your own code. Make sure you read the whole of task.
def reverse(str_to_reverse):
raise NotImplementedError("You have not implemented the reverse function")
def get_character(string_one, i):
raise NotImplementedError("You have not implemented the get_character function")
if __name__ == "__main__":
print(reverse("This is a string")) # prints "gnirts a si sihT"
print(reverse("Hello World")) # prints "dlroW olleH"
print(get_character("This is a string", 3)) # prints "i"
print(get_character("Hello World", 5)) # prints "o"
The first function is called reverse()
and has a single parameter, a string. The function should output the reverse of the string.
You should do this using a loop. There is a simple way to do this in Python, but it is good practice to write your own reverse()
method.
The second function is called get_character()
and has a string and a number as parameters. It should return the ith character of the string.
The table below gives the return values of the functions for different calls.
Function call | Expected return value |
---|---|
reverse("This is a string") | "gnirts a si sihT" |
reverse("Hello World") | "dlroW olleH" |
get_character("This is a string", 3) | "i" |
get_character("Hello World", 5) | "o" |
I suggest you test the above work correctly in the terminal, you should not rely on the tests as the feedback may not be useful.
The Maximum of a List Program
We are now going to recycle our maximum()
function.
def maximum(x, y):
if x > y:
return x
else:
return y
1. Planning the Program
Here is an outline of the program in pseudocode.
Given a list
Store the first value of the list as current_max
for each element x in the list (not including the first)
current_max = max(current_max, x)
print out current_max as the result
2. Program - Maximum of a list
Let's now implement this and use our maximum()
function in a program to loop through a list of numbers and print out the maximum.
def maximum(x, y):
if x > y:
return x
else:
return y
if __name__ == "__main__":
num_list = [3,6,2,1,8,4,4,2,7]
# set the current max to the first element in the list
current_max = num_list[0]
# loop through the list (start at the second element)
for x in num_list[1:]:
# store result of current_max and x
current_max = maximum(current_max, x)
print(f"Maximum number is {current_max}")
Hopefully, you can see that we didn't really care how maximum()
worked, we just knew that maximum()
takes in an x
and y
and returns the larger.
3. Program - Maximum of a list (Function)
I now want to take this opportunity to rewrite this. Let's create a function that can take in a list of numbers and outputs the maximum.
def maximum(x, y):
if x > y:
return x
else:
return y
def max_list(num_list):
current_max = num_list[0]
for x in num_list[1:]:
current_max = maximum(current_max, x)
return current_max
if __name__ == "__main__":
list_one = [3,6,2,1,8,4,4,2,7]
list_two = [30,16,4,45,27,84]
list_three = [-10,-4,-3,-2]
print(f"Maximum of list 1 is {max_list(list_one)}")
print(f"Maximum of list 2 is {max_list(list_two)}")
print(f"Maximum of list 3 is {max_list(list_three)}")
Congratulations, you have just written a function max_list()
that takes in a list
of numbers and returns the maximum.
This is a reusable function that we don't really care how it works, we just know it works! This is the essence of what we call decomposition and abstraction which we will look at in unit 7.
Interestingly Python provides such a function for us. Try typing this into the Python Interactive Shell.
max([5,2,4,7])
Again let me stress, max()
is a built-in function that we do not know how it works. We just know it takes list
and returns the maximum.
You can type the following into the Python Interactive Shell to get help information about the max()
function.
help(max)
Scope
This can be a tough one to get your head around, so I suggest you speak to the instructors in the labs if it doesn't make sense.
When you define a function you define a new namespace also called a scope.
Any variable defined within the function is said to be a local variable and can only be accessed inside the function (the scope of the function).
We use function parameters and the return value to exchange information between parts of our program.
NOTE: I do not use the if __name__ == "__main__":
block here, it is NOT required, but normally it is very good practice to use it.
1. Example 1
Let's illustrate this using the example from Guttag, J.V. (2021).
def f(x):
# variables defined here are only available to f() - local
# we also do not have access to variables created outside of f()
y = 1
x = x + y
print(f"x = {x}")
return x
# main body of code starts here
# note we have left out if __name__ == "__main__" here. It isn't needed, but is recommended.
x = 3
y = 3
z = f(x)
print(f"z = {z}")
print(f"x = {x}")
print(f"y = {y}")
When a function is called, it creates a new stack frame and any variable defined in the function scope exists in this stack frame and is local to this stack frame.
Once the function has finished, the stack frame is removed and the local variables are also removed.
Here x
and y
are first defined on the global frame. Then we call f(x)
which creates a new stack frame and new variables x
and y
. We say that information is passed to f()
These two x
's and y
s' are different! I cannot stress the importance of this. The same name, but different scope. They are not the same variable!
I have created an animation from Python Tutor which shows the stack frame that is created when the function is called and the other local variable x
.
I would also copy the example into Python Tutor and follow the program line by line.
2. Example 2
The following code defines two functions f()
and g()
and calls both of them one after another.
def f(x):
""" adds 1 to x and prints result"""
y = 1
x = x + y
print(f"x = {x}")
return x
def g(x):
""" adds 2 to x and prints result"""
y = 2
x = x + y
print(f"x = {x}")
return x
# main body of code starts here
x = 3
y = 3
z = f(x)
t = g(x)
print(f"z = {z}")
print(f"t = {t}")
print(f"x = {x}")
print(f"y = {y}")
This has the effect of creating a stack frame for f()
. When f()
is finished the stack frame is removed.
Next a stack frame for g()
is created. When g()
is finished the stack frame is removed.
The program now drops back to the global stack frame and completes.
The animation below illustrates this visually.
Again, I would also copy the example into Python Tutor and follow the program line by line.
3. Example 3
Each call to a function creates a new stack frame.
So if we call a function from within a function, we actually create a new stack frame on top of the previous stack frame.
The reason we call them stack frames is because they get stacked upon each other.
Here f()
has been updated to actually call g()
from within its function body.
def f(x):
""" adds g(x) to x and prints result"""
y = g(x)
x = x + y
print(f"x = {x}")
return x
def g(x):
""" adds 2 to x and prints result"""
y = 2
x = x + y
print(f"x = {x}")
return x
# main body of code starts here
x = 3
y = 3
z = f(x)
print(f"z = {z}")
print(f"x = {x}")
print(f"y = {y}")
You will see from the animation or, by experimenting on Python Tutor or using the debugger, that we end up with the following stack frames all at once.
- Global stack frame
- f - stack frame for
f()
- g - stack frame for
g()
Once g()
finishes it is popped off (removed) from the stack and we go back to f()
. Once f()
finishes we go back to the global frame.
Note that Python Tutor displays them underneath each other, if it helps think of them stacked on top of each other.
=== TASK ===
Don't skip reading the above lesson, in the long term it won't help you!
This one is a bit of a freebie.
Print out the following about scope, namespaces, local variables, and stack frames
print("When you define a function, you define a new namespace also called a scope.\n")
print("Any variable defined within the function is said to be a local variable and can only be accessed inside the function (the scope of the function).\n")
print("Calling a function creates a new stack frame and any variable defined in the function scope exists on this stack frame and is local to this stack frame.")
References
Guttag, J.V. (2021). Introduction to Computation and Programming Using Python, Third Edition (3rd ed.). MIT Press.
Keyword Arguments and Default Values
1. Parameters and Arguments
To date, our functions have just had parameters such as:
def print_name_age(name, age):
print(f"Hello {name}, your age is {age}")
This function takes two parameters; name
and age
. When we call the function we can provide arguments (values) to the function that are bound to the parameters.
So print_name_age("Jimi", 27)
will bind name = Jimi
and age = 27
. These are then used in the print
statement.
Formally these are known as positional parameters because you call the function and pass the arguments in the correct position.
2. Keyword Arguments and Default Values
Python provides another way of defining the parameters of a function called keyword arguments.
You can think of these as optional arguments, you will though need to give them a default value.
Let's look at an example from Guttag, J.V. (2021).
def print_name(first_name, last_name, reverse = False):
if reverse:
print(f"{last_name}, {first_name}")
else:
print(f"{first_name}, {last_name}")
print_name()
is now a function whereby the third parameter is a keyword argument with a default value of False
.
This means we can leave out the keyword argument. We can just call using the two positional parameters first_name
and last_name
. Inside the function reverse
will take on the default value of False
.
print_name("Jimi", "Hendrix") # prints out Jimi, Hendrix
We can also choose to call the function and overwrite the value of the keyword argument, for example we can tell the function to set reverse
to False
.
print_name("Jimi", "Hendrix", reverse = True) # prints out Hendrix, Jimi
which prints out Hendrix, Jimi
because reverse is True
.
There is also nothing stopping you setting reverse
to False
, this is perfectly legal, but not necessary.
print_name("Jimi", "Hendrix", reverse = False) # prints out Jimi, Hendrix
2.1 Order matters.
You can't put keyword arguments before positional parameters.
The following is illegal in Python:
def print_name(reverse = False, first_name, last_name):
if reverse:
print(f"{last_name}, {first_name}")
else:
print(f"{first_name}, {last_name}")
2.2 Another Simple Example
Let's say you want a price()
function that calculates discounted price.
This has an obvious default, that is, no discount.
Here is a function that reflects this. Note that discount is a percentage between 0
and 100
.
def calculate_price(price, discount=0):
""" returns new price with discount applied"""
return price * (1 - discount/100)
if __name__ == "__main__":
print(calculate_price(30)) # will just return 30. No discount
print(calculate_price(30, discount=50)) # will just return 15. 50% discount on the price
We could extend this to add an optional tax argument.
def calculate_price(price, discount=0, tax=20):
""" returns new price with discount applied and tax"""
total_before_tax = price * (1 - discount/100)
total_after_tax = total_before_tax * (1 + tax/100)
return total_after_tax
if __name__ == "__main__":
print(calculate_price(30)) # will just return 36. No discount, tax=20
print(calculate_price(30, discount=50)) # will return 18. 50% discount on the price, tax=20
print(calculate_price(30, discount=50, tax=25)) # will return 18.75. 50% discount on the price, tax=25
3. A Few More Keyword Arguments
Just to complete the picture. Here is a function with two keyword arguments. You can have any number of keyword arguments.
def print_person(first_name, last_name, reverse=True, age=None):
if reverse:
print(f"{last_name}, {first_name}")
else:
print(f"{first_name}, {last_name}")
if age:
print(f"Aged {age}.")
We can call this function a number of different ways.
print("Jimi", "Hendrix") # Prints Jimi, Hendrix
print("Jimi", "Hendrix", age = 27)
# Prints
# Jimi, Hendrix
# Aged 27.
print("Jimi", "Hendrix", age = 27, reverse = True)
# Prints
# Hendrix, Jimi
# Aged 27.
Some of you may have spotted in the last example that age
and reverse
are given in a different order to the function definition.
This is deliberate, once you have given the positional arguments, you can give the keyword arguments in any order.
4. Mutable Keyword Arguments (DO NOT USE)
Consider the following code:
def test_keyword_mutability(list_one=[]):
list_one.append("sam")
return list_one
if __name__ == "__main__":
test_keyword_mutability()
test_keyword_mutability()
We would expect this to print out:
['sam']
['sam']
Instead it will print out:
['sam']
['sam', 'sam']
This wasn't the intention, this is because the keyword argument list_one=[]
is mutable and Python stores it the when the function is defined, not when it is called.
We can correct this by doing the following:
def test_keyword_mutability(list_one=None):
if list_one is None:
list_one = []
list_one.append("sam")
return list_one
if __name__ == "__main__":
test_keyword_mutability()
test_keyword_mutability()
['sam']
['sam']
Much better!
For more information you can see this link:
Python Mutable Defaults Are The Source of All Evil
=== TASK ===
Write a function called print_list
that prints the multiple of each item in the list
For example print_list([4,1,6,7], multiple = 2)
8
2
12
14
For example print_list([4,1,6,7], multiple = 3)
12
3
18
21
By default your function when called without the keyword argument multiple
, i.e. print_list([4,1,6,7])
should output:
4
1
6
7
Copy the following into a new Python file to get started.
def print_list():
pass
if __name__ == "__main__":
print_list([4,1,6,7], multiple = 2)
print_list([4,1,6,7], multiple = 3)
print_list([4,1,6,7])
Note: Due to the way that the tests are written for this task, an incorrect solution may result in the tests looking as though they are "hanging". Refresh the page to reattempt the test after considering why your solution may be incorrect.
References
Guttag, J.V. (2021). Introduction to Computation and Programming Using Python, Third Edition (3rd ed.). MIT Press.
Specification and Docstrings
When defining functions it is important to inform the person using your function of three things
- What does the function do?
- What are the parameters of the function and what type should they be?
- What does the function return?
Sometimes a function won't take parameters or return anything, so not all of these need documenting.
The standard way of doing these in Python is docstrings. A docstring uses triple quotes """
.
1. Creating a Docstring
It is easiest to look at a simple example.
The following function is the simple add function we have seen before:
def add(x, y):
return x + y
The answers to the 3 questions above for this function are
- Adds up two numbers and return the result.
x
(int
orfloat
),y
(int
orfloat
). These are the numbers to add.- Returns the sum of
x
andy
.
The following code snippet shows how we add these with a docstring.
def add(x, y):
"""
Adds up two numbers and returns the result.
Parameters
----------
x: int or float
First number to add.
y: int or float
Second number to add.
Returns
-------
int or float
Sum of x and y.
"""
return x + y
Our basic template is:
"""
Function description
Parameters
----------
param_name: type
Description of parameter
param_name: type
Description of parameter
.
.
.
Returns
-------
type
Description of return value
"""
We will be using the NumPy standard in this course that works with the documentation generator library Sphinx. We are not looking at this in detail in this course but is important to know about it.
If we are missing either the parameters or return statement then we just leave these out of the docstring.
def print_hello():
"""
Prints Hello.
"""
print("Hello")
def print_sum(x, y):
"""
Prints the sum of the two numbers.
Parameters
----------
x: int or float
First number to add.
y: int or float
Second number to add.
"""
print(x + y)
And even though you would rarely see a function like the following, for completeness.
def five():
"""
Returns 5
Returns
-------
int
Always returns 5
"""
return 5
2. Python's help() Function
Python has many built-in functions. You can find an extensive list below.
One of these functions is help()
which will display the docstring of a function.
Try the following in the terminal:
help(abs)
This gives you the docstring for the built-in function abs()
.
3. When To Document
Generally, when you are hacking away at your own code, documentation probably will be furthest from your mind. I would still suggest putting in a short description.
Every programmer has written some complicated code and then come back to it at a later date and not had a clue what it does. This then requires time, some hints will help you.
Clearly when working with others and on a shared codebase documenting is important. It is also important if people will be using your code and they would like some hints by using the help()
function. If you don't write a docstring, there won't be any help!
=== TASK ===
Copy the following into a new Python file. You won't have to understand this code to pass the task, but you will need to read on.
def add(x, y):
"""
Adds up two numbers and returns the result.
Parameters
----------
x: int or float
First number to add.
y: int or float
Second number to add.
Returns
-------
int or float
Sum of x and y.
"""
return x + y
# Example from Guttang (2021)
def find_root(x, power, epsilon=0.01):
"""
Find a y such that y**power = x (within epsilon x).
e.g. For x = 27 and power = 3 then y = 3 as 3**3 = 27.
i.e. the cubed root of 27 is 3.
Parameters
----------
x :int or float
Number we want the root of.
power: int
Root number e.g. square root, power = 2.
epsilon: int or float, default 0.01
Error tolerance for the answer.
Returns
-------
float or None
"""
# if x is negative and power even. No root exists
if x < 0 and power % 2 == 0:
return None
low = min(-1.0, x)
high = max(1.0, x)
ans = (high + low) / 2.0
# check if the answer is close enough
while abs(ans**power - x) >= epsilon:
if ans**power < x:
low = ans
else:
high = ans
ans = (high + low) / 2.0
return ans
if __name__ == "__main__":
# add the line of code here
pass
The built-in help()
function allows us to print out the docstring of a function.
You will also see a more complicated function called find_root
.
For now, it doesn't matter how it works, but the docstring does tell us what it does and requires a longer description.
Add a line of code to the bottom that prints out the docstring for find_root
by using the help()
function. Make sure you don't use any indentation so that the code isn't inside a function.
Think carefully, help()
is a function that prints out to the terminal and returns None
.
The Receive and Return Program
This is an example program from:
Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.
It demonstrates three variations of functions.
display()
receives one parameter message
, prints it out, and then does not have a return
statement. It will therefore return None
.
give_me_five()
receives no parameters and it returns the value 5
.
ask_yes_no()
receives one parameter question
and asks the user to answer "y"
or "n"
. It returns the user's response.
1. Complete Program
# Receive and Return
# Demonstrates parameters and return values
def display(message):
print(message)
def give_me_five():
return 5
def ask_yes_no(question):
"""Ask a yes or no question."""
print(question)
print("\nPlease enter 'y' or 'n': ")
response = None
while response not in ("y", "n"):
response = input().lower()
return response
if __name__ == "__main__":
# main
display("Here's a message for you.\n")
number = give_me_five()
print("Here's what I got from give_me_five():", number)
answer = ask_yes_no("\nDo you like Python? ")
print("\nThanks for entering:", answer)
input("\n\nPress the enter key to exit.")
2. The Function Specifications
# Receive and Return
# Demonstrates parameters and return values
def display(message):
"""Prints out the message.
Parameters
----------
message: str
Message to display.
"""
print(message)
def give_me_five():
"""Returns the value 5.
Returns
-------
int
Always returns 5
"""
return 5
def ask_yes_no(question):
"""Ask a yes or no question to the user and return the answer "y" or "n".
Parameters
----------
question: str
The question to ask the user.
Returns
-------
str
The response of the user. Either "y" or "n".
"""
print(question)
print("\nPlease enter 'y' or 'n': ")
response = None
while response not in ("y", "n"):
response = input().lower()
return response
OK, so our code now looks more verbose, but it is much clearer to someone else what the function does.
Sometimes functions are complicated and giving the person reading it a good specification is important.
How this is done can differ between projects and programmers, but in larger codebases, it is very important as programmers may come and go or hundreds or thousands of programmers may work on a single codebase.
Passing by Assignment
WARNING: This is really fundamental and can cause major bugs and errors if you don't understand it.
I strongly suggest that you paste the examples in Python Tutor and work through them.
If you are struggling to follow this then please speak to the instructors in the practicals.
Python differs from other programming languages in that everything is an object.
Some of these objects are immutable (cannot be changed) and some are mutable (can be changed).
Immutable and mutable types are common in most languages and how these are dealt with differs.
We are specifically talking about what Python does.
1. Immutable vs Mutable Types
1.2 Immutable Type
An immutable type is something that once created cannot be changed. Immutable types you have seen in Python are str
, float
, int
and bool
.
Consider the following example which uses int
. Here we also use the is
keyword to see if x
and y
are the same object in memory.
x = 1
y = x
print(x is y) # prints True. They are the same object in memory
y += 1
print(x) # prints 1
print(y) # prints 2
print(x is y) # prints False. They are now different objects in memory
The first print(x is y)
prints True
because x
and y
are both attached to the same immutable object 1
.
The reason the second print(x is y)
prints False
is because to start with x
and y
are attached to the immutable object 1
; however, when we add 1
to y
Python creates a new object and reassigns y
to 2
.
Now x
points to the object 1
in memory and y
points to the object 2
in memory. Different objects!
1.2 Mutable Type
Lists are mutable. If we do something similar to the above we get a very different result.
Consider the following example which uses list
. Here we also use the is
keyword to see if list_one
and list_two
are the same object in memory.
list_one = [1,2,3]
list_two = list_one
list_two.append(4)
print(list_one) # prints [1,2,3,4]
print(list_two) # prints [1,2,3,4]
print(list_one is list_two) # prints True. They are the same object in memory
When we assign list_two = list_one
both names are pointing to the same object [1,2,3]
in memory.
When we call the append method, we add 4
to the end of the list [1,2,3]
. Both list_one
and list_two
still point to the same list which is now [1,2,3,4]
. Hence print(list_one is list_two)
prints True
.
We can stop this from happening by using a copy.
Here we assign list_two
to a copy of the object [1,2,3]
in memory. This now means that list_one
points to one object [1,2,3]
and list_two
points to a separate object [1,2,3]
.
list_one = [1,2,3]
list_two = list_one.copy() # now we create a copy
list_two.append(4)
print(list_one) # prints [1,2,3]
print(list_two) # prints [1,2,3,4]
print(list_one is list_two) # prints False. They are now different objects in memory
The append()
method is now only called on the object attached to list_two
and therefore the object attached to list_one
is not updated. Hence print(list_one is list_two)
prints False
.
2. Passing by Assignment
When we pass arguments to functions, what we are actually doing is binding objects to the parameters of the function.
2.1 Passing an Immutable Type
n Python, a variable is just a name (label) that can be attached to an object. When we pass an object to a function, the function parameter is attached to the passed object
In the example below x
will be attached to whatever is passed into the function add_one()
. If you pass in an immutable object, as soon as you try to update the object it will create a new object to reflect the changes.
Remember immutable objects can't be changed.
The following example illustrates this. We pass in a number that is attached to num
and then add 1
. Because we are trying to change an immutable object, we have to create a new object. However, we don't return anything and reassign, so the original variable num_one
is not changed.
def add_one(num):
num += 1 # add one to the local variable x
if __name__ == "__main__":
num_one = 1
print(num_one) # prints 1
add_one(num_one)
print(num_one) # prints 1
We can reflect the changes by returning the result and reassigning it to num_one
.
def add_one(num):
num += 1
return num # return the new object created by adding 1
if __name__ == "__main__":
num_one = 1
print(num_one) # prints 1
num_one = add_one(num_one) # reassign x1 to the result of add_one()
print(num_one) # prints 2
2.2 Passing a Mutable Type
When you pass a mutable object to a function and change it you are updating the original object. That is doing something to the object passed to the function doesn't create a new copy, it operates on the same object!
Here is a simple example that adds 4
to the end of a list.
def func_one(items):
""" append 4 to the list. """
# append adds to the end of the list
items.append(4)
if __name__ == "__main__":
list_one = [1, 2, 3]
func_one(list_one) # call func_one with list_one
print(list_one) # prints [1,2,3,4] - this has been updated by the function call
The object attached to variable list_one
is first passed to func_one()
and the variable items
is attached to the object.
The function then adds 4
to the end of the list using the append()
method. What happens here is both list_one
and items
point to the mutable list. When it is updated, both are updated as they point to the same object.
Below are some more examples that illustrate passing a list to a function.
def func_one(items):
""" append 4 to the list. """
# append adds to the end of the list
items.append(4)
def func_two(items):
""" add the list and [4] """
# this creates a new object by adding the lists items and [4]
items = items + [4]
def func_three(items):
""" add the list and [4] """
# again this creates a new object by adding the lists items and [4]
items = items + [4]
# however this time we return the new object
return items
if __name__ == "__main__":
list_one = [1, 2, 3]
func_one(list_one) # call func_one with list_one
print(list_one) # prints [1,2,3,4] - this has been updated by the function call
list_one = [1, 2, 3]
func_two(list_one) # call func_two
print(list_one) # prints [1,2,3] - this has not been updated by the function call
list_one = [1, 2, 3]
list_two = func_three(list_one) # call func_three and bind to the variable list_two
print(list_two) # prints [1,2,3,4] - the new object created by the function call
func_one()
operates on the original list, this means you don't need to return anything. You could choose to return the list here, but there is no point.
func_two()
adds the original list and the list [4]
. This creates a new list but we do not return it and therefore we lose the new list.
func_three()
adds the original list and the list [4]
. This creates a new list and we return it. This means we can use it outside of our function.
I would experiment with these in Python Tutor to make sure you understand what is going on.
3.3 To Return or Not To Return
Generally, if you are operating on a mutable object and you don't need to keep a copy of the original then there is no need to return anything.
If you need the original list intact then you need to copy the original list before you operate on it and return the modified copy. See the next section.
3.4 Copy and Deep Copy
We will talk about these more in unit 6, but we can stop Python from updating the original list by using a copy of the list.
For now, we will only need the copy()
function as we will demonstrate on a list of numbers. Things get more complicated if we operate on a list that contains other mutable data types like a list of lists.
import copy
def func_four(items):
""" append 4 to a copy of the list. """
# create a copy of the list
new_list = copy.copy(items)
# append to the copy
new_list.append(4)
# return the copy
return new_list
if __name__ == "__main__":
list_one = [1,2,3]
list_two = func_four(list_one)
print(list_one) # prints [1,2,3]
print(list_two) # prints [1,2,3,4]
Note, we could have overwritten the original list by reassigning the return value. e.g.
list_one = [1,2,3]
print(list_one) # prints [1,2,3]
list_one = func_four(list_one)
print(list_one) # prints [1,2,3,4]
=== TASK ===
Create two functions. The first function update_list_item_one()
should take a list and a number and update the first item in the list with the number. It should operate on the original list. It does not require a return statement.
For example,
list_one = [1,2,3,4]
update_list_item_one(list_one, 0)
print(list_one) # prints out [0,2,3,4]
The second function new_list_item_one()
should create a copy of the list that is passed into the function (this will be a new object), update the first item in the new list and then return the new list.
For example,
list_one = [1,2,3,4]
new_list_item_one(list_one, 0)
print(list_one) # prints out [1,2,3,4]
does not update the original list, however,
list_one = [1,2,3,4]
list_one = new_list_item_one(list_one, 0) # bind the new object to list_one
print(list_one) # prints out [0,2,3,4]
reflects the changes made because the new object returned by new_list_item_one()
was reassigned to list_one
.
To get you started copy the following into a new Python file.
import copy
def update_list_item_one(items, x):
pass
def new_list_item_one(items, x):
pass
if __name__ == "__main__":
list_one = [1,2,3,4]
update_list_item_one(list_one, 0)
print(list_one) # should print out [0,2,3,4]
list_one = [1,2,3,4]
new_list_item_one(list_one, 0)
print(list_one) # should print out [1,2,3,4]
list_one = [1,2,3,4]
list_one = new_list_item_one(list_one, 0) # bind the new object to l
print(list_one) # should print out [0,2,3,4]
References
Python Docs - Passing by Assignment
Unit 6 - Lists, Tuples, Sets, and Dictionaries
Data structures are a fundamental concept in all programming languages as a means of organising and accessing data in your programs.
They allow us to store and retrieve information in different parts of our programs.
Python is no different and supports 4 built-in data structures.
- Lists
- Tuples
- Sets
- Dictionaries
Lists and Dictionaries are by far the most important and frequently used, but we will deal with them in the order given above in this unit.
Lists
Lists are by far the most used data structure in Python, so we will spend a bit more time on them.
The list
type in Python is used to store multiple items (objects) in a single variable.
We have already seen them in use, but here is a quick example:
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
This is a list of strings which has 5 items. If you need convincing that this is a list type, then you can use type(car_list)
to check the type.
Lists are hetrogeneous, which is a fancy way of saying we can store different data types. They can also store duplicates e.g.
car_list = ["Volvo", 1, False, "Ford", "Ford", False, 3.14]
This list has both different types and duplicates.
1. List Items
Each object in the list is called an item (element). We will use the previous example to demonstrate simple operations on lists.
If you want to follow along then type this into the Python interactive shell.
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
1.1 Access List Items
You can access a list item by it's index. Python indexing starts at 0
, so to get the first item of a list you would use.
For example we can print the first item using,
print(car_list[0]) # prints out Volvo
or the third item by using:
car_list[2] # prints out Honda
Notice how the car_list
has 5
items. So it's indexing goes from 0-4
.
If you try typing,
car_list[5] # IndexError
you will get an IndexError
because your list index if not in the valid range 0-4
.
1.1.1 Slicing
You can also access slices (portions) of a list. For example we can access items 1,2,3 using:
print(car_list[1:4]) # prints ["Volvo", "Audi", "Honda"]
1.1.2 Negative Indices
You can also access from the back of the list. To get the last item use -1
.
print(car_list[-1]) # prints ["Mercedes"]
You can also get slices such as the last three items.
print(car_list[-4,-1]) # prints ["Honda", "Ford", "Mercedes"]
1.2 Changing List Items
You change a list item using indexing as follows:
car_list[2] = "Ferrari"
If we print out the list,
print(car_list) # prints ["Volvo", "Audi", "Ferrari", "Ford", "Mercedes"]
you will get ['Volvo', 'Audi', 'Ferrari', 'Ford', 'Mercedes']
.
1.3 Adding List Items
You can add to a list by using the append()
and insert()
methods.
Methods are very much like functions, they take in arguments and return something. Hey, they even look like them!
The key difference is a method is attached to an object and you call the method using dot notation.
To add to the end of the list use append()
method. Note that it is called using a .
:
car_list.append("Skoda") # ["Volvo", "Audi", "Ferrari", "Ford", "Mercedes", "Skoda"]
Here the str
"Skoda"
is added to the end of the list.
To insert an item somewhere use the insert()
method, you supply the index as the first parameter and the item as the second.
car_list.insert(2, "Porsche") # insert "Porsche" at index 2, i.e. 3rd item
# ["Volvo", "Audi", "Porsche", "Ferrari", "Ford", "Mercedes", "Skoda"]
1.4 Extending and Joining Lists
To extend the list by multiple items you can use either the extend()
method or the +
operator.
list_one = ["a", "b" , "c"]
list_two = [1, 2, 3]
list_one.extend(list_two) # list_one now contains ["a", "b", "c", 1, 2, 3]
list_one = ["a", "b" , "c"]
list_two = [1, 2, 3]
list_three = list_one + list_two # list_three contains ["a", "b", "c", 1, 2, 3]
Note the difference, the extend()
method extends the list it is operating on (e.g. list_one
). The +
operator creates a new list by joining the lists, we stored this in list_three
.
We can use the +
operator to join many lists.
list_one = ["a", "b" , "c"]
list_two = [1, 2, 3]
list_three = [True, False]
list_four = list_one + list_two + list_three # list4 contains ["a", "b", "c", 1, 2, 3, True, False]
1.5 Remove List Items
Again working on the car example:
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
You can remove a list item using either remove()
or pop()
methods, or the del
keyword.
remove()
lets you remove an item by specifying the value of the item, .e.g.
car_list.remove("Audi") # removes the second item of the list
If you try to remove an object that isn't in the list you will get a ValueError
. For example
car_list.remove("Ferrari") # ValueError: list.remove(x): x not in list
Note that this returns None
. This is demonstrated by this code snippet.
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
item_removed = car_list.remove("Audi")
print(car_list) # prints ["Volvo", "Honda", "Ford", "Mercedes"]
print(item_removed) # prints None
Here we stored the return
value of remove()
in item_removed
which was None
.
pop()
lets you remove an item by index, e.g.
car_list.pop(1) # removes the second item of the list
Note that this returns the item that is removed from the list. This is demonstrated by this code snippet.
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
item_removed = car_list.pop(1)
print(car_list) # prints ["Volvo", "Honda", "Ford", "Mercedes"]
print(item_removed) # prints Audi
The del
keyword also lets you remove by index, e.g.
del car_list[1] # removes the second item of the list
We can also remove slices.
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
del car_list[1:3]
print(car_list) # prints ["Volvo", "Ford", "Mercedes"]
1.6 Note on In-place Methods
All the methods we have seen operating on a list are known as an in-place method as they update the list directly. We will discuss this more in the next lesson.
Also be careful with understanding what a method returns. If you tried the following,
print(car_list.append("Renault")) # prints None
it will update the list, but print the return
value which is None
.
2. List Methods
There are a number of built-in list methods you can use:
Method | Description |
---|---|
append() | Adds an element at the end of the list |
clear() | Removes all the elements from the list |
copy() | Returns a copy of the list |
count() | Returns the number of elements with the specified value |
extend() | Add the elements of a list (or any iterable), to the end of the current list |
index() | Returns the index of the first element with the specified value |
insert() | Adds an element at the specified position |
pop() | Removes the element at the specified position |
remove() | Removes the item with the specified value |
reverse() | Reverses the order of the list |
sort() | Sorts the list |
For example we can sort the list as follows:
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
car_list.sort() # in-place operation, sorts car_list
print(car_list) # prints ['Audi', 'Ford', 'Honda', 'Mercedes', 'Volvo']
3. Looping Through a List
You can loop through a list using a for
loop.
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
for car in car_list:
print(car)
# prints out...
# Volvo
# Audi
# Honda
# Ford
# Mercedes
We can also loop through a list using a while
loop.
car_list = ["Volvo", "Audi", "Honda", "Ford", "Mercedes"]
i = 0
while i < len(car_list):
print(car_list[i])
i += 1
# prints out...
# Volvo
# Audi
# Honda
# Ford
# Mercedes
3.1. List of lists
We can also have lists of lists. These are very common and you will need to now how to iterate (loop) over them
Here is an example that just iterates over the list and prints out each of the sublists.
list_one = [[4,2,6], [3,8,3], [5,4,7]]
for sub_list in list_one:
print(sub_list)
# prints out...
# [4,2,6]
# [3,8,3]
# [5,4,7]
As each of the sublists is a list, we can iterate over each of these.
list_one = [[4, 2, 6], [3, 8, 3], [5, 4, 7]]
for sub_list in list_one:
for x in sub_list:
print(x)
# prints out...
# 4
# 2
# 6
# 3
# 8
# 3
# 5
# 4
# 7
We can even write a simple program that gives us back a list of the total of each sublist. Here we create an empty list called sum_list
and then we compute the total of each sublist and store it in the sum_list
.
list_one = [[4, 2, 6], [3, 8, 3], [5, 4, 7]]
sum_list = []
for sub_list in list_one:
total = 0
for x in sub_list: # iterate over the sub_list
total += x # keep adding items in the sub_list to total
sum_list.append(total) # append the result to sum_list
print(sum_list) # prints [12,14,16]
3.2. Zipping Two Lists
Zipping lists together is a very useful thing to do when we want to combine lists together and loop over them.
The zip operation is demonstrated by the following code snippet. Note both must be the same length.
list_one = [1,2,3]
list_two = ["a","b","c"]
for x,y in zip(list_one, list_two):
print(x,y)
# prints
# 1 a
# 2 b
# 3 c
=== TASK ===
Copy the following into a new Python file to get started.
def longest_word(string_list):
pass # placeholder, delete and replace with your code
def num_characters(string_list):
pass # placeholder, delete and replace with your code
if __name__ == "__main__":
test_string = [["Hello", "World"], ["You", "say", "you", "want", "a", "revolution"], ["It's", "fine", "to", "celebrate", "success", "but", "it", "is", "more", "important", "to", "heed", "the", "lessons", "of", "failure"]]
test_string2 = [["Hello", "World"], ["Apple", "Pears"]]
print(longest_word(test_string)) # should print revolution
print(num_characters(test_string)) # should print 105
print(longest_word(test_string2)) # should print hello
print(num_characters(test_string2)) # should print 20
The first function is called longest_word
which takes in a list of a list of strings and returns the longest word in any of the lists. In the case of equal length words, return the first word you encounter. In the case of no words, e.g. an empty list []
return None
.
For example:
longest_word([["Hello", "World"], ["You", "say", "you", "want", "a", "revolution"], ["It's", "fine", "to", "celebrate", "success", "but", "it", "is", "more", "important", "to", "heed", "the", "lessons", "of", "failure"]]) # return revolution
The second is called num_characters
which takes in a list of a list of strings and returns the total number of characters in all of the strings in the list lists.
num_characters([["Hello", "World"], ["You", "say", "you", "want", "a", "revolution"], ["It's", "fine", "to", "celebrate", "success", "but", "it", "is", "more", "important", "to", "heed", "the", "lessons", "of", "failure"]]) # return 105
References
Python List Methods - W3schools
MP: The High Scores Program
This is an example program from:
Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.
The following is a mini-program that demonstrates the use of lists to add, store, remove and access information.
It is a simple high scores program, not exactly useful without a game attached to it, but it illustrates the concepts.
Note the use of choice = None
. This is useful to get the program started when we don't yet have a user defined choice
.
Run the code to see what it does.
# High Scores 1.0
# Demonstrates list methods
scores = []
choice = None
while choice != "0":
print(
"""
High Scores
0 - Exit
1 - Show Scores
2 - Add a Score
3 - Delete a Score
4 - Sort Scores
"""
)
choice = input("Choice: ")
print()
# exit
if choice == "0":
print("Good-bye.")
# list high-score table
elif choice == "1":
print("High Scores")
for score in scores:
print(score)
# add a score
elif choice == "2":
score = int(input("What score did you get?: "))
scores.append(score)
# remove a score
elif choice == "3":
score = int(input("Remove which score?: "))
if score in scores:
scores.remove(score)
else:
print(score, "isn't in the high scores list.")
# sort scores
elif choice == "4":
scores.sort(reverse=True)
# some unknown choice
else:
print("Sorry, but", choice, "isn't a valid choice.")
input("\n\nPress the enter key to exit.")
Note that you could refactor this code by moving each of the choices into a function. Here we have put the removal of a score into a function.
# This demonstrates that when you pass a list, you are accessing the original list, not a copy!
def remove_score(score, scores):
if score in scores:
scores.remove(score)
else:
print(score, "isn't in the high scores list.")
.
. # rest of code as before
.
# update choice 3 as follows.
elif choice == "3":
score = int(input("Remove which score?: "))
remove_score(score, scores)
To pass the tests you can just run the original program, however, you should try and write functions for the other choices. It is good practice. It will also aid your understanding of decomposition and abstraction in the next unit.
More on Lists
Please carefully read this lesson, it is very important!
I highly recommend you copy and paste code snippets into Python Tutor.
It visualises what is happening and will really aid your understanding!
1. Copy and Deep Copy
1.1. Copying a List
Copying a list is a bit stickier than you might imagine!
Lists are stored using some things called references.
The variable (name) stores the list location in memory.
list1 = [1,2,3]
list2 = list1 # copies the reference to list1
print(f"List 1 = {list1}")
print(f"List 2 = {list2}")
list2.append(4) # this will update both list1 and list2
print(f"List 1 = {list1}") # prints [1,2,3,4]
print(f"List 2 = {list2}") # prints [1,2,3,4]
The above copies the reference to the list object. We can check this by using the id()
function.
id(list1) # something like 140416978254080
id(list2) # will be the same id as above 140416978254080
To copy a list we use the copy()
method.
list1 = [1,2,3]
list2 = list1.copy() # copies the list to a new object and store
print(f"List 1 = {list1}")
print(f"List 2 = {list2}")
list2.append(4) # this will update both list1 and list2
print(f"List 1 = {list1}") # prints [1,2,3]
print(f"List 2 = {list2}") # prints [1,2,3,4]
Now you will see that list1
is not updated when you append to list2
.
Again we can check this with:
id(list1) # something like 140416978254080
id(list2) # will be a different id e.g. 140404308187456
1.2. Deep Copying a List
There is a caveat. When we have a list of lists, the copy()
method will create copies of the lists, but not new copies of the items in the list.
Again, I strongly suggest that you copy this into Python Tutor and visually understand what is happening.
For example,
demo_list = [[1],2,3]
copy_list = demo_list.copy()
print(id(demo_list)) # these ids will be different
print(id(copy_list))
# print item 1 ids
print(id(demo_list[0])) # these ids are the same
print(id(copy_list[0]))
# print item 2 ids
print(id(demo_list[1])) # these ids are the same
print(id(copy_list[1]))
# print item 3 ids
print(id(demo_list[2])) # these ids are the same
print(id(copy_list[2]))
Because the item at index 0 is actually a list and is mutable (see the next section). Changing the items in this list will be reflected in both the orginal list and the copy of the list.
For example,
demo_list = [[1],2,3]
copy_list = demo_list.copy() # do a shallow copy
demo_list[0][0] = "Sam" # change the first item in the first item (this is a list)
print(demo_list) # prints [["Sam"],2,3]
print(copy_list) # prints [["Sam"],2,3]
Yikes, this updated both lists! Be careful.
To fix this you can use the deepcopy()
function. You will need to import the copy module.
import copy
demo_list = [[1],2,3]
copy_list = copy.deepcopy(demo_list) # do a deep copy
demo_list[0][0] = "Sam" # change the first item in the first item (this is a list)
print(demo_list) # prints [["Sam"],2,3]
print(copy_list) # prints [[1],2,3]
Try running the above code in Python Tutor to get a better understanding of this.
2. Immutable vs Mutable
Python differs from other programming languages in that everything is an object.
Some of these objects are immutable (cannot be changed) and some are mutable (can be changed).
Immutable and mutable types are common in most languages and how these are dealt with differs.
2.1. Immutable Types
An immutable type is something that once created cannot be changed. Immutable types you have seen in Python are str
, float
, int
and bool
.
Consider the following example which uses int
. Here we also use the is
keyword to see if x
and y
are the same object in memory.
x = 1
y = x
print(x is y) # prints True. They are the same object in memory
y += 1
print(x) # prints 1
print(y) # prints 2
print(x is y) # prints False. They are now different objects in memory
The first print(x is y)
prints True
because x
and y
are both attached to the same immutable object 1
.
The reason the second print(x is y)
prints False
is because to start with x
and y
are attached to the immutable object 1
; however, when we add 1
to y
Python creates a new object and reassigns y
to 2
.
Now x
points to the object 1
in memory and y
points to the object 2
in memory. Different objects!
2.2. Mutable Types
Lists are mutable. If we do something similar to the above we get a very different result.
Consider the following example which uses list
. Here we also use the is
keyword to see if list_one
and list_two
are the same object in memory.
list_one = [1,2,3]
list_two = list_one
list_two.append(4)
print(list_one) # prints [1,2,3,4]
print(list_two) # prints [1,2,3,4]
print(list_one is list_two) # prints True. They are the same object in memory
When we assign list_two = list_one
both names are pointing to the same object [1,2,3]
in memory.
When we call the append method, we add 4
to the end of the list [1,2,3]
. Both list_one
and list_two
still point to the same list which is now [1,2,3,4]
. Hence print(list_one is list_two)
prints True
.
We can stop this from happening by using a copy.
Here we assign list_two
to a copy of the object [1,2,3]
in memory. This now means that list_one
points to one object [1,2,3]
and list_two
points to a separate object [1,2,3]
.
list_one = [1,2,3]
list_two = list_one.copy() # now we create a copy
list_two.append(4)
print(list_one) # prints [1,2,3]
print(list_two) # prints [1,2,3,4]
print(list_one is list_two) # prints False. They are now different objects in memory
The append()
method is now only called on the object attached to list_two
and therefore the object attached to list_one
is not updated. Hence print(list_one is list_two)
prints False
.
3. Passing by Assignment
WARNING: This is really fundamental and can cause major bugs and errors if you don't understand it.
This section is repeated from Unit 5, but it is so fundamental I want to present it again.
When we pass arguments to functions, what we are actually doing is binding objects to the parameters of the function.
3.1 Passing an Immutable Type
In Python, a variable is just a name (label) that can be attached to an object. When we pass an object to a function, the function parameter is attached to the passed object
In the example below x
will be attached to whatever is passed into the function add_one()
. If you pass in an immutable object, as soon as you try to update the object it will create a new object to reflect the changes.
Remember immutable objects can't be changed.
The following example illustrates this. We pass in a number that is attached to num
and then add 1
. Because we are trying to change an immutable object, we have to create a new object. However, we don't return anything and reassign, so the original variable num_one
is not changed.
def add_one(num):
num += 1 # add one to the local variable x
num_one = 1
print(num_one) # prints 1
add_one(num_one)
print(num_one) # prints 1
We can reflect the changes by returning the result and reassigning it to num_one
.
def add_one(num):
num += 1
return num # return the new object created by adding 1
num_one = 1
print(num_one) # prints 1
num_one = add_one(num_one) # reassign x1 to the result of add_one()
print(num_one) # prints 2
3.2 Passing a Mutable Type
Try running the code snippets in Python Tutor to get a better understanding of the following.
When you pass a mutable object to a function and change it you are updating the original object. That is, doing something to the object passed to the function doesn't create a new copy, it operates on the same object!
Here is a simple example that adds 4
to the end of a list.
def func_one(items):
""" append 4 to the list. """
# append adds to the end of the list
items.append(4)
list_one = [1, 2, 3]
func_one(list_one) # call func_one with list_one
print(list_one) # prints [1,2,3,4] - this has been updated by the function call
The object attached to variable list_one
is first passed to func_one()
and the variable items
is attached to the object.
The function then adds 4
to the end of the list using the append()
method. What happens here is both list_one
and items
point to the mutable list. When it is updated, both are updated as they point to the same object.
Below are some more examples that illustrate passing a list to a function.
def func_one(items):
"""append 4 to the list."""
# append adds to the end of the list
items.append(4)
def func_two(items):
"""add the list and [4]"""
# this creates a new object by adding the lists items and [4]
items = items + [4]
def func_three(items):
"""add the list and [4]"""
# again this creates a new object by adding the lists items and [4]
items = items + [4]
# however this time we return the new object
return items
list_one = [1, 2, 3]
func_one(list_one) # call func_one with list_one
print(list_one) # prints [1,2,3,4] - this has been updated by the function call
list_one = [1, 2, 3]
func_two(list_one) # call func_two
print(list_one) # prints [1,2,3] - this has not been updated by the function call
list_one = [1, 2, 3]
list_two = func_three(list_one) # call func_three and bind to the variable list_two
print(list_two) # prints [1,2,3,4] - the new object created by the function call
func_one()
operates on the original list, this means you don't need to return anything. You could choose to return the list here, but there is no point.
func_two()
adds the original list and the list [4]
. This creates a new list but we do not return it and therefore we lose the new list.
func_three()
adds the original list and the list [4]
. This creates a new list and we return it. This means we can use it outside of our function.
I would experiment with these in Python Tutor to make sure you understand what is going on.
3.3 To Return or Not To Return
Generally, if you are operating on a mutable object and you don't need to keep a copy of the original then there is no need to return anything.
If you need the original list intact then you need to copy the original list before you operate on it and return the modified copy. Review Section 1 on Copy and Deep Copy.
=== TASK ===
Copy and paste the following into a new Python file to get started.
def change_list(my_list):
pass
if __name__ == "__main__":
fruit_list = ["Pear", "Orange", "Peach", "Apple"]
print(fruit_list) # prints ["Pear", "Orange", "Peach", "Apple"]
new_fruit_list = change_list(fruit_list)
print(fruit_list) # prints ["Pear", "Orange", "Peach", "Apple"]
print(new_fruit_list) # prints ["Apple", "Banana", "Peach", "Pear"]
There is a function called change_list()
that you need to update.
The function should remove the second item, add "Banana" to the list and then sort the list.
The function should return the modified list without affecting the original list.
An example of how the function should work is given below.
fruit_list = ["Pear", "Orange", "Peach", "Apple"]
print(fruit_list) # prints ["Pear", "Orange", "Peach", "Apple"]
new_fruit_list = change_list(fruit_list)
print(fruit_list) # prints ["Pear", "Orange", "Peach", "Apple"]
print(new_fruit_list) # prints ["Apple", "Banana", "Peach", "Pear"]
Tuples
Tuples are very similar to lists but differ in a two key ways. Tuples are:
- Ordered
- Immutable (unchangeable)
By ordered we mean that the tuple has an order and it cannot be changed.
By immutable we mean that the tuple cannot be added to, removed from and items cannot be changed.
You define a tuple in much the same way as a list, but with round brackets, not square.
car_tuple = ("Audi", "Mercedes", "Ferrari")
1. Tuple Items
You can access tuple items using indexing, e.g.
print(car_tuple[1]) # prints the 2nd item of the tuple, "Mercedes"
You can also use slicing and negative indexing as per lists.
print(car_tuple[0:2]) # prints the 1st and 2nd items of the tuple, ("Audi", "Mercedes")
print(car_tuple[-1]) # prints the last item of the tuple, "Ferrari"
2. Looping Through a Tuple
You can loop through a tuple just like a list using a for
loop.
car_tuple = ("Audi", "Mercedes", "Ferrari")
for car in car_tuple:
print(car)
# prints..
# Audi
# Mercedes
# Ferrari
You can also use a while loop.
car_tuple = ("Audi", "Mercedes", "Ferrari")
i = 0
while i < len(car_tuple):
print(car_tuple[i])
i += 1
# prints..
# Audi
# Mercedes
# Ferrari
3. Unpacking a Tuple
It is often useful to unpack a tuple. This allows you to bind each item of a tuple to a variable.
car_tuple = ("Audi", "Mercedes", "Ferrari")
(x,y,z) = car_tuple # binds x = "Audi", y = "Mercedes" and z = "Ferrari"
3.1. Function Return Values
At first you might think what the point of this is. It comes in very handy in a few circumstances. One of the main ones is with function return values.
Remember that a function can only return one object, but it can return a tuple!
Suppose that we want a function to computer the sum, average and the length of a list.
def num_list_stats(num_list):
len_list = len(num_list)
sum_list = sum(num_list)
avg_list = sum_list/len_list
return (sum_list, avg_list, len_list)
# main code
test_list = [1,64,71,-3]
(test_sum, test_avg, test_len) = num_list_stats(test_list) # call function and bind return tuple
print(f"Sum of list is {test_sum}")
print(f"Average of list is {test_avg}")
print(f"Length of list is {test_len}")
In the code above num_list_stats
takes a list and then computes the sum, average and length of the list and returns these as a 3-tuple.
The calling code unpacks this and binds it to the variables test_sum
, test_avg
and test_len
.
Note that you can do it without the brackets, and it looks like it is returning 3 things, it isn't it is still just a returning a tuple
.
def num_list_stats(num_list):
len_list = len(num_list)
sum_list = sum(num_list)
avg_list = sum_list/len_list
return sum_list, avg_list, len_list
# main code
test_list = [1,64,71,-3]
test_sum, test_avg, test_len = num_list_stats(test_list) # call function and bind return tuple
If you aren't convinced then try this piece of code.
def tuple_test(x,y):
return x, y
print(tuple_test(1,2)) # prints (1,2)
print(type(tuple_test(1,2))) # prints <class 'tuple'>
3.2. Zipping Two Tuples
Zipping tuples together is a very useful thing to do when we want to combine tuples together and loop over them.
The zip operation is demonstrated by the following code snippet. Note both must be the same length.
tuple_one = (1,2,3)
tuple_two = ("a","b","c")
for x,y in zip(tuple_one, tuple_two):
print(x,y)
# prints
# 1 a
# 2 b
# 3 c
You can also zip a tuple and list.
tuple_one = (1,2,3)
list_one = ["a","b","c"]
for x,y in zip(tuple_one, list_one):
print(x,y)
# prints
# 1 a
# 2 b
# 3 c
4. Tuple Methods
Method | Description |
---|---|
count() | Returns the number of times a specified value occurs in a tuple |
index() | Searches the tuple for a specified value and returns the position of where it was found |
5. What if I Need to Change a Tuple?
You can do this by converting (casting) the tuple
to a list
using the list()
and tuple()
functions.
car_tuple = ("Audi", "Mercedes", "Ferrari")
temp = list(car_tuple) # create a tempory copy of the tuple as a list
temp[0] = "Ford" # Overwrite the 1st item
car_tuple = tuple(temp) # Reassign car_tuple as a new tuple using temp
print(car_tuple) # prints ("Ford", "Mercedes", "Ferrari")
=== TASK ===
Copy and paste the following into a new Python file to get started.
def tuple_search(t, search_index):
pass
if __name__ == "__main__":
# main code
spaghetti_soup = ("a", "b", "g", "c", "a", "z", "g", "a", "g", "y")
print(tuple_search(spaghetti_soup, "g")) # should print (3, 2)
There is a function called tuple_search()
that you need to update.
The function should count the number of times an item occurs in the tuple and the index (position) of the first of these occurences.
tuple_search(t, search_item)
For example,
spaghetti_soup = ("a", "b", "g", "c", "a", "z", "g", "a", "g", "y")
tuple_search(spaghetti_soup, "g")
Should return (3, 2)
References
Python Tuple Methods - W3schools
The High Scores 2.0 Program
This is an example program from:
Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.
We can extend the functionality of our previous High Scores Program by changing the data structure that we use.
Now instead of storing just the score in a list. We now store the person's name and the score. Do do this we will use tuples inside a list. This is an example of a nested sequence.
scores = [(person_1, score_1), (person_2, score_2), ...]
For example,
scores = [("Sam", 3), (Bradley, 9999), ...]
Clearly I lost!
The following program shows how to use this to extend the listing/adding scores functionality. Run the code to see how it works.
You might want to consider rewriting the whole program for practice.
# High Scores 2.0
# Demonstrates nested sequences
scores = []
choice = None
while choice != "0":
print(
"""
High Scores 2.0
0 - Quit
1 - List Scores
2 - Add a Score
"""
)
choice = input("Choice: \n")
# exit
if choice == "0":
print("Good-bye.")
# display high-score table
elif choice == "1":
print("High Scores\n")
print("NAME\tSCORE")
for entry in scores:
score, name = entry
print(name, "\t", score)
# add a score
elif choice == "2":
name = input("What is the player's name?: ")
score = int(input("What score did the player get?: "))
entry = (score, name)
scores.append(entry) # append entry
scores.sort(reverse=True) # sort the list. Highest first
scores = scores[:5] # keep only top 5 scores
# some unknown choice
else:
print("Sorry, but", choice, "isn't a valid choice.")
input("\n\nPress the enter key to exit.")
Sets
We will not spend long on sets as their use is somewhat limited, but they can come in very handy at times.
- Unordered and unindexed
- Items are immutable (unchangeable)
- Set is mutable (changable)
- Collection of distinct items
By unordered and unindexed we mean that the set does not have an order and therefore does not use indexing. You cannot access an item in a set by an index.
By items are immutable we mean that you cannot change an item in the set, this follows from it being unordered and unindexed. If you can't access an item, you definitely can't change it.
By set is mutable we mean that you can add and remove items from the set.
By collection of distinct items we mean that a set cannot contain duplicates.
You define a set as follows.
car_set = {"Audi", "Porsche", "Ford"}
Note that if you try and define a set with multiple items, it will only include one copy of each distinct item.
car_set = {"Audi", "Porsche", "Ford", "Audi"} # set is {"Audi", "Porsche", "Ford"}
1. Set Items
There are a few things we can do to a set involving items.
1.1 Is an Item in the Set?
The in
keyword checks whether an item is in a given set and returns True
or False
.
"Audi" in car_set # evaluates to True
"Skoda" in car_set # evaluates to False
Note that in
can also be used on lists and tuples.
car_list = ["Audi", "Porsche", "Ford", "Audi"]
car_tuple = ("Audi", "Porsche", "Ford", "Audi")
"Audi" in car_list # evaluates to True
"Audi" in car_tuple # evaluates to True
1.2 Add an Item
You can add an item to a set by using the add()
method.
car_set. add("Skoda") # set is {"Audi", "Porsche", "Ford", "Skoda"}
1.3 Remove an Item
You can remove an item in a set by using the remove()
method.
car_set. remove("Ford") # set is {"Audi", "Porsche", "Skoda"}
2. Looping Through a Set
Just like lists and tuples, you can loop through a set.
car_set = {"Audi", "Porsche", "Ford"}
for car in car_set:
print(car)
# prints...
# Audi
# Porsche
# Ford
3. Joining Sets
You can join sets using either the union()
or intersection()
methods.
For a mathematician, this is a very loose definition, but basically the union()
method will include all the items that are in both sets, but not any duplicates.
a = {1,2,3}
b = {1,4,5,6}
c = a.union(b) # c now contains {1,2,3,4,5,6}
The intersection includes only the items that appear in both sets.
a = {1,2,3}
b = {1,4,5,6}
c = a.intersection(b) # c now contains {1}
4. Set Methods
There are a number of set methods that you can use, we have seen two in add()
and remove()
. We will not look at these in detail in this course.
You can find a full list here Python Set Methods - W3schools.
I suggest you use these when you are studying sets in computational mathematics.
=== TASK ===
Copy and paste the following into a new Python file to get started.
def items_in_both(a,b):
pass
if __name__ == "__main__":
# main
# use this code to help check the function
a = {1,2,3}
b = {1,4,5,6}
print(items_in_both(a,b)) # prints {1}
Update the function items_in_both()
so that it returns the set that contains only the items that appear in both sets.
References
Dictionaries
Dictionaries are a very common in Python are used to store key:value pairs.
In other languages these might be called something else such as a HashMap.
Dictionaries are:
- Indexed by Key
- Mutable (changeable)
- Hetrogeneous
By indexed by key we mean that each value that we store in the dictionary has a unique key that allows you to access it.
By mutable we mean that the items in the dictionary can be changed and we can add/remove key:value pairs
By hetrogeneous we mean that we can mixed different data types for the key:value pairs.
Here is a quick example:
student_dict = {
1123425: "Ada Lovelace",
1425243: "Melba Roy Mouton",
1512414: "Alan Turing"
}
# stores student names by their id.
# Here key:value pair is id: name
Here is another example using different types as keys and values.
test_dict = {
1: "Hello World",
"Test": False,
"Another Key":[1,2,3,4]
}
The only issue here is that keys need to be a hashable type. For now that means don't use a list as a key.
A good analogy to remember dictionaries is well, a dictionary. In a dictionary you look up a word (key) and you can get the corresponding description (value)
1. Dictionary Items
Let's use the student_dict
as our example.
student_dict = {
1123425: "Ada Lovelace",
1425243: "Melba Roy Mouton",
1512414: "Alan Turing"
}
# stores student names by their id.
# Here key:value pair is id: name
1.1 Access Dictionary Items
We can access a dictionary item using the key name inside square brackets.
print(student_dict[1123425]) # prints "Ada Lovelace"
When accessing the item using the key name, it returns the corresponding value.
1.2 Change Dictionary Items
We can change a dictionary item using the key name inside square brackets.
student_dict[1123425] = "Guido van Rossum" # changes the value from "Ada Lovelace" to "Guido van Rossum"
print(student_dict)
# {1123425: 'Guido van Rossum', 1425243: 'Melba Roy Mouton', 1512414: 'Alan Turing'}
1.3 Add Dictionary Items
To add an item to a dictionary you again use the key name with square brackets
It is essentially the same as changing an item, but the key isn't yet in the dictionary.
student_dict[1234567] = "Margaret Hamilton" # adds the key:value pair to the dictionary
print(student_dict)
# {1123425: 'Guido van Rossum', 1425243: 'Melba Roy Mouton', 1512414: 'Alan Turing', 1234567: 'Margaret Hamilton'}
1.4 Remove Dictionary Items
You can remove a dictionary item by using the del
keyword.
del student_dict[1425243] # removes the 1425243: "Melba Roy Mouton"
print(student_dict)
# {1123425: 'Guido van Rossum', 1512414: 'Alan Turing', 1234567: 'Margaret Hamilton'}
2. Types of Dictionary Keys
Anything that is immutable can be a dictionary key. e.g., int
, str
, float
.
dictionary1 = {
1: 100,
2: 300,
42: 250
}
dictionary2 = {
"Abid": 100,
"Fatima": 300,
"Sam": 250
}
We can also use a tuple as a key.
dictionary3 = {
(1,10): 100,
(11,20): 300,
(21,30): 250
}
You can access an item using the tuple or the values of the tuple.
print(dictionary3[(1,10)]) # print 100
print(dictionary3[1,10]) # print 100
You can have mixed immutable types in your tuple, e.g.
dictionary4 = {
(1,'a'): 100,
(1,'b'): 300,
(1,'c'): 250
}
print(dictionary4[1,'a']) # print 100
print(dictionary4[1,'b']) # print 300
2.1 Lists Aren't Allowed (Mutable)
dictionary4 = {
[1,'a']: 100,
[1,'b']: 300,
[1,'c']: 250
}
This will cause an error!
Traceback (most recent call last):
File "main.py", line 1, in <module>
dictionary4 = {
TypeError: unhashable type: 'list'
The reason is that lists are mutable, hence can be changed. A key should never change otherwise how do you access it if you don't know the key. The error is saying we can't hash the type, meaning we can't create a unique identifier in memory because it might change.
Yes you could track it but this would be a bad idea. Just don't let it change!
3. Looping Through a Dictionary
You can loop through a dictionary in a number of ways.
If you want to loop through the keys you can do the following.
student_dict = {
1123425: "Ada Lovelace",
1425243: "Melba Roy Mouton",
1512414: "Alan Turing"
}
for key in student_dict.keys():
print(key)
# prints...
# 1123425
# 1425243
# 1512414
You can also use this to get the values:
for key in student_dict.keys():
print(f"Key = {key}, Value = {student_dict[key]}")
# prints...
# Key = 1123425, Value = Ada Lovelace
# Key = 1425243, Value = Melba Roy Mouton
# Key = 1512414, Value = Alan Turing
Another way, perhaps the most common is using the items()
method.
for k, v in student_dict.items():
print(f"Key = {k}, Value = {v}")
Here items
returns a list (actually a type called dict_items
) of (key
, value
) pairs that you can then bind to the variables k
and v
for each iteration of the loop.
3.1 Looping Through a Dictionary with Tuple Keys
We can also loop over a dictionary with tuple
keys.
dictionary4 = {
(1,'a'): 100,
(1,'b'): 300,
(1,'c'): 250
}
for key, value in dictionary4.items():
print(key)
print(value)
Or to access the first and second items of the key individually.
for key, value in dictionary4.items():
print(key[0])
print(key[1])
print(value)
4. Nested Dictionaries
Nested dictionaries are extremely useful.
For example we might want to store lots of information about each student. Let's store their email as well as their name.
student_dict = {
1123425: {
"Name": "Ada Lovelace",
"Email": "a.lovelace@unimail.derby.ac.uk"
},
1425243: {
"Name": "Melba Roy Mouton",
"Email": "m.mouton@unimail.derby.ac.uk"
},
1512414: {
"Name": "Alan Turing",
"Email": "a.turing@unimail.derby.ac.uk"
}
}
# stores student names by their id.
# Here key:value pair is id: {information dictionary}
Now if you want information about a student you can get the corresponding dictionary.
student1 = student_dict[1123425] # returns {"Name": "Ada Lovelace", "Email": "a.lovelace@unimail.derby.ac.uk" }
print(student1["Name"]) # prints 'Ada Lovelace'
print(student1["Email"]) # prints a.lovelace@unimail.derby.ac.uk
You can access the email in one line if you like.
print(student1[1123425]["Email"]) # prints a.lovelace@unimail.derby.ac.uk
5. Copying a Dictionary
Just like a list, copying a dictionary is stickier than you might imagine!
Dictionaries are also stored using references.
The variable stores the dictionary location in memory.
dict1 = {"a":1, "d":4}
dict2 = dict1 # copies the reference to dict1
print(f"Dictionary 1 = {dict1}")
print(f"Dictionary 2 = {dict2}")
dict2["c"] = 3
print(f"Dictionary 1 = {dict1}") # prints {'a':1, 'd':4, 'c':3}
print(f"Dictionary 2 = {dict2}") # prints {'a':1, 'd':4, 'c':3}
The above copies the reference to the list object. We can check this by using the id()
function.
id(dict1) # something like 140094119425920
id(dict2) # will be the same id as above 140094119425920
To copy a dictionary we must use the copy()
method.
dict1 = {"a":1, "d":4}
dict2 = dict1.copy() # copies the reference to dict1
print(f"Dictionary 1 = {dict1}") # prints {'a':1, 'd':4}
print(f"Dictionary 2 = {dict2}") # prints {'a':1, 'd':4}
dict2["c"] = 3
print(f"Dictionary 1 = {dict1}") # prints {'a':1, 'd':4}
print(f"Dictionary 2 = {dict2}") # prints {'a':1, 'd':4, 'c':3}
Now you will see that dict1
is not updated when you add to dict2
.
Again we can check using the id()
function if they point to the same location:
id(list1) # something like 140094119425920
id(list2) # will be a different id e.g. 140094117695872
6. Dictionary Methods
There are a number of built in methods you can use on a dictionary. You have already seen keys()
and items()
Method | Description |
---|---|
clear() | Removes all the elements from the dictionary |
copy() | Returns a copy of the dictionary |
fromkeys() | Returns a dictionary with the specified keys and value |
get() | Returns the value of the specified key |
items() | Returns a list containing a tuple for each key value pair |
keys() | Returns a list containing the dictionaries keys |
pop() | Removes the element with the specified key |
popitem() | Removes the last inserted key-value pair |
setdefault() | Returns the value of the specified key. If the key does not exist: insert the key, with the specified value |
update() | Updates the dictionary with the specified key-value pairs |
values() | Returns a list of all the values in the dictionary |
Python Dictionaris - W3schools
=== TASK ===
Copy the following into a new Python file to get started.
def add_from_list(d, items_to_add):
pass
if __name__ == "__main__":
test_dict = {
"a":1,
"b":2
}
print(test_dict) # prints {'a':1, 'b':2}
items_to_add = [("e", 5), ("z", 26)]
add_from_list(test_dict, items_to_add)
print(test_dict) # prints {'a':1, 'b':2, 'e':5, 'z':26}
The function add_from_list()
, should take in a dictionary and a list of tuples and then add these as key value pairs to the dictionary. It should not return anything.
For example,
test_dict = {
"a":1,
"b":2
}
print(test_dict) # prints {'a':1, 'b':2}
items_to_add = [("e", 5), ("z", 26)]
add_from_list(test_dict, items_to_add)
print(test_dict) # prints {'a':1, 'b':2, 'e':5, 'z':26}
HINT: You can loop through a list of paired values as follows. You might want to try this code out if you don't quite understand it.
for x,y in [(1,2), (5,1), (2,3)]:
print(x,y)
References
Python Dictionaries - W3schools
MP: The Geek Translator Program
This is an example program from:
Dawson, M. (2010). Python programming for the absolute beginner, third edition (3rd ed.). Delmar Cengage Learning.
The following is a mini-program that demonstrates the use of dictionaries to add, store, remove and access information.
It is a simple word translator program.
My main issue with this program is that it is hard to read. I would generally consider moving the functionality into functions to make it clearer what the main body of the program does. It would be worth rewriting this as an exercise.
Run the code to see what it does.
# Geek Translator
# Demonstrates using dictionaries
geek = {
"404": "clueless. From the web error message 404, meaning page not found.",
"Googling": "searching the Internet for background information on a person.",
"Keyboard Plaque": "the collection of debris found in computer keyboards.",
"Link Rot": "the process by which web page links become obsolete.",
"Percussive Maintenance": "the act of striking an electronic device to make it work.",
"Uninstalled": "being fired. Especially popular during the dot-bomb era.",
}
choice = None
while choice != "0":
print(
"""
Geek Translator
0 - Quit
1 - Look Up a Geek Term
2 - Add a Geek Term
3 - Redefine a Geek Term
4 - Delete a Geek Term
5 - List Known Terms
"""
)
choice = input("Choice: ")
print()
# exit
if choice == "0":
print("Good-bye.")
# get a definition
elif choice == "1":
term = input("What term do you want me to translate?: ")
if term in geek:
definition = geek[term]
print("\n", term, "means", definition)
else:
print("\nSorry, I don't know", term)
# add a term-definition pair
elif choice == "2":
term = input("What term do you want me to add?: ")
if term not in geek:
definition = input("\nWhat's the definition?: ")
geek[term] = definition
print("\n", term, "has been added.")
# redefine an existing term
elif choice == "3":
term = input("What term do you want me to redefine?: ")
if term in geek:
definition = input("What's the new definition?: ")
geek[term] = definition
print("\n", term, "has been redefined.")
else:
print("\nThat term doesn't exist! Try adding it.")
# delete a term-definition pair
elif choice == "4":
term = input("What term do you want me to delete?: ")
if term in geek:
del geek[term]
print("\nOkay, I deleted", term)
else:
print("\nI can't do that!", term, "doesn't exist in the dictionary.")
elif choice == "5":
for key, value in geek.items():
print(f"Term: {key}")
print(f"Definition: {value}\n")
else:
print("\nThat term already exists! Try redefining it.")
The Frequency Count Program
We would like to create a program that counts the frequency of numbers that have been entered by the user.
For example, the program would run as follows.
Please enter a whole number:
3
Please enter a whole number:
2
Please enter a whole number:
2
Please enter a whole number:
6
Please enter a whole number:
63
Please enter a whole number:
q
The numbers seen were [3, 2, 2, 6, 63]
The count for 2 is: 2
The count for 3 is: 1
The count for 6 is: 1
The count for 63 is: 1
1. The Simple Problem
Let's start with a simpler problem.
We will create a program that asks the user for numbers and displays the count seen of each numbers 1 - 5 and the count for everything else.
For example, the program would run as follows.
Please enter a whole number:
3
Please enter a whole number:
2
Please enter a whole number:
2
Please enter a whole number:
6
Please enter a whole number:
63
Please enter a whole number:
q
The numbers seen were [3, 2, 2, 6, 63]
The count for 1 is: 0
The count for 2 is: 2
The count for 3 is: 1
The count for 4 is: 0
The count for 5 is: 0
The count for everything else is: 2
1.1. Solving Using a List
To answer this problem we can use a list.
number_list = [] # define a list to store numbers seen
count_list = [0, 0, 0, 0, 0, 0] # defint a list to count the frequency of numbers
input_string = input("Please enter a whole number:\n")
while input_string != "q":
number_list.append(int(input_string)) # add number seen to list
# check if number between 1 and 5
if int(input_string) > 0 and int(input_string) < 6:
count_list[int(input_string) - 1] += 1 # increment number count by 1
else:
count_list[5] += 1 # increment everything else by 1
input_string = input("Please enter a whole number:\n")
# print out the count of the numbers 1 to 5
print(f"The numbers seen were {number_list}")
for i in range(5):
print(f"The count for {i+1} is: {count_list[i]}")
# print out the count of everything else
print(f"The count of everything else is: {count_list[5]}")
2. The Full Problem.
The issue with using a list to solve the full problem is that we don't know how many different numbers will be entered. In fact even worse, there are an infinite choice of numbers.
If you use a list you need to know how big it should be and therefore how many different types of numbers you expect to see.
This was fine when we said the numbers 1 - 5 and everything else. This is just counting 6 things. So we know the list should be of length 6.
2.1 Using a Dictionary
We can now use a dictionary to solve the full problem.
If you start with an empty dictionary, then you just need to add a new entry whenever you see a new number, then you can increment this by 1 if you see the number again.
number_list = []
count_dict = {}
input_string = input("Please enter a whole number:\n")
while input_string != "q":
number_list.append(int(input_string))
# check if the key exists in the dictionary
if int(input_string) in count_dict.keys():
count_dict[int(input_string)] += 1 # update entry
else:
count_dict[int(input_string)] = 1 # add new entry
input_string = input("Please enter a whole number:\n")
# print out the count of the numbers
print(f"The numbers seen were {number_list}")
for key, value in sorted(count_dict.items()):
print(f"The count for {key} is: {value}")
List/Dictionary Comprehension
List and dictionary comprehension is an incredibly powerful technique for writing compact code. It basically allows you to generate complicated lists or dictionaries in one line of code.
List and dictionary comprehension is something that takes time to get used to and I think the easiest way to understand it is to see it in action.
In this lesson you will see simple code snippets and then see the same code rewritten using list/dictionary comprehension.
You should note how compact this code looks, but how as the complexity builds up it can be harder to read.
I suggest that you copy the code into Python Tutor to make sure you understand it.
1. List Comprehension
We will start with the example given on W3schools.
# filter list for words with "a" in.
fruits = ["apple", "banana", "cherry", "kiwi", "mango"]
newlist = []
for x in fruits:
if "a" in x:
newlist.append(x)
print(newlist) # prints ['apple', 'banana', 'mango']
We can rewrite this using list comprehension.
# using list comprehension
# filter list for words with "a" in.
fruits = ["apple", "banana", "cherry", "kiwi", "mango"]
newlist = [x for x in fruits if "a" in x]
print(newlist) # prints ['apple', 'banana', 'mango']
Notice that list comprehension is in the following form.
[<expression> for <item> in <iterable> if <condition> == True]
The return value is a new list, leaving the old list unchanged.
1.1 Even Numbers
The following is a simple program that generates the even numbers between 1 and 100.
# generate even numbers between 1 and 100
even_num_list = []
for x in range(1,51):
even_num_list.append(2*x)
print(even_num_list) # prints [2,4,6, ...,98,100]
We can rewrite this using list comprehension.
# using list comprehension
# generate even numbers between 1 and 100
even_num_list = [2*x for x in range(1,51)]
print(even_num_list) # prints [2,4,6, ...,98,100]
Here we didn't use a condition so our list comprehension was in the form.
[<expression> for <item> in <iterable>]
An alternative program that generates the even numbers uses the %
modulus operator.
# generate even numbers between 1 and 100
even_num_list = []
for x in range(1,101):
if x % 2 == 0:
even_num_list.append(x)
print(even_num_list) # prints [2,4,6, ...,98,100]
We can rewrite this using list comprehension.
# using list comprehension
# generate even numbers between 1 and 100
even_num_list = [x for x in range(1,101) if x % 2 == 0]
print(even_num_list) # prints [2,4,6, ...,98,100]
Here we did use a condition so our list comprehension was in the form.
[<expression> for <item> in <iterable> if <condition> == True]
1.2 Length of Words
The following program takes a list of words and then calculates the length of each of the words.
# compute the length of each word in the list
words = ["Python", "is", "a", "cool", "programming", "language"]
len_words = []
for w in words:
len_words.append(len(w))
print(len_words) # prints [6, 2, 1, 4, 11, 8]
We can rewrite this using list comprehension.
# using list comprehension
# compute the length of each word in the list
words = ["Python", "is", "a", "cool", "programming", "language"]
len_words = [len(s) for s in words]
print(len_words) # prints [6, 2, 1, 4, 11, 8]
We can also take a sentence and use the split()
method to generate a list of words from a string and then do the above.
# compute the length of each word in the sentence
text = "Python is a cool programming language"
len_words = []
for w in text.split(" "):
len_words.append(len(w))
print(len_words) # prints [6, 2, 1, 4, 11, 8]
We can rewrite this using list comprehension.
# using list comprehension
# compute the length of each word in the sentence
text = "Python is a cool programming language"
len_words = [len(s) for s in text.split(" ")]
print(len_words) # prints [6, 2, 1, 4, 11, 8]
2. Nested List Comprehension
What about nested for
loops? List comprehension has you covered.
2.1 Length of Words in a List of Sentences
This is a program that takes a list of sentences and creates a list containing the length of each word in each sentence.
# compute the length of each word in a list of sentences
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
len_words = []
for sentence in sentence_list:
for word in sentence.split(" "):
len_words.append(len(word))
print(len_words) # prints [6, 2, 1, 4, 11, 8, 1, 4, 2, 7, 11, 2, 2, 4]
We can rewrite this using list comprehension.
# using list comprehension
# compute the length of each word in a list of sentences
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
len_words = [len(word) for sentence in sentence_list for word in sentence.split(" ")]
print(len_words) # prints [6, 2, 1, 4, 11, 8, 1, 4, 2, 7, 11, 2, 2, 4]
Which is in the form:
[<expression> for <inner_list> in <outer_list> for <item> in <inner_list>]
The output produced a single list with the length of the words.
What if we want the list to contain a separate list of word lengths for each of the sentences? e.g. [[6, 2, 1, 4, 11, 8], [1, 4, 2, 7], [11, 2, 2, 4]]
# compute the length of each word in a each of the sentences
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
len_words = []
for sentence in sentence_list:
temp = []
for word in sentence.split(" "):
temp.append(len(word))
len_words.append(temp)
print(len_words) # prints [[6, 2, 1, 4, 11, 8], [1, 4, 2, 7], [11, 2, 2, 4]]
We can rewrite this using list comprehension.
# using list comprehension
# compute the length of each word in each of the sentences
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
len_words = [[len(word) for word in sentence.split(" ")] for sentence in sentence_list]
print(len_words) # prints [[6, 2, 1, 4, 11, 8], [1, 4, 2, 7], [11, 2, 2, 4]]
Which is in the form:
[[<expression> for <item> in <inner_list>] for <inner_list> in <outer_list> if <condition> == True]
That is, produce a list from each of the inner lists of the outer list.
2.2 Nested with a Condition
What about adding a condition so that we only store the length of the words that are less than 4.
# compute the length of each word less than 4 in a list of sentences
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
less_than_4 = []
for sentence in sentence_list:
for word in sentence.split(" "):
if(len(word) < 4):
less_than_4.append(len(word))
print(less_than_4) # prints [2, 1, 1, 2, 2, 2]
We can rewrite this using list comprehension.
# using list comprehension
# compute the length of each word less than 4 in a list of sentences
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
less_than_4 = [len(word) for sentence in sentence_list for word in sentence.split(" ") if len(word) < 4]
print(less_than_4) # prints [2, 1, 1, 2, 2, 2]
3. Dictionary Comprehension
We can also do comprehension using dictionaries.
Consider the following example:
# compute the length of each word in the list and store it with the word
words = ["Python", "is", "a", "cool", "programming", "language"]
len_words = {}
for s in words:
len_words[s] = len(s)
print(len_words) # prints {'Python': 6, 'is': 2, 'a': 1, 'cool': 4, 'programming': 11, 'language': 8}
We can write this using dictionary comprehension as follows:
# compute the length of each word in the list and store it with the word
words = ["Python", "is", "a", "cool", "programming", "language"]
len_words = {s:len(s) for s in words}
print(len_words) # prints {'Python': 6, 'is': 2, 'a': 1, 'cool': 4, 'programming': 11, 'language': 8}
Here for each s
in the list words
we store the key:value pair s:len(s)
The general form is:
{<key>:<value> for <item> in <iterable> if <condition> == True}
The return
value is a new dictionary, leaving the list we used to generate the dictionary unchanged.
We could also change this to just store the words that are less than length 4.
# compute the length of each word in the list and store it with the word
words = ["Python", "is", "a", "cool", "programming", "language"]
less_than_four = {s:len(s) for s in words if len(s) < 4}
print(less_than_four) # prints {'is': 2, 'a': 1}
4. Nested Dictionary Comprehension
What about nested for loops? Dictionary comprehension also has you covered.
# compute the length of each word in the list of sentences and store it with the word
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
len_words = {}
for sentence in sentence_list:
for word in sentence.split(" "):
len_words[word] = len(word)
print(len_words)
# prints {'Python': 6, 'is': 2, 'a': 1, 'cool': 4, 'programming': 11, 'language': 8, 'I': 1, 'love': 4, 'to': 2, 'program': 7, 'Programming': 11, 'so': 2}
We can rewrite this using dictionary comprehension.
# using dictionary comprehension
# compute the length of each word in the list of sentences and store it with the word
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
len_words = {word:len(word) for sentence in sentence_list for word in sentence.split(" ")}
print(len_words)
# prints {'Python': 6, 'is': 2, 'a': 1, 'cool': 4, 'programming': 11, 'language': 8, 'I': 1, 'love': 4, 'to': 2, 'program': 7, 'Programming': 11, 'so': 2}
Which is in the form:
newlist = {<key>:<value> for <inner_list> in <outer_list> for <item> in <inner_list>}
We could also compute the length of each word in the sentences that is less than 4.
# compute the length of each word in the list of sentences that is less that 4 and store it with the word
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
less_than_4 = {}
for sentence in sentence_list:
for word in sentence.split(" "):
if len(word) < 4:
less_than_4[word] = len(word)
print(less_than_4) # prints {'is': 2, 'a': 1, 'I': 1, 'to': 2, 'so': 2}
We can rewrite this using dictionary comprehension.
# using dictionary comprehension
# compute the length of each word in the list of sentences that is less that 4 and store it with the word
sentence_list = ["Python is a cool programming language", "I love to program", "Programming is so cool"]
less_than_4 = {word:len(word) for sentence in sentence_list for word in sentence.split(" ") if len(word) < 4}
print(less_than_4) # prints {'is': 2, 'a': 1, 'I': 1, 'to': 2, 'so': 2}
Which is in the form:
newlist = {<key>:<value> for <inner_list> in <outer_list> for <item> if <condition> == True}
5. Comprehension on Dictionaries
Note that you can do comprehension using any iterable such as a range
, list
or dict
.
5.1 Range
We already saw this at the start with the even numbers.
# using list comprehension
# generate even numbers between 1 and 100
even_no_list = [x for x in range(1,101) if x % 2 == 0]
print(even_no_list)
5.2 Dictionary
We could get the list of student names from a dictionary of students.
student_dict = {
1123425: "Ada Lovelace",
1425243: "Katherine Johnson",
1512414: "Alan Turing"
}
student_names = [value for key, value in student_dict.items()]
print(student_names) # prints ['Ada Lovelace', 'Katherine Johnson', 'Alan Turing']
Note we could just get this using student_dict.values()
.
The following takes a dictionary of students and adds their email address to their data.
# automatically generate student emails and store them in a nested dictionary.
def email_from_name(name):
[first_name, surname] = name.split(" ")
return f"{first_name[0]}.{surname}.unimail.derby.ac.uk"
student_dict = {
1123425: "Ada Lovelace",
1425243: "Katherine Johnson",
1512414: "Alan Turing"
}
student_info_dict = {key:{"Name":value, "Email":email_from_name(value)} for key, value in student_dict.items()}
print(student_info_dict)
# prints {1123425: {'Name': 'Ada Lovelace', 'Email': 'A.Lovelace.unimail.derby.ac.uk'}, 1425243: {'Name': 'Katherine Johnson', 'Email': 'K.Johnson.unimail.derby.ac.uk'}, 1512414: {'Name': 'Alan Turing', 'Email': 'A.Turing.unimail.derby.ac.uk'}}
=== TASK ===
Copy and paste the following into a new Python file to get started.
def sum_div_3(n):
"""you should update div_by_3_list to generate the list of numbers divisible by 3 and return their sum.
e.g. for n = 10 it should look at 1,2,3,4,5,6,7,8,9,10 and the list should contain [3,6,9]. The function should then return the sum of this list, in this case 18"""
div_by_3_list = []
return sum(div_by_3_list)
if __name__ == "__main__":
print(sum_div_3(10)) # should print 18
print(sum_div_3(12)) # should print 30
print(sum_div_3(100)) # should print 1683
Use list comprehension to find the sum of all numbers that are divisable by 3
between 1
and an upper limit n
. If you are struggling, write this in a more "traditional" format first and then convert it to list comprehension.
You should update the function sum_div_3(n)
so that it returns the correct sum.
For example, sum_div_3(12)
should sum up 3,6,9
and 12
which totals to 30
.
Unit 7 - Modules and Files
Modules are essentially a collection of functions that you can then include in your project.
They are a means of letting you organise functions into useful groups and better structure your programs.
To date, our programs have been small enough that we have cared little for this. As our programs grow in size, structuring them into seperate files becomes essential.
No one wants to read single file of 10000 lines of code!
We will also take a look at how to read and write to files to allow our programs to load and save simple data.
In this unit we will look at:
- Module Basics
- Creating Modules
- Reading to Files
- Writing to Files
- Serialisation with JSON
References
Module Basics
Modules are files that can be imported into your project so you can use pre-built functionality.
Python comes with a number of built-in modules and you can also install python libraries which contain modules that other programmers have installed, but are not part of standard Python.
We will use three modules to demonstrate the basics. math
,
datetime
and random
.
1. Importing a Module
You ALWAYS import modules at the top of your python files!
# import the built-in maths module
import math
print(math.pi)
print(math.sqrt(2.78))
If you want to see what is in math
you can type help(math)
into the terminal or use Google!
import datetime
# Uses the now method in the datetime class (more on this later)
print(datetime.datetime.now())
import random
print(random.randint(3, 9)) # rand number between 3 and 9
mylist = ["apple", "banana", "cherry"]
print(random.choice(mylist)) # randomly picks from mylist
The main point here is you haven't had to do much to get some serious functionality!
Generally, unless you feel the need, don't recode something that already exists!
1.1 Importing Part of A Module
We could have chosen to just import the randint()
function,
# only import a given function
from random import randint
print(randint(3, 9)) # rand number between 3 and 9
or multiple functions.
# only import a given function
from random import randint, choice
print(randint(3, 9)) # rand number between 3 and 9
mylist = ["apple", "banana", "cherry"]
print(choice(mylist)) # randomly picks from mylist
I would generally avoid doing this! It is harder to tell when a function has come from an external module.
1.2 Renaming a Module
Sometimes it is useful to give a module an alias (shorten a module name). Again this is something that is done for some modules, if in doubt on how to import a module, just use Google!
# I wouldn't do this!
import datetime as dt
print(dt.datetime.now())
Ultimately getting used to using modules takes time and experience, but you can always look up how to do these things.
Normally if I want to do something in Python, I google it and look at some code, there is normally a module either pre-built or as part of an external library that will help.
2. Useful Built-in Modules
You can find a complete list of built-in modules via the Python Docs, or a slightly friendlier list of the most common ones via W3Schools.
Again, Google is your friend. If you can use a built-in module instead of a 3rd party one then you should
3. Libraries
Libraries in Python are just a collection of related modules.
The built-in modules are part of the Python standard library.
You can install other libraries and use their modules, we have already seen the use of a 3rd party library (a very common one) called Matplotlib. And we used the pyplot module.
# import LIBRARY.MODULE as NAME
import matplotlib.pyplot as plt
Here we import the pyplot module from the matplotlib library and give it an alias (name).
We also import the numpy module as np
.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(1,100,0.1)
y = np.sin(x)
plt.plot(x,y)
plt.show()
If you run this code it will give you a plot of the sine function.
=== TASK ===
Import the os
module
Amend the function get_current_working_directory()
so that it returns the current working directory as a string.
HINT: Search for "current working directory" on the following URL - Python Docs - os Module
Implement the functions encode_string()
and decode_string()
using the base64
module so that they encode and decode a string using base64.
You can find out more via the following URLs. You will be required to read at least one of these links to implement the functions.
Encoding and Decoding Base64 Strings in Python - G4G
Copy and paste this into a new Python file to get started.
def get_current_working_directory():
pass
def encode_string(str_to_encode):
""" Encodes a string using base64"""
pass
def decode_string(base64_str_to_decode):
""" Decodes a string encoded in base64"""
pass
if __name__ == "__main__":
print(get_current_working_directory())
print(encode_string("Python is a cool programming language!"))
# should print UHl0aG9uIGlzIGEgY29vbCBwcm9ncmFtbWluZyBsYW5ndWFnZSE=
str_to_decode = "UHl0aG9uIGlzIGEgY29vbCBwcm9ncmFtbWluZyBsYW5ndWFnZSE="
print(decode_string(str_to_decode))
# should print Python is a cool programming language!
References
1. Python Docs - Built-in Modules
2. W3schools - Python Built-in Modules
3. Matplotlib - Plotting Library
Creating a Module
To create your own module you just need to create a .py file that includes your code. Generally, this will include functions.
Later we will look at including classes.
Modules can include both functions and variables.
1. Creating a Module
Create a file called calculator1.py
.
Includes two functions and a variable:
# calculator1.py
# terrible approximation of pi
pi = 3.14
def add(x,y):
return x + y
def mul(x,y):
return x * y
Create another file in the same directory called main.py
. We can then use calculator.py
in our main.py
via an import.
# main.py
import calculator1 as calc
print(calc.add(3,4)) # use the add function from calc
print(calc.mul(3,4)) # use the mul function from calc
print(calc.pi) # use the pi variable
For now, it's enough that the module is in the same folder as your main.py
. Later we will discuss ways to organise your project.
2. if name == "__main__"
We have used this during this module already, but now it is time to explain why.
Now create two new files called calculator2.py
and calculator3.py
.
calculator2.py
should contain the following code.
# calculator2.py
# terrible approximation of pi
pi = 3.14
def add(x,y):
return x + y
def mul(x,y):
return x * y
print(f"These are lines printed inside {__name__}")
print("For example we might like to test the add function")
print(add(3, 4))
calculator3.py
should contain the following code.
# calculator3.py
# terrible approximation of pi
pi = 3.14
def add(x,y):
return x + y
def mul(x,y):
return x * y
if __name__ == "__main__":
print(f"These are lines printed inside {__name__}")
print("For example we might like to test the add function")
print(add(3, 4))
The difference is that calculator3.py
has this code contained within the if __name__ == "__main__"
block.
__name__
is a special reserved name in Python which is used to keep track of the names of the files that are being run and imported.
The file that you run, normally main.py
but it can be any file, takes on the name __main__
.
Try running all of these files and look at the differences in the output.
Note if you want to run them manually from the terminal you can do by opening the terminal and typing ls
. This will list the files in the current directory.
Type python main.py
to run the main file.
Type python calculator2.py
to run the calculator2.py
file.
Type python calculator2.py
to run the calculator3.py
file.
You will not that __name__
for both calculator2.py
and calculator3.py
is __main__
. This is because it is the starting (main) file being run by Python.
OK, back to if __name__ == "__main__"
.
What this does is tell python only to run that block of code if you are running calculator3.py
.
Why do this?
Well, try importing calculator2.py
and using it.
import calculator2 as calc
print(calc.add(3,4)) # use the add function from calc
print(calc.mul(3,4)) # use the mul function from calc
print(calc.pi) # use the pi variable
This will print out the following:
These are lines printed inside calculator2.py
For example, we might like to test the add function
7
7
12
3.14
Oh, dear! It printed out the code that is contained at the bottom of the file as well, really we just wanted to use the functions.
Now try it with calculator3.py
:
import calculator3 as calc
print(calc.add(3,4)) # use the add function from calc
print(calc.mul(3,4)) # use the mul function from calc
print(calc.pi) # use the pi variable
This will print out the following:
7
12
3.14
Much better! Now it just imported the functions for us to use.
Try running calculator3.py
again.
This will run the file and print out the following.
These are lines printed inside __main__
For example, we might like to test the add function
7
Notice now that we are running the file itself, so the if __name__ == "__main__"
block runs!
=== TASK ===
Create a new file called hello.py
Your module should include two functions called hello()
and goodbye()
which take in a name and returns a string.
For example, hello()
should operate as follows.
hello("Joe") # returns "Hello Joe"
print(hello("Joe")) # prints Hello Joe
And, goodbye()
should operate as follows.
goodbye("Joe") # returns "Goodbye Joe"
print(goodbye("Joe")) # prints Goodbye Joe
I have included this code so that you can use in main.py
to test your module.
# main.py
import hello
def main():
print(hello.hello("Joe")) # prints Hello Joe
print(hello.goodbye("Joe")) # prints Goodbye Joe
if __name__ == "__main__":
main()
If you have done this correctly, the output of your program should be:
Hello Joe
Goodbye Joe
The Calculator Program
This is a simple demonstration of a program that acts like a calculator.
The main point of this program is to demonstrate that all the functionality of the calculator that does that hard work is stored in the module calc.py
.
We then import calc.py
using the import statement:
import calc
This allows the main program to then use the functions that are defined (written) in main.py
The tests should just pass if you run them.
Main File
# main.py
# importing module calc (calc.py)
import calc
def main():
choice = None
print("Welcome to the Calculator...")
print(f"Did you know the value of Pi is approximately {calc.pi}")
while choice != "0":
print("""
What would you like to do?
1. Add
2. Subtract
3. Multiply
4. Divide
5. Power
6. Nth Root
""")
choice = input("")
if choice in ["1", "2", "3", "4", "5"]:
x = float(input("Please enter the first number:\n"))
y = float(input("Please enter the second number:\n"))
if choice == "1":
ans = calc.add(x,y)
elif choice == "2":
ans = calc.subtract(x,y)
elif choice == "3":
ans = calc.multiply(x,y)
elif choice == "4":
ans = calc.divide(x,y)
elif choice == "5":
ans = calc.power(x,y)
if ans:
print(f"Answer: {ans}")
else:
print("Division by 0 is illegal")
elif choice == "6":
x = float(input("Please enter the first number:\n"))
n = int(input("Please enter a whole value for n (nth root):\n"))
ans = calc.root(x,n)
print(f"Answer: {ans}")
else:
print("Invalid Option")
if __name__ == "__main__":
main()
Calc Module
# calc.py
pi = 3.14159
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x,y):
return x*y
def divide(x,y):
if not y == 0:
return x/y
else:
return None
def power(x,y):
return x**y
def root(x,y, epsilon=0.001):
if x < 0 and y % 2 == 0:
return None
low = min(-1.0, x)
high = max(1.0, x)
ans = (high + low) / 2.0
# check if the answer is close enough
while abs(ans**y - x) >= epsilon:
if ans**y < x:
low = ans
else:
high = ans
ans = (high + low) / 2.0
return ans
Reading Files
Reading and writing to files allows us to start using persistent storage to remember data for our programs.
In this lesson, we will look at reading from a simple text file (.txt).
1. Opening a file
We can open a file using the following.
open(path_to_file, mode)
Mode | Description |
---|---|
'r' | Open text file for reading text |
'w' | Open text file for writing text |
'a' | Open text file for appending text |
e.g.
# open file in write mode
f = open("example.txt", "r")
Here f
is now a file object that represents the file. We can now use this to read the contents of the file, write to the file or append to the contents of the file.
1.1 Relative and Absolute Paths
In the above example we are using a relative path and Python will open the file based on what is known as the current working directory. This is normally the directory that the Python file (.py) is run from.
It can change though. To check you can run the following (try it out).
import os
print(os.getcwd()) # This uses the os module and returns the current working directory.
We could have given an absolute path e.g. C:\Users\Bob\example.txt
if we were running our code on a windows machine and we wanted to read the file from a different directory. If you are using an absolute path on windows, because it uses a \
(backslash) in its paths, you should but an r
in front of it. This will tell Python not to treat the \
as an escape character, i.e. treat it as a raw string.
# open file in write mode
f = open(r"C:\Users\Bob\example.txt", "r")
Alternatively you can escape the escape characters.
# open file in write mode
f = open("C:\\Users\\Bob\\example.txt", "r")
2. Reading a file
Reading Methods
Here are some select methods for reading.
Method | Description |
---|---|
read() | Returns the file content. |
read(size) | Read the specified number of bytes. |
readline() | Returns one line from the file. If you call this multiple times it will return the next line and internally keep track of which line is next. |
readlines() | Returns a list of the lines in the file. |
close() | Closes the file. This is extremely important for freeing up resources. Always close your file. |
You can see other file methods via this link - Python File Methods
Read the whole contents
We can read the whole contents of the file using the read()
method.
file = open("example.txt", "r")
lines = file.read()
file.close()
print(lines)
You will note the close()
method here. This closes the file. You should always close your file when done. Here are some reasons.
-
It can mean that you have too many things open, and thus more used space in the RAM, this may slow down your program.
-
Changes are not written to the file until it is closed. If you don't explicitly close then you may not see the edits in another part of your program.
-
If you have too many files open, you could run into limits which will throw an error.
-
You become reliant on the garbage collector - though the file in theory will be automatically closed when it is closed is now not up to you.
Using the context manager with
We can make sure the file is closed by using the with
keyword.
I would strongly advise this.
# this loads all of the lines in the file into a list
with open('example.txt', "r") as file:
# read all the lines and put in a list of containing each line
lines = file.readlines()
# print the list of lines
print(lines)
Read the file line by line
We can also read the file line by line. The following code reads the whole content of the file and puts each line into a list.
We can then iterate over the list of lines.
# this loads all of the lines in the file into a list
with open('example.txt', "r") as file:
# read all the lines and put in a list containing each line
lines = file.readlines()
# for each of the lines in the list
for l in lines:
# print the line
print(l)
# print(l.strip()) # if you want to stop python printing a new line after each line
This is fine when we have a small file, but if we have a large volume of data, we may not wish to load the entire file into memory.
Don't load the whole file into memory
One way to combat this is to read the file line by line and do something with each line. Now we are not loading the whole file into memory, just a line at a time.
# this reads the file line by line, without reading it all into memory at once
with open('example.txt', "r") as file:
# read the first line as a string
line = file.readline()
# while line is not empty
while line:
# print line
print(line)
# read the next line
line = file.readline()
FileNotFoundError
Opening files is a potential source of errors in your program. If the file does not exist and your try to read from it, it will throw an error.
# this loads all of the lines in the file into a list
with open('example1.txt', "r") as file:
# read all the lines and put in a list containing each line
lines = file.readlines()
As example1.txt
does not exist, this will throw a FileNotFoundError
.
We can deal with this error by using a try..except
try:
with open("example1.txt", "r") as file:
lines = file.read()
print(lines)
except FileNotFoundError:
print("File not found")
=== TASK ===
Amend the function number_of_lines()
so that it reads the contents of the file stored at filename
and counts the number of lines in the file. It should then return this number.
Copy and paste the following into a new Python file to get started.
def number_of_lines(filename):
""" Write your code here """
pass
def main():
print(number_of_lines("example.txt")) # prints 3 as example.txt contains 3 lines
print(number_of_lines("test.txt")) # prints 25 as test.txt contains 25 lines
if __name__ == "__main__":
main()
Writing to Files
1. Writing To Files
We can write to files in two ways. Either by writing a string or by writing a list of lines.
WARNING - Writing will overwrite the entire contents of the file.
We can write a string to the contents of a file using the write
method.
# write to the file example.txt
with open("example.txt", "w") as file:
file.write("Write some content to the file")
The contents of the file will now be:
Write some content to the file
We can also write a list of strings to the file using the writelines()
method.
string_list = ["This is a line", "This is another line"]
# write to the file example.txt
with open("example.txt", "w") as file:
file.writelines(string_list)
The contents of the file will now be:
This is the line
This is another line
2. Appending To Files
Appending will append to the end of the file.
We can append it to a file as follows:
# append some content to the file example.txt
with open("example.txt", "a") as file:
file.write("\nThis is some extra content to append\n")
For example, if the contents of the file are,
This is the line
This is another line
then after the append, the contents will be:
This is the line
This is another line
This is some extra content to append
Be careful
The following code snippet does the following:
- Writes to the file and prints it out
- Appends to the file and prints it out
- Writes to the file and prints it out (this now overwrites the content produced by 1 and 2)
You should try this code and make sure you understand what it is doing.
import os
# this is a relative path
filepath = "example.txt"
# if you do not use the r for the absolute path on windows, the \ are interpreted as an escape character.
# filepath = r"C:\Users\Bob\example.txt"
print("The current working directory (the default path for reading/writing/appending) is:\n")
print(os.getcwd())
print()
print("\n-----Writing some content-----\n")
# write to the file example.txt
with open(filepath, "w") as file:
file.write("Write some content to the file")
# read the contents of example.txt
with open(filepath) as file:
lines = file.read()
print("-----Reading content after first write-----\n")
print(lines)
print("\n-----Appending some content-----\n")
# append some content to the file example.txt
with open(filepath, "a") as file:
file.write("\nThis is some extra content to append\n")
# read the contents of example.txt
with open(filepath) as file:
lines = file.read()
print("\n-----Reading content after append-----\n")
print(lines)
print("\n-----Writing some content-----\n")
# write some content to the file example.txt.
# This will now overwrite the previous content
with open(filepath, "w") as file:
file.write("Oh dear we have overwritten all the content in example.txt")
# read the contents of example.txt
with open(filepath) as file:
lines = file.read()
print("-----Reading content after second write-----")
print(lines)
=== TASK ===
Write a function called write_nums()
that outputs the first positive n numbers to a txt file called nums.txt
For example,
write_nums(10)
Should output the first 10 numbers to the file nums.txt
1
2
3
4
5
6
7
8
9
10
def write_nums(n):
pass
if __name__ == "__main__":
write_nums(10)
# writes out the first 10 positive numbers to nums.txt
Serialisation with JSON
OK, so imagine we have a simple student records system that uses a dictionary for storing the data. What if we would like to save this so that at a later date we can load it?
For example, we might have the following data in a dictionary.
std_dict = {
"100456789": ["Sam O'Neill", "Computer Science"],
"100123456": ["Roisin Hunt", "Computer Games `Modelling and Animation"],
"100654321": ["Tom Hughes-Roberts", "Computer Games Programming"],
"100987654": ["Bradley Davis", "Computer Science"],
"100345678": ["Rich Conniss", "Mathematics"],
"100876543": ["Patrick Merritt", "Computer Games Programming"]
}
1. Saving and Reading a Dictionary with Standard Write
We could try and do this by saving the dictionary to a txt file, for example:
std_dict = {
"100456789": ["Sam O'Neill", "Computer Science"],
"100123456": ["Roisin Hunt", "Computer Games `Modelling and Animation"],
"100654321": ["Tom Hughes-Roberts", "Computer Games Programming"],
"100987654": ["Bradley Davis", "Computer Science"],
"100345678": ["Rich Conniss", "Mathematics"],
"100876543": ["Patrick Merritt", "Computer Games Programming"]
}
# save the dictionary to the file std.txt
with open('std.txt', 'w') as file:
for (k,v) in std_dict.items():
file.write(f"{k}:{v}\n")
This will save the following in std.txt
100456789:["Sam O'Neill", 'Computer Science']
100123456:['Roisin Hunt', 'Computer Games `Modelling and Animation']
100654321:['Tom Hughes-Roberts', 'Computer Games Programming']
100987654:['Bradley Davis', 'Computer Science']
100345678:['Rich Conniss', 'Mathematics']
100876543:['Patrick Merritt', 'Computer Games Programming']
We could then attempt to load this back in using something like:
# try and load the dictionary back into memory from std.txt
load_dict = {}
with open('std.txt', 'r') as file:
for line in file.readlines():
item = line.split(":")
load_dict[item[0]] = item[1]
print(load_dict)
This prints out:
{'100456789': '["Sam O\'Neill", \'Computer Science\']\n', '100123456': "['Roisin Hunt', 'Computer Games `Modelling and Animation']\n", '100654321': "['Tom Hughes-Roberts', 'Computer Games Programming']\n", '100987654': "['Bradley Davis', 'Computer Science']\n", '100345678': "['Rich Conniss', 'Mathematics']\n", '100876543': "['Patrick Merritt', 'Computer Games Programming']\n"}
This is not quite what we had in mind, in fact, we would need to make some alterations to the code to get it to be the original dictionary.
But it gets worse, what if we have a different data structure, maybe we have a dictionary of dictionaries etc? In this case, we would now need to modify the code again!
This leads us to use serialisation which allows us to convert a python object into something we can then store and then at a later date just reverse the process.
We can serialise using many formats, JSON, pickle, XML etc. We will just look at JSON. In general, this is used for transmitting data in web applications.
If you were building a real student record system you would store the data in a database.
2. Saving and Reading a Dictionary with JSON Serialisation
Here is how we can do this in JSON though.
We can first save the dictionary as JSON in the file std_dict.json
, we will also indent the file to make it more readable.
import json
std_dict = {
"100456789": ["Sam O'Neill", "Computer Science"],
"100123456": ["Roisin Hunt", "Computer Games `Modelling and Animation"],
"100654321": ["Tom Hughes-Roberts", "Computer Games Programming"],
"100987654": ["Bradley Davis", "Computer Science"],
"100345678": ["Rich Conniss", "Mathematics"],
"100876543": ["Patrick Merritt", "Computer Games Programming"]
}
with open('std_dict.json', 'w') as f:
json.dump(std_dict, f, indent=4)
This will output the following to std_dict.json
{
"100456789": [
"Sam O'Neill",
"Computer Science"
],
"100123456": [
"Roisin Hunt",
"Computer Games `Modelling and Animation"
],
"100654321": [
"Tom Hughes-Roberts",
"Computer Games Programming"
],
"100987654": [
"Bradley Davis",
"Computer Science"
],
"100345678": [
"Rich Conniss",
"Mathematics"
],
"100876543": [
"Patrick Merritt",
"Computer Games Programming"
]
}
We can then load it back into memory.
import json
with open('std_dict.json', 'r') as f:
new_dict = json.load(f)
print(new_dict)
Which prints out:
{'100456789': ["Sam O'Neill", 'Computer Science'], '100123456': ['Roisin Hunt', 'Computer Games `Modelling and Animation'], '100654321': ['Tom Hughes-Roberts', 'Computer Games Programming'], '100987654': ['Bradley Davis', 'Computer Science'], '100345678': ['Rich Conniss', 'Mathematics'], '100876543': ['Patrick Merritt', 'Computer Games Programming']}
Much better!
=== TASK ===
This should be a nice easy one.
Create a load()
function that takes in a filename and loads the data stored in the filename.
Copy and paste the following into a new Python file to get started.
def load(filename):
pass
if __name__ == "__main__":
print(load("items.json"))
items.json
data from W3Schools
Unit 8 - Decomposition, Abstraction, Recursion and Lambda (Advanced)
This unit is about advanced function concepts. I would like you to have at least understood:
- Decomposition and Abstraction
- *args and **kwargs
The rest are more difficult and may take some time to understand. Please do not worry if these don't make sense at first.
In this unit we will cover:
- Decomposition and Abstraction
- *args and **kwargs
- Decorators
- Recursion 101
- Memoization (Advanced)
- Lambda Functions, Map and Filter
Decomposition and Abstraction
As well as this lesson, I also highly recommend you read the following - https://cs.stanford.edu/people/nick/py/python-style-decomposition.html
The following definitions are given in [1] (see References).
Decompostion creates struture. It allows us to break a problem into parts that are reasonably self-contained and that may be reused in different settings.
An analogy for this is a car. There are a number of different parts to a car that can be maintained separately. If the car breaks, we probably only have to fix one small part. The car is decomposed into many smaller parts.
Abstraction hides detail. It allows us to use a piece of code as if it were a black-box - that is, something whose interior details we cannot see, don't need to see and shouldn't even want to see.
An analogy for this is also a car, this time though we don't care how the car works. We just need to know how to drive it, e.g. use a steering wheel, clutch, accelerator and brake.
1. Decomposition with Functions
We will see other ways to decompose a program with modules and classes, but for now we will consider functions.
Take the following program.
# program to format a list of strings
name_list = ["sam", "JOE", "Emily", "richard"]
new_name_list = []
# first change the name to lower case, then change the first character to upper
for name in name_list:
lower_name = name.lower()
formatted_name = f"{lower_name[0].upper()}{lower_name[1:]}"
new_name_list.append(formatted_name)
# print names
for name, new_name in zip(name_list, new_name_list):
print(f"The formatted name for {name} is {new_name}.")
# prints...
# The formatted name for sam is Sam.
# The formatted name for JOE is Joe.
# The formatted name for Emily is Emily.
# The formatted name for richard is Richard.
This is actually a series of jobs that get done.
- convert the list of strings to a list of lower case strings
- convert the list of lower case strings to a list with the inital capitalised
- print the formatted strings
We can write this as a decomposed program using functions.
def string_list_to_lower(items):
lower_items = []
for s in items:
lower_items.append(s.lower())
return lower_items
def string_list_capitalise_initial(items):
lower_items = []
for s in items:
lower_items.append(f"{s[0].upper()}{s[1:]}")
return lower_items
def print_formatted(items, formatted_items):
for s, format_s in zip(items, formatted_items):
print(f"The formatted name for {s} is {format_s}.")
# main program
name_list = ["sam", "JOE", "Emily", "richard"]
# convert the list of strings to a list of lower case strings
lower_name_list = string_list_to_lower(name_list)
# convert the list of lower case strings to a list with the inital capitalised
formatted_list = string_list_capitalise_initial(lower_name_list)
# print the formatted strings
print_formatted(name_list, formatted_list)
Now each of the functions is a smaller self-contained part, that could be reused in a different setting.
Note that this is actually a larger program, decomposition doesn't necessarily mean that the program is smaller.
It does though help with maintainability and seperation of concerns.
Here is a version where the formatting is done in one function, but the function now does 2 things.
def format_list(items):
formatted_items = []
for s in items:
formatted_items.append(f"{s[0].upper()}{s[1:].lower()}")
return formatted_items
def print_formatted(items, formatted_items):
for s, format_s in zip(items, formatted_items):
print(f"The formatted name for {s} is {format_s}.")
# main program
name_list = ["sam", "JOE", "Emily", "richard"]
# format_strings
formatted_list = format_list(name_list)
# print the formatted strings
print_formatted(name_list, formatted_list)
And just for fun, here it is using list comprehension...
def format_list(items):
return [f"{s[0].upper()}{s[1:].lower()}" for s in items]
def print_formatted(items, formatted_items):
for s, format_s in zip(items, formatted_items):
print(f"The formatted name for {s} is {format_s}.")
# main program
name_list = ["sam", "JOE", "Emily", "richard"]
# format_strings
formatted_list = format_list(name_list)
# print the formatted strings
print_formatted(name_list, formatted_list)
If you inspect all the main programs that use functions, you will see they are about 3-4 lines long and that it is easy to see which bit does what.
Easier to test and easier to debug!
2. Abstraction with Functions
Again, we will see other ways to abstract a program with modules and classes, but for now we will consider functions.
The essence of abstraction is that you only need know what a function does, but not how it works.
Take for example the following function.
def string_list_capitalise_initial(items):
formatted_items = []
for s in items:
formatted_items.append(f"{s[0].upper()}{s[1:]}")
return formatted_items
I don't really care how it works, I care that I understand what it does and how to use it. Here it is written with a docstring.
def string_list_capitalise_initial(items):
""" Takes a list of strings and Capitalises the first character.
e.g. ["test", "case"] -> ["Test" "Case"]
["heLLo", "WorLD"] -> ["HeLLo", "WorLD"]
Parameters
----------
items: list
list of strings.
Returns
-------
list:
list of transformed strings.
"""
formatted_items = []
for s in items:
formatted_items.append(f"{s[0].upper()}{s[1:]}")
return formatted_items
In many cases this sort of docstring might be left out depending on the programmer; however it is clear what it does and how to use it. It is also important that the function name and parameters makes sense!
The real point it I can use this function without looking at it's internal workings, it is a black-box!
Ideally a function should do one thing only. This makes them easier to test and reuse. It’s hard to define a length restriction but going over 15 lines should make you pause and reflect.
This is an example of something called the single-responsibility principle (SRP).
=== TASK ===
Rewrite the code below so that it uses the functions specified in the remainder of the TASK.
# main program
# list of strings
str_list = ["ABBA", "snake", "Peep", "this", "kayak"]
pal_list = []
for s in str_list:
lower_s = s.lower()
if len(lower_s) > 1:
is_pal = True
start_index = 0
end_index = len(lower_s) - 1
while start_index <= end_index and is_pal:
if not lower_s[start_index] == lower_s[end_index]:
is_pal = False
start_index += 1
end_index -= 1
if is_pal:
pal_list.append(s)
print(pal_list) # prints ['ABBA', 'Peep', 'kayak']
You will note that there are specifications written for each of the functions.
When decomposed properly, your main program should just be 3 lines long.
Copy the following into a new Python file to get started. You can always delete the docstrings to make it easier to read your code.
def is_palindrome(s):
""" Tests if a string is a palindrome, ignore upper/lower case
e.g. ABBA returns True
AbBa returns True
snake returns False
Parameters
----------
s: str
String to test as palindrome
Returns
-------
bool:
Return True if s in a palindrome; False otherwise.
"""
pass
def filter_palindromes(items):
""" Filters a list for palindromes.
e.g. ["ABBA", "snake", "Peep", "this"] returns ["ABBA", "Peep"]
Parameters
----------
items: list
List of strings.
Returns
-------
list:
Filtered list of palindromes
"""
pass
if __name__ == "__main__":
# main program when decomposed
str_list = ["ABBA", "snake", "Peep", "this"]
pal_list = filter_palindromes(str_list)
print(pal_list) # prints ['ABBA', 'Peep']
HINT: You should be calling the is_palindrome()
function from inside the filter_palindromes()
function.
References
[1] Guttag, J.V. (2021) Introduction to Computation and Programming Using Python, third edition: With Application to Computational Modeling. The MIT Press.
[2] https://cs.stanford.edu/people/nick/py/python-style-decomposition.html
MP: The Frequency Count Program 2.0
Consider a similar program to the Frequency Count Program 1.0.
# print the results with the dictionary sorted.
number_list = []
count_dict = {}
input_string = input("Please enter a whole number: ")
while input_string != "q":
number_list.append(int(input_string))
if int(input_string) in count_dict.keys():
count_dict[int(input_string)] += 1
else:
count_dict[int(input_string)] = 1
input_string = input("Please enter a whole number: ")
# print out the count of the numbers
print(f"The numbers seen were {number_list}")
for key, value in sorted(count_dict.items()):
print(f"The count for {key} is: {value}")
For example, the program will work as follows:
Please enter a whole number: 3
Please enter a whole number: 5
Please enter a whole number: 2
Please enter a whole number: 6
Please enter a whole number: 7
Please enter a whole number: 1
Please enter a whole number: 10
Please enter a whole number: 63
Please enter a whole number: q
The numbers seen were [3, 5, 2, 6, 7, 1, 10, 63]
The count for 1 is: 1
The count for 2 is: 1
The count for 3 is: 1
The count for 5 is: 1
The count for 6 is: 1
The count for 7 is: 1
The count for 10 is: 1
The count for 63 is: 1
Decomposition
We can reorder this program to do the three tasks separately.
- Collect the numbers
- Do the counting
- Print out to the count of the numbers to the user
# print the results with the dictionary sorted.
# collect the numbers
number_list = []
input_string = input("Please enter a whole number: ")
while input_string != "q":
number_list.append(int(input_string))
input_string = input("Please enter a whole number: ")
# do the counting
count_dict = {}
for num in number_list:
if num in count_dict.keys():
count_dict[num] += 1
else:
count_dict[num] = 1
# print out the count of the numbers
print(f"The numbers seen were {number_list}")
for key, value in sorted(count_dict.items()):
print(f"The count for {key} is: {value}")
This program now has three distinct parts. That is, it has been decomposed into three separate parts.
We can now go further and wrap each of these parts in a function.
Decompostion with Functions
def get_numbers():
""" Get a sequence of numbers from the user """
number_list = []
input_string = input("Please enter a whole number: ")
while input_string != "q":
number_list.append(int(input_string))
input_string = input("Please enter a whole number: ")
return number_list
def frequency_count(num_list):
""" Count the frequency of the numbers """
count_dict = {}
for num in num_list:
if num in count_dict.keys():
count_dict[num] += 1
else:
count_dict[num] = 1
return count_dict
def print_frequency_count(count_dict):
"""Print out the count of the numbers"""
for key, value in sorted(count_dict.items()):
print(f"The count for {key} is: {value}")
def main():
number_list = get_numbers() # get numbers from user
freq_count_dict = frequency_count(number_list) # get freq count
print(f"The numbers seen were {number_list}")
print_frequency_count(freq_count_dict) # print feedback
if __name__ == '__main__':
main()
You can see here that we now have three functions that have the responsibility for single parts of our program. These are then called in sequence by the main()
function.
get_numbers()
returns the numbers entered by the userfrequency_count()
takes in a list of numbers and counts the frequency. Returns a dictionary.print_frequency_count()
takes in a dictionary containing the frequency count. Prints out the frequency count to the terminal.
Abstraction
This is now much more manageable and I don't care how these functions work, just that they do! You could replace any of these functions with something that has the same inputs and outputs and the main()
program would still work.
Here is an example.
def get_numbers():
return [1,1,1,2,2,4,5,7,35]
The whole program will still work even though we changed get_numbers()
. This is because it still just returns a list of numbers. This is the essence of abstraction. Functions can be treated as black-boxes.
We can even replace frequency_count()
with dictionary comprehension using a list method and some set magic.
def frequency_count(num_list):
return {item:num_list.count(item) for item in set(num_list)}
I'll leave you to try and work this out, but the point again is that we don't care how it works, we just care it does! Black-box!
*args
and **kwargs
This lesson will explain what *args
and **kwargs
are in Python.
1. *args
Let's consider the following function.
def mul(x, y):
return x * y
This is a simple enough function that takes in two numbers. However, what if you would like a function that can multiply any number of numbers. e.g. 3 * 4 * 5 * 6
One way of doing this is with a list.
def mul(num_list):
total = 1
for num in num_list:
total *= num # multiply total by num
return total
list_of_nums = [3,4,5,6]
print(mul(list_of_nums)) # prints 360
This though requires that we create and pass a list of numbers, we can do it another way using *args
.
def mul(*args):
total = 1
for num in args:
total *= num
return total
print(mul(3,4,5)) # prints 60
print(mul(3,4,5,6)) # prints 360
Here we use *args
to represent any number of positional arguments. These get packed into what is known as an iterable (in this case it is a tuple) which we can then loop over. The *
operator in front of args is known as the unpacking operator.
Note that args
is just a name, we could call this something else like numbers
.
For example:
def mul(*numbers):
total = 1
for num in numbers:
total *= num
return total
print(mul(3,4,5)) # prints 60
print(mul(3,4,5,6)) # prints 360
However, it is convention to use args
, other programmers will understand better if you stick to this convention!
Here is another example that finds the maximum from a number of numbers.
def maximum(*args):
# set current max to first item in args
current_max = args[0]
# iterate over the rest of args
for x in args[1:]:
if x > current_max:
current_max = x
return current_max
print(max(1,4,2,7,4)) # prints 7
print(max(1, 4)) # prints 4
2. **kwargs
**kwargs
is very similar to *args
but instead of being positional arguments, it takes in keyword (named) arguments.
**kwargs
will be stored in a dictionary. So you can iterate over the keys()
, values()
or both using items()
.
In the example below we iterate over the values. In this case the price of each item.
def bill(**kwargs):
total = 0
for price in kwargs.values():
total += price
return total
print(bill(bread=5, milk=1.5, orange_juice=1)) # prints
print(bill(banana=3, pasta=3))
print(bill(bread=5, pasta=3, orange_juice=1, milk=1.5))
In the next example we iterate over both the item and the price using the items()
method. Note we have renamed the keyword arguments in the function as bill_items
. Again we can call it what we want, we just need to put the **
unpacking operator in front.
def bill(**bill_items):
total = 0
for key, value in bill_items.items():
print(f"Adding {key} at price {value}")
total += value
return total
print(bill(bread=5, milk=1.5, orange_juic=1))
print(bill(banana=3, pasta=3))
print(bill(bread=5, pasta=3, orange_juice=1, milk=1.5))
# prints ....
# Adding bread at price 5
# Adding milk at price 1.5
# Adding orange_juic at price 1
# 7.5
# Adding banana at price 3
# Adding pasta at price 3
# 6
# Adding bread at price 5
# Adding pasta at price 3
# Adding orange_juice at price 1
# Adding milk at price 1.5
# 10.5
Again, it is convention to use kwargs
, so stick to that.
3. Combining *args and **kwargs
We won't labour on this point, but you can use both in a function, but you need to get the order correct.
Normally we have that positional arguments come before keyword arguments. e.g.
def my_func(x,y,z=10):
pass
For *args
and **kwargs**
, *args
must come after any positional arguments and **kwargs
before any keyword arguments.
However, I would just stick to this order which is common practice:
- Positional Arguments
- Keyword Arguments
*args
arguments**kwargs
arguments
Here are some example function definitions that work and don't work. Try copying them into main.py and running.
3.1 Correct Usage
# This is fine
def my_func(x, y, *args, **kwargs):
pass
# This is fine
def my_func(x, y, *args):
pass
# This is fine
def my_func(x, y, **kwargs):
pass
# This is fine, we now include a keyword argument z
def my_func(x, y, z=10 *args, **kwargs):
pass
3.2 Incorrect Usage
# This is NOT fine, *args before **kwargs
def my_func(x, y, **kwargs, *args):
pass
# This is NOT fine, *args before **kwargs
def my_func(x, y, z=10, **kwargs, *args):
pass
# This is NOT fine, positional arguments come before *args and **kwargs
def my_func(*args, **kwargs, x, y)):
pass
# This is NOT fine, keyword arguments come before *args and **kwargs
def my_func(x, y, *args, **kwargs, z=10):
pass
=== TASK ===
Create a function called initials that takes in a persons names and then returns the initials. You should pass the names using *args
.
For example for James Marshall Hendrix
it should return J.M.H
.
Or, for John Ronald Reuel Tolkien it should return J.R.R.T
(one *args
to rule them all).
Copy this code into a new Python file to get started.
def initials(*args):
pass
if __name__ == "__main__":
print(initials("James", "Marshall", "Hendrix")) # should print the return value of "J.M.H"
print(initials("John", "Ronald", "Reuel", "Tolkien")) # should print the return value of "J.R.R.T"
Lambda Functions, Map and Filter
1. Lambda Functions
Sometimes it is useful to generate a small function on the fly. Lamda functions let us do this. We can also bind them to a variable.
Consider the following code.
def increment(x):
return x+1
print(increment(5)) # prints 6
We can also write this as a lambda function.
increment = lambda x: x+1
print(increment(5)) # prints 6
Notice how we have one x
after the lambda
, this is the function parameter. We then have the return
value which is x + 1
.
We can also do this for functions with multiple parameters.
def sum(x,y):
return x+y
print(sum(5,9)) # prints 14
Can be written as a lambda function as follows.
sum = lambda x,y: x+y
print(sum(5,9)) # prints 14
One more simple example. Our is_even()
function:
def is_even(x):
return x % 2 == 0
print(is_even(4)) # prints True
print(is_even(5)) # prints False
This can be also be written as a lambda function.
is_even = lambda x: x % 2 == 0
print(is_even(4)) # prints True
print(is_even(5)) # prints False
2. Map
map()
is an extremely useful function for working on lists.
It takes two arguments, a function and an iterable (normally a list).
Syntax
map(function, iterable)
Parameter Values
Parameter | Description |
---|---|
function | Required. The function to execute for each item |
iterable | Required. A sequence, collection or an iterator object. You can send as many iterables as you like, just make sure the function has one parameter for each iterable. |
def is_even(x):
return x % 2 == 0
even_nums = map(is_even, [1,2,3,6,26,73,25,99])
# note that map returns a map object and to print we need to cast to a list.
print(list(even_nums)) # prints [False, True, False, True, True, False, False, False]
It essentially applies the function to each item in the list and then returns a new mapped object of the results. To see the contents of this turn it into a list
.
Now we could have done this with a lambda function!
even_nums = map(lambda x: x % 2 == 0, [1,2,3,6,26,73,25,99])
# note that map returns a map object and to print we need to cast to a list.
print(list(even_nums)) # prints [False, True, False, True, True, False, False, False]
Wow, so we just tested a list of numbers to see if they are even using a function that we created on the fly! Phew!
If this seems abit out there (esoteric) it is because it is more advanced and creeping towards the weird and worderful world of functional programming.
If you never use these techniques it really doesn't matter, you don't have to and you will still be a good programmer
Here is another example of using map()
on two lists.
def add_item(x, y):
return x + y
list1 = [1, 2, 3]
list2 = [4, 5, 6]
result = list(map(add_item, list1, list2))
print(result) # Output: [5, 7, 9]
Here we added each item of the list1
and list2
to get a new list of the added items.
Filter
filter()
is another extremely useful function for working on lists.
It takes two arguments, a function (must return a bool) and a list.
Syntax
filter(function, iterables)
Parameter Values
Parameter | Description |
---|---|
function | Required. The function to execute for each item. Returns a bool |
iterable | Required. A sequence, collection, or iterator to filter. |
The filter()
function returns a filter object. To see the results, you need to convert it to a list or another iterable type.
Filtering Even Numbers
Here, we filter even numbers from a list using both a named function and a lambda function:
def is_even(x):
return x % 2 == 0
even_nums = filter(is_even, [1,2,3,6,26,73,25,99])
# note that filter returns a filter object and to print we need to cast to a list.
print(list(even_nums)) # prints [2,6,26]
Again we could have done this with a lambda function!
even_nums = filter(lambda x: x % 2 == 0, [1,2,3,6,26,73,25,99])
# note that map returns a filter object and to print we need to cast to a list.
print(list(even_nums)) # prints [2,6,26]
Again, really neat and pretty funky.
Filtering Based on Multiple Lists
You can filter items using multiple lists by combining them with ``zip()```:
list1 = [3, 6, 1, 8, 5]
list2 = [2, 5, 4, 7, 2]
# Keep items from list1 if they are greater than the corresponding items in list2
result = filter(lambda pair: pair[0] > pair[1], zip(list1, list2))
print([x[0] for x in result]) # Output: [6, 8, 5]
Filtering and Mapping Strings
As a final example we will take a list of string, filter out those less than 5 and then convert them to upper case. Try copying these into main.py to see the output.
str_one = "We've known each other for so long Your heart's been aching, but you're too shy to say it (say it) Inside, we both know what's been going on (going on) We know the game and we're gonna play it"
# first we remove the brackets.
str_one = str_one.replace("(", "")
str_one = str_one.replace(")", "")
# next create a list of words by splitting on " " (space)
string_list = str_one.split(" ")
# filter the strings. Keep those less that 5
filtered_list = filter(lambda s: len(s) < 5, string_list)
# map the upper function over the list of filtered words
upper_filtered_list = map(lambda s: s.upper(), filtered_list)
# prints the final result, need to convert to a list
print(list(upper_filtered_list))
and in one line...
string_list = "We've known each other for so long Your heart's been aching, but you're too shy to say it (say it) Inside, we both know what's been going on (going on) We know the game and we're gonna play it"
upper_filtered_list = map(lambda s: s.upper(), filter(lambda s: len(s) < 5, string_list.replace("(", "").replace(")", "").split(" "))) # note this is a single line
print(list(upper_filtered_list))
Now although that it quite impressive, it lacks readability. If you saw this for the first time, you would have to spend time working out what it does.
We won't do any more on this, but it is important to have seen them as they will appear when you see Python code.
I wasn't taught them and for years I'd either just copy and paste or ignore them. Really you should know what a bit of code does.
=== TASK ===
For a list of strings, filter out the strings that are odd in length and then reverse each of these strings and return the resulting list.
You can reverse a string using:
str_one = "test"
print(str_one[::-1])
A function has been created for you that takes a string and returns a reversed string.
You should update the task_answer()
function so that it returns the correct list.
Copy and paste the following into a new Python file to get started.
def reverse_string(str_to_reverse):
return str_to_reverse[::-1]
def task_answer(string_list):
""" you should filter out odd length strings and reverse these here.
return the result as a list """
pass
if __name__ == "__main__":
str_list = ["this", "IS", "a", "list", "of", "a", "few", "strings"]
result = task_answer(str_list)
print(result) # should print ['a', 'a', 'wef', 'sgnirts']
Recursion 101
In computer science, it refers to solving a problem by solving a smaller version of the same problem that is solved by solving a smaller version of the same problem that is solved by solving a smaller version of the same problem that is solved by solving a smaller version of the same problem …and so on… until we reach a problem that is small enough to be trivial to solve.
This is often done by having a function invoke itself until the problem is trivial to solve without invoking itself.
You can write all recursive functions iteratively (with loops). You will see this.
In general, if you can, write things with loops.
1. Factorial
One of the simplest recursive methods computes the factorial of a number, where given a number n, n factorial (usually written as n!) is computed as:
n! = n × (n-1) × (n-2) ×.. × 1
E.g:
5! = 5 × 4 × 3 × 2 × 1 = 120
NOTE: for those of you familiar with maths. 0! = 1
and this is normally the base case. We have ignored this and are stopping at 1.
If we use computing speak and write this as a function fact(n)
:
# note this is not working code, just more familiar notation
fact(n) = n * (n-1) * ... * 1
E.g:
fact(5) = 5 * (5-1) * ... * 1 = 5*4*3*2*1 = 120
Which we could choose to implement in python write using a for
loop.
def fact(n):
total = 1
for i in range(1, n+1):
total = total * i
return total
print(fact(5)) # prints 120 = 5*4*3*2*1
If we look carefully at
fact(n) = n * (n-1) * ... * 1
we will notice something seemingly unremarkable, but it has deep consequences!
fact(5) = 5*4*3*2*1
Take a look at
4*3*2*1
this is also fact(4)
, therefore we can rewrite fact(5)
as follows:
fact(5) = 5*fact(4)
We now have a function defined in terms of itself!
In general,
fact(n) = n*fact(n-1)
Where,
fact(1) = 1
So, when we call fact(5)
the following happens:
fact(5) = 5*fact(5-1)
which calls
fact(4) = 4*fact(4-1)
which calls
fact(3) = 3*fact(3-1)
which calls
fact(2) = 2*fact(1)
which calls
fact(1)=1
We reach the easily evaluated case of fact(1)
We can then unwind all of this.
fact(1)=1
means that
fact(2) = 2*fact(1) = 2*1 = 2
which means
fact(3) = 3*fact(2) = 3*2 = 6
which means
fact(4) = 4*fact(3) = 4*6 = 24
and finally
fact(5) = 5*24 = 120
fact(1)
is known as the base case. This is normal a case we know how to evaluate easily. If you think about it, if we didn’t have a base case, we would have just kept on going!!!
Written as a python program is simple. We just need to put in our general recursion and base case.
fact(n) = n*fact(n-1)
Where,
fact(1) = 1
Which becomes
def fact(n):
if n < 2:
return 1 # return base case
return n*fact(n-1)
print(fact(5)) # prints 120
Note that we could put in a float or a number less than 0 and get erroroneous answers, but for ease of reading we assume whole numbers of 0 and above.
You can visualise what is happening on Python Tutor. Paste in the code and call fact(4)
which will return 24
.
2. Summing a List of Numbers
We can also consider summing a list of numbers, e.g. 5,4,2,6
.
Let’s just define a function called sum_list()
that takes a list of numbers and adds them up.
Then by definition of additon.
sum_list([3,4,9]) = 3 + sum_list([4,9])
sum_list([3,4,9]) = 3 + sum_list([4,9])
= 3 + 4 + sum_list([9])
= 3 + 4 + 9 + sum_list([])
= 3 + 4 + 9 + 0 = 16
Here we can clearly see the base case is the sum of an empty list, which is just 0
. i.e. sum_list([]) = 0
.
In general,
sum_list(number_list) = item_one + sum_list(rest_of_list)
Where,
sum_list(empty_list) = 0
def sum_list(num_list):
# test for base case
if len(num_list) == 0:
return 0
# return head + sum of rest of list
return num_list[0] + sum_list(num_list[1:])
print(sum_list([3,4,9])) # prints 16
To me that is pretty darn beautiful!
Note that you can also do this in one less recursive call by noting the following:
sum_list([3,4,9]) = 3 + sum_list([4,9])
= 3 + 4 + sum_list([9])
= 3 + 4 + 9
i.e. a list with 1 item can also be considered a base case. We don't have to go all the way to the empty list.
sum_list(number_list) = item_one + sum_list(rest_of_list)
Where,
sum_list(single_item_list) = single_item_list
def sum_list(num_list):
# test for base case
if len(num_list) == 1:
return numlist[0]
# return head + sum of rest of list
return num_list[0] + sum_list(num_list[1:])
print(sum_list([3,4,9])) # prints 16
Of course you can just use a loop.
def sum_list(num_list):
total = 0
for num in num_list:
total += num
return total
print(sum_list([3,4,9])) # prints 16
3. Fibonacci Numbers
No introduction to recursion would be complete without a look at the Fibonacci numbers.
These are a sequence of numbers that you get by adding the previous two numbers of the sequence. You start with 1 and 1.
1,1 # add 1 and 1
1,1,2 # add 1 and 2
1,1,2,3 # add 2 and 3
1,1,2,3,5 # add 3 and 5
etc...
1,1,2,3,5,8,13,21,34
If you define the nth fibonacci number of the sequence as fib(n)
then
# the next number is the sum of the previous 2 in the sequence!
fib(n) = fib(n-1) + fib(n-2)
where,
fib(0) = 1 and fib(1) = 1
Here we have the recursive formula and the two base cases fib(0)
and fib(1)
.
We can easily put this into python.
def fib(n):
if n < 2:
return 1
return fib(n-1) + fib(n-2)
# Two ways of printing the first 6 terms
for n in range(6):
print(f"fib({n}) = {fib(n)}")
# prints
# fib(0) = 1
# fib(1) = 1
# fib(2) = 2
# fib(3) = 3
# fib(4) = 5
# fib(5) = 8
print([fib(n) for n in range(6)]) # prints [1,1,2,3,5,8]
I recommend that you look at this in Python Tutor to see an animated version of what is happening.
4. Why Bother?
You can write all recursive methods iteratively (with loops), but:
- it does require you to track the information
- sometimes it is just a lot easier to solve a problem recursively.
You'll see a lovely example of this in the exercise for the lesson on Memoization.
In general, if you can, write things with loops.
===TASK ===
Create the functions string_list_concat()
and string_list_concat_with_space()
so that it takes a list of strings as input and concatenates (joins) them into one string as per the function docstring.
YOUR FUNCTION SHOULD USE RECURSION AND USE THE MINIMAL AMOUNT OF RECURSIVE CALLS.
HINT: Section 2 on summing numbers is an excellent place to start. You should consider how this task is similar to the recursive program given in Section 2.
Copy and paste the following into a new Python file to get started.
def string_list_concat(string_list):
""" Concatenates a list of strings into a single string
eg. ["This", "is", "a", "list", "of", "strings"]
returns
"Thisisalistofstrings"
"""
pass
def string_list_concat_with_space(string_list):
""" Concatenates a list of strings into a single string
eg. ["This", "is", "a", "list", "of", "strings"]
returns
"This is a list of strings"
"""
pass
if __name__ == "__main__":
str_list = ["This", "is", "a", "list", "of", "strings"]
print(string_list_concat(str_list)) # prints the return value of "Thisisalistofstrings"
print(string_list_concat_with_space(str_list)) # prints the return value of "This is a list of strings"
Memoization (Advanced)
We won't go into great detail here, but we will look at improving the efficiency of the Fibonacci numbers. This is a reasonably advanced topic.
There are two ways of doing this.
One is simply the bottom-up approach. Instead of writing recursively, we just write this with a loop.
The other is by making recursion more efficient.
Sometimes a recursive algorithm is just easier to write, so looking for improvements on speed and memory are important.
To demonstrate this, try the following:
import timeit
def fib(n):
if n < 2:
return 1
return fib(n-1) + fib(n-2)
start_time = timeit.default_timer()
# call the function
fib(35)
end_time = timeit.default_timer()
print(end_time - start_time) # You will get about 5 seconds for this. That is slow!!!
You will get about 5 seconds for this. That is slow!!!
import timeit
def fib_loop(n):
if n < 2:
return 1
fib0 = 1
fib1 = 1
while n > 2:
temp = fib1
fib1 = fib1 + fib0
fib0 = temp
n -= 1
return fib1
start_time = timeit.default_timer()
# call the function
fib_loop(35)
end_time = timeit.default_timer()
print(end_time - start_time) # You will get a fraction of a second for this. That is much faster!!!
You will get a fraction of a second for this. That is much faster!!!
In fact as soon as you go beyond about 36 it starts to take a long long time.
This is the essence of something called computational efficiency. Sometimes our algorithms are just plain bad and slow!
It doesn't matter if we have the worlds fastest machine, in some cases, it just won't complete.
What is Going On?
The recursive approach continues to call the function until it hits a base case. In the cases of the fibonacci, this is actually a tree of function calls.
What you should notice is that there are multiple calls to the fib()
function for fib(2)
, fib(1)
and fib(0)
. This takes time.
The key to making this faster is that when we make a call to a function with a given value, e.g. fib(1)
, we store the result in a dictionary and then when we try to call it again, instead of using recursion, we just look up the result in the dictionary.
I would read the following article for a more in depth explanation.
Memoization
To code this in Python we can save a fib number everytime we see it and then if we see it again we just look it up, we don't do the expensive function call!
Here is the code. The function fib_memo
uses a dictionary memo
to keep track of the fib numbers. I strongly recommend that you paste this into Python Tutor.
import timeit
def fib_memo(n, memo):
# if not seen add to dictionary
if not n in memo.keys():
# add unseen to dictionary
if n < 2:
memo[n] = 1
else:
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
fib_nums = {}
start_time = timeit.default_timer()
# call the function
fib_memo(35, fib_nums)
end_time = timeit.default_timer()
print(end_time - start_time) # You will get a fraction of a second for this. That is much faster!!!
Analysing all 3 Approaches
The program below will output the following plot using Python's plotting library matplotlib.
You can clearly see here that the the recursive approach is very poor after only 36.
Thus, you can use the other approaches to generate much much larger Fibonacci numbers.
import timeit
import matplotlib.pyplot as plt
def fib(n):
if n < 2:
return 1
return fib(n-1) + fib(n-2)
def fib_loop(n):
if n < 2:
return 1
fib0 = 1
fib1 = 1
while n > 2:
temp = fib1
fib1 = fib1 + fib0
fib0 = temp
n -= 1
return fib1
def fib_memo(n, memo):
# if not seen add to dictionary
if not n in memo.keys():
# add unseen to dictionary
if n < 2:
memo[n] = 1
else:
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
def time_function(func, n, memo=None):
start_time = timeit.default_timer()
if not memo == None:
func(n, memo)
else:
func(n)
end_time = timeit.default_timer()
return end_time - start_time
fib_nums = {}
cases = range(1,36)
times_fib = []
times_fib_loop = []
times_fib_memo = []
for n in cases:
print(f"Computing times for n={n}")
times_fib.append(time_function(fib, n))
times_fib_loop.append(time_function(fib_loop, n))
times_fib_memo.append(time_function(fib_memo, n, memo=fib_nums))
plt.plot(cases, times_fib)
plt.plot(cases, times_fib_loop)
plt.plot(cases, times_fib_memo, '--')
plt.xlabel("Fib Number (n)")
plt.ylabel("Time (s)")
plt.legend(["Fib Recursive", "Fib Loop", "Fib Recursive Memo"])
plt.show()
=== TASK ===
Please note that this is an advanced topic and you are by no means expected to be able to do this straight away. In fact you shouldn't worry if you can't complete it. If you are struggling then I would move on.
We have a grid. Your goal is to walk through the grid from start to finish by only moving right or down.
How many paths are there from the start to the finish given that we can move right or down?
For the two by two grid given in the example we have the following paths.
RRDD
RDRD
RDDR
DRRD
DRDR
DDRR
To solve this we can turn to recursion.
If we look at the number of paths at the end (2,2)
we can note that they will actually be the sum of the number of paths to (1,2)
and (2,1)
.
Similarly the number of paths from (1,2)
are the sum of the number of paths to (0,2)
and (1,1)
.
The number of paths on the edge, such as (0,2)
are the just the number of paths from (0,1)
(that is you can only get to (0,2)
from (0,1)
.
Once we get back to the start (0,0)
there is just 1 path, that is, the start.
We actually have the following:
if we are not on an edge
no_paths(x,y) = no_paths(x-1,y) + no_paths(x,y-1)
if we are on an edge left x = 0
no_paths(0,y) = no_paths(0, y-1)
if we are on an edge left y = 0
no_paths(x,0) = no_paths(x-1, 0)
if we are at the start (0,0)
no_paths(0,0) = 1
We can now write this as a recursive function as follows:
def no_paths(x,y):
if x == 0 and y == 0:
return 1
elif x == 0:
return no_paths(0, y-1)
elif y == 0:
return no_paths(x-1, 0)
else:
return no_paths(x-1, y) + no_paths(x, y-1)
print(no_paths(1,1)) # prints 2
print(no_paths(2,2)) # prints 6
print(no_paths(1,2)) # prints 3
Copy and paste the following into a new Python file to get started.
You need to fix the paths_memo()
function so that it uses memoization. This will mean that it will run extremely quickly, where as trying to run larger numbers for paths
will take a long time or never end.
import timeit
def no_paths(x,y):
if x == 0 and y == 0:
return 1
elif x == 0:
return no_paths(0, y-1)
elif y == 0:
return no_paths(x-1, 0)
else:
return no_paths(x-1, y) + no_paths(x, y-1)
def no_paths_memo(x, y, memo):
pass
if __name__ == "__main__":
# This times the paths method
start_time = timeit.default_timer()
# walk a grid 13 by 13. This takes approximately 2.5 seconds
print(f"Number of paths for a 13x13 grid is {no_paths(12,12)}")
end_time = timeit.default_timer()
print(f"Time taken: {end_time - start_time}")
# This times the paths_memo method
start_time = timeit.default_timer()
# walk a grid 13 by 13. This should take a fraction of a second
print(f"Number of paths for a 13x13 grid is {no_paths_memo(12,12, {})}")
end_time = timeit.default_timer()
print(f"Time taken: {end_time - start_time}")
References
Closures and Decorators (Advanced)
Please note that this is a long lesson because there are a lot of details that you need to understand closures and decorators.
In this lesson we will learn about:
- Global variables
- Nested functions
- Nonlocal variables
- Higher order functions
- Closures
- Decorated functions
1. Closures
1.1 Global Variables
I have deliberately not introduced global
variables until now because in 99.9% of time if you are using a global variable you have designed your program badly.
I am introducing them so that we will understand nonlocal
variables in a couple of sections.
Consider the following code,
def add_five(x):
x = x + 5
print(f"Value of x in add_five() is {x}")
x = 10
print(f"Value of x before add_five() is {x}")
add_five(x)
print(f"Value of x after add_five() is {x}")
will output:
Value of x before add_five() is 10
Value of x in add_five() is 15
Value of x after add_five() is 10
This is because the x
in add_five()
is local to the function add_five()
. Modifying it does not change the x
defined in the global scope (outside the function).
We can alter this by adding the global
keyword in front of x
in the function and not passing it as a parameter as follows:
def add_five():
global x
x = x + 5
print(f"Value of x in add_five() is {x}")
x = 10
print(f"Value of x before add_five() is {x}")
add_five()
print(f"Value of x after add_five() is {x}")
The output now is:
Value of x before add_five() is 10
Value of x in add_five() is 15
Value of x after add_five() is 15
Now we are modifying the global
variable x
, it does not belong to the scope of the add_five()
function.
1.2 Nested Functions
Python allows you to nest a function within a function. Here is an example.
def outer():
print("Hi I am in the outer function")
def inner():
print("Hi I am in the inner function")
# make a call to the inner function
inner()
print("Hi I am again in the outer function")
outer()
This will print out the following:
Hi I am in the outer function
Hi I am in the inner function
Hi I am in again in the outer function
1.3 Nonlocal variables
A nonlocal
variable works very much like a global variable, but it is restricted to the scope of the outer function.
Consider the following example,
def outer():
x = 10
print(f"Hi I am in the outer function, the value of x is {x}")
def inner():
print(f"Hi I am in the inner function, the value of x is {x}")
# make a call to the inner function
inner()
print(f"Hi I am again in the outer function, the value of x is {x}")
outer()
this will result in,
Hi I am in the outer function, the value of x is 10
Hi I am in the inner function, the value of x is 10
Hi I am in again in the outer function, the value of x is 10
as the inner()
function has access to the x
defined in the scope of the outer()
function.
Now consider the following example,
def outer():
x = 10
print(f"Hi I am in the outer function, the value of x is {x}")
def inner():
x = 15
print(f"Hi I am in the inner function, the value of x is {x}")
# make a call to the inner function
inner()
print(f"Hi I am again in the outer function, the value of x is {x}")
outer()
this prints out the following:
Hi I am in the outer function, the value of x is 10
Hi I am in the inner function, the value of x is 15
Hi I am in again in the outer function, the value of x is 10
Notice how once the inner()
function had ended, the value of x
is 10
, this is because the x
that is defined at the beginning of outer()
is not the same as the one that is used at the beginning of inner()
.
They are different, when we ran the code x = 15
it defined a new x
in the scope of inner()
.
Now consider the following:
def outer():
x = 10
print(f"Hi I am in the outer function, the value of x is {x}")
def inner():
nonlocal x
x = 15
print(f"Hi I am in the inner function, the value of x is {x}")
# make a call to the inner function
inner()
print(f"Hi I am again in the outer function, the value of x is {x}")
outer()
Here we use the nonlocal
keyword to let the inner()
function know that we are working with the x
that belongs to the outer()
function.
The output is,
Hi I am in the outer function, the value of x is 10
Hi I am in the inner function, the value of x is 15
Hi I am in again in the outer function, the value of x is 15
and you can see that we actually changed the value of the x
belonging to outer()
.
1.3.1 Being More Explicit
We can be more explicit by using the outer function name when declaring our x
variable.
For example,
def outer():
# declare the variable using the function name.
outer.x = 10
print(f"Hi I am in the outer function, the value of x is {outer.x}")
def inner():
x = 15
print(f"Hi I am in the inner function, the value of x belonging to inner is {x}")
print(f"Hi I am in the inner function, the value of x belonging to outer is {outer.x}")
# make a call to the inner function
inner()
print(f"Hi I am again in the outer function, the value of x is {outer.x}")
outer()
will print out:
Hi I am in the outer function, the value of x is 10
Hi I am in the inner function, the value of x belonging to inner is 15
Hi I am in the inner function, the value of x belonging to outer is 10
Hi I am in again in the outer function, the value of x is 10
Now it is clear that the inner function is not overwriting the value of outer.x
, but we can still access it within the inner function.
We could of course overwrite outer.x
from within the inner function:
def outer():
# declare the variable using the function name.
outer.x = 10
print(f"Hi I am in the outer function, the value of x is {outer.x}")
def inner():
outer.x = 15
print(f"Hi I am in the inner function, the value of x is {outer.x}")
# make a call to the inner function
inner()
print(f"Hi I am again in the outer function, the value of x is {outer.x}")
outer()
This prints out the following:
Hi I am in the outer function, the value of x is 10
Hi I am in the inner function, the value of x is 15
Hi I am in again in the outer function, the value of x is 15
1.4 Higher Order Functions
In Python, fucntions are first class objects. This means that you can do the same things with them as you do with other objects like:
- Assign to a variable
- Pass them into other functions
- Return them from other functions
- Store them in data strucutres like lists and dictionaries
For example the following assigns a function to a variable.
def add(x,y):
return x + y
# assign the function add to the name my_add
my_add = add
print(my_add(5,3)) # prints 8
print(add(5,3)) # prints 8
This example returns a function from another function.
# this function returns a function
def another_function():
def add(x,y):
return x + y
return add
# assign the return function to my_add
my_add = another_function()
# call the function using my_add
print(my_add(5,2))
Finally the following example passes a function into another function and then calls it.
def print_function(func, *args):
# print the return values of the passed in function
print(func(*args))
def add(x,y):
return x + y
# pass the add function to print_function
print_function(add, 3, 4) # prints 7
1.5 Closures
What is a closure?
The following is the definition given on Geeks For Geeks:
A Closure is a function object that remembers values in enclosing scopes even if they are not present in memory.
-
It is a record that stores a function together with an environment: a mapping associating each free variable of the function (variables that are used locally but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.
-
A closure—unlike a plain function—allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.
When do we have closures?
The criteria that must be met to create a closure in Python are summarised in the following points.
- We must have a nested function (function inside a function).
- The nested function must refer to a value defined in the enclosing function.
- The enclosing function must return the nested function.
This last statement is important. We can return a function, just like a variable. In Python functions are first class objects and we pass them around just like variables, lists etc.
1.4.1 A Simple Example
Here is an example of a closure that returns a function that will multiply a number by a set number, e.g. 3
or 5
.
def create_adder(n):
def adder(x):
return n + x
return adder
# Creates a function that is adds 3 to the input
add3 = create_adder(3)
# Output: 12
print(add3(9))
# Creates a function that is a adds 5 to the input
add5 = create_adder(5)
# Output: 8
print(add5(3))
# Output: 10
print(add5(add3(2)))
We can see here that create_adder()
is a function that returns another function using the enclosing functions n
.
So add3 = create_adder(3)
is actually creating and returning the following function.
def add3(x):
return 3 + x
# Output: 12
print(add3(9))
2. Decorators
Decorators are basically a closure, whereby we pass in a function and add additional functionality around the function and then return another (decorated) function.
Functions are callable and implement the special __call__()
method. Other objects such as methods (see object orientation and classes) implement this method as well.
Anything that is callable can be decorated.
2.1 Decorated Function
Here is a simple example which takes in a function and adds some additional print statements around the function.
def pretty_print(f):
def inner():
print("Let's print out this function in a pretty way")
print("---------------------------------------------")
# invoke the passed in function
f()
print("---------------------------------------------")
return inner
def print_hello():
print("Hello")
# pass pretty_print the print_hello() function
# this returns a new decorated function
pretty_hello = pretty_print(print_hello)
# invoke the decorated function
pretty_hello()
Prints out the following:
Let's print out this function in a pretty way
---------------------------------------------
Hello
---------------------------------------------
Here we are passing pretty_print()
the print_hello()
function and assigning it to the name pretty_hello
.
We can then call this newly decorated function.
2.2 Using the @
Symbol for Decorating
It turns out that this is such a common pattern that Python has a special way of marking a function to be decorated by another function.
To do this we write @decorator_name
above the function to be decorated. decorator_name
is the name of the function that does the decorating.
For example we can amend the code above as follows:
def pretty_print(f):
def inner():
print("Let's print out this function in a pretty way")
print("---------------------------------------------")
f()
print("---------------------------------------------")
return inner
def print_hello():
print("Hello")
# decorates the function using the @ symbol
@pretty_print
def pretty_hello():
print_hello()
pretty_hello()
Prints out the following:
Let's print out this function in a pretty way
---------------------------------------------
Hello
---------------------------------------------
2.3 Decorating with Parameters
We can also decorate functions that take in parameters, good job because it would be very limiting if we couldn't.
Here is an excellent example modified from Programiz.
We decorate a standard division so that if we try to divide by 0 we get infinity (division by zero is actually an error, but some calculators display infinity because of something known as limits.)
import math
def smart_divide(func):
# note that the parameters of inner match the parameters of func
def inner(a, b):
print("I am going to divide", a, "and", b)
if b == 0:
print("Whoops! cannot divide")
return math.inf
return func(a, b)
return inner
@smart_divide
def divide(a, b):
return a/b
if __name__ == "__main__":
print(divide(3,2))
print(divide(3,0))
The above prints out:
I am going to divide 3 and 2
1.5
I am going to divide 3 and 0
Whoops! cannot divide
inf
The following logs (via print statements) the result of a simple add()
function.
def logger_add(func):
# note that the parameters of inner match the parameters of func
def inner(x, y):
print(f"I am going to add {x} and {y}")
result = func(x,y)
print(f"The answer is {result}")
return inner
@logger_add
def add(x, y):
return x + y
add(3,4)
# prints
# I am going to add 3 and 4
# The answer is 7
You may notice that the parameters of inner()
function in logger_add()
match the parameters of the function passed in, in this case add()
.
We can get around this by using *args
to take in a unspecified number of arguments.
Here is an example.
def logger_add(func):
# note that the parameters of inner match the parameters of func
def inner(*args):
print(f"I am going to add {args}")
result = func(*args)
print(f"The answer is {result}")
return inner
@logger_add
def add_two(x, y):
return x + y
@logger_add
def add_three(x, y, z):
return x + y + z
add_two(3,4)
# prints
# I am going to add (3, 4)
# The answer is 7
add_three(3,4,5)
# prints
# I am going to add (3, 4, 5)
# The answer is 12
You can also accepts any number of arguments and keyword arguments.
def universal_decorator(func):
def inner(*args, **kwargs):
print("I can decorate all functions")
func(*args, **kwargs)
return inner
@universal_decorator
def funky_func(x, y, z=1):
print(x)
print(y)
print(z)
# call the decorated function
funky_func(3,4,z=10)
The above will print out:
I can decorate all functions
3
4
10
=== TASK ===
If you have read everything and understand it, then this should be reasonably straight forward.
Create a logger for a function called logger()
. (I would use the examples below as your starting point.)
Your logger should decorate a function and do the following:
- print some stuff
- call the function
- print some more stuff
It should work on any function with any number of arguments or keyword arguments.
Note you will need to look up how to get access to a functions name. You can read the following to help - Function Name
The following examples illustrate how the logger decorator should work. Currently the decorator does nothing but return the same function.
Example 1
def logger(func):
# write your code here
return func
@logger
def add_three(x, y, z):
print(x + y + z)
if __name__ == "__main__":
add_three(3,4,5)
The above code should print the following:
Logging function...
The functions name is add_three
Calling the function...
12
Ending logging...
Example 2
def logger(func):
# write your code here
return func
@logger
def hello():
print("hello")
if __name__ == "__main__":
hello()
The above code should print the following:
Logging function...
The functions name is hello
Calling the function...
hello
Ending logging...
Example 3
def logger(func):
# write your code here
return func
@logger
def test_keyword(a=1, b=2, c=3):
print(a+b+c)
if __name__ == "__main__":
test_keyword(a=2)
The above code should print the following:
Logging function...
The functions name is test_keyword
Calling the function...
7
Ending logging...
To get started copy and paste the following into a new Python file. Currently the logger
function just returns the function undecorated.
def logger(func):
# write your code here
return func
@logger
def funky_func(x, y, z=1):
print(x)
print(y)
print(z)
@logger
def add_three(x, y, z):
return x + y + z
@logger
def hello():
print("hello")
@logger
def test_keyword(a=1, b=2, c=3):
print(a + b + c)
if __name__ == "__main__":
funky_func(3, 4, z=10)
print()
funky_func(3, 4)
print()
add_three(3, 4, 5)
print()
hello()
print()
test_keyword()
It should output the following if implemented correctly.
Logging function...
The functions name is funky_func
Calling the function...
3
4
10
Ending logging...
Logging function...
The functions name is funky_func
Calling the function...
3
4
1
Ending logging...
Logging function...
The functions name is add_three
Calling the function...
12
Ending logging...
Logging function...
The functions name is hello
Calling the function...
hello
Ending logging...
Logging function...
The functions name is test_keyword
Calling the function...
6
Ending logging...
Unit 9 - Object Oriented Programming 1 (OOP)
This week, we begin object-oriented programming. Object-oriented programming is sometimes considered difficult, but the concept is simple:
Instead of writing one big monolithic program with a bunch of functions, you write a bunch of simple, small, self-contained programs, each with their own functions. Then you make the small, self-contained programs communicate with each other by invoking each other’s functions, which collectively does the same thing that one big monolithic program would do.
Done properly, a bunch of little programs should be easier to write, easier to debug, and easier to maintain than one big program. Also, you might be able to re-use some of the little programs in other applications or other parts of the same application.
At least, that’s the idea. Whether object-oriented programming actually is easier than writing programs only modularised with functions is a matter of some debate (and largely beyond the scope of this module), but object-oriented programming is pervasive in the industry and will likely remain so for a long time, so it makes sense to learn it.
To summarise what will be covered:
- A class defines instances, also known as objects, of that class. The relationship between a class and its instances is 0-to-many; you have a class and zero or more instances of it. This relationship can be understood in different ways:
-
A class represents a category; an instance is in item in that category. For example, my little buddies Calvin and Indi are instances of the class Cat.
-
A class is like the source code for a small, self-contained program. An instance of a class is that small program loaded into memory and ready to run.
-
A class can represent a data type. Just as built-in types like
int
,float
, andstr
are data types (and 3, 3.4, “fish” notionally instances thereof), you can create your own data types using classes and create instances of them. For example, you might create a Book class with instances “20,000 Leagues Under the Sea” and “The Hobbit”. -
A class is like a blueprint; instances are things made from the blueprint.
-
- You can make an instance do things by running, also known as invoking, its methods. A method is simply a function associated with a particular class, and thus all its instances. Collectively, methods represent the possible behaviour of a class, and thus all its instances.
- An instance usually contains data. This is stored in instance variables (sometimes called member variables) or fields. The data in instance variables is often described as representing the state of an instance. If the data in an instance has changed, we say the instance’s state has changed. In Python, the preferred mechanism for reading or changing instance data is via attributes or properties. We can also use methods to read or change instance variables. In languages like Java, the only way to read or change instance variables is via methods. In languages like C#, using properties is preferred, but methods can also be used to read or update instance variables.
- We say that an instance “can take care of itself”. In other words, you can ask an instance to do things (by invoking its methods), but you don’t have to know or care how the instance does it. You communicate with the instance – i.e., tell it to do things, or retrieve or change its data – through public methods and (in Python and some but not all other object-oriented languages) properties. The internal implementation details of the instance are kept hidden (i.e. private). This hiding of the internal implementation and data is often known as encapsulation.
If you’ve not done object-oriented programming before, this may seem complicated and confusing, but it will become clearer over the next few weeks – and beyond if you take Programming II.
Classes, Methods and Objects
Instead of writing one big monolithic program with a bunch of functions, you write a bunch of simple, small, self-contained programs, each with its own functions. Then you make the small, self-contained programs communicate with each other by invoking each other’s functions, which collectively does the same thing that one big monolithic program would do.
Done properly, a bunch of little programs should be easier to write, easier to debug, and easier to maintain than one big program. Also, you might be able to reuse some of the little programs in other applications or other parts of the same application.
At least, that’s the idea. Whether object-oriented programming is easier than writing programs only modularised with functions is a matter of some debate (and largely beyond the scope of this module), but object-oriented programming is pervasive in the industry and will likely remain so for a long time, so it makes sense to learn it.
1. Defining a Class and a Method
Before we define our first class, let's take a quick look back at types.
1.1 Types and Objects
So far we have been using built-in types that Python gives us like int
, float
, str
, bool
. An object was an instance of one of these types, e.g. 5
is an object of type int
.
Python lets us check what type an object is with the type()
function.
x = 5
print(type(x))
<class 'int'>
Now, what if we want to build our own types? This is exactly where classes come in, object orientation is a way of representing types and this is what we are about to learn.
Let's start by making a simple class
that represents a pet. Yes, that is right, a pet, you can represent whatever you like, you are the architect!
This class
will be a blueprint (template) that will let us create many pets.
We will start by giving our Pet
class a single method (this is like a function, but it belongs to the class) called talk()
which will just allow an instance of a pet to say Hi.
The following code does this and also create two instances of Pet and invokes (calls) the talk()
method for each instance of the class (object). You will also notice that talk
is defined with the word self
as the first parameter. This will be explained in the next lesson.
class Pet:
""" A pet class """
# This is a method, it belongs to the instance of the pet created
def talk(self):
print("Hi, I am an instance of pet")
if __name__ == "__main__":
# create an instance of Pet
pet_one = Pet()
# create another instance of Pet
pet_two = Pet()
# call the method talks on the object pet_one
pet_one.talk()
# call the method talks on the object pet_two
pet_two.talk()
Hi, I am an instance of pet
Hi, I am an instance of pet
OK, so nothing really magic here yet...
1.1 Type of a Class
Before we move on, back to types, let's check what type our Pet
objects are.
pet_one = Pet()
print(type(pet_one))
<class 'Pet'>
Ah so the type of pet_one
is the Pet
class, this now starts to shed some light on other built-in types such as int
, float
, str
and also why we got the class
word in front of our type.
Internally Python uses classes to build its built-in types.
=== TASK ===
Create a class to represent a person.
Your person class should be called Person
and have one method called wave()
which should print out Hi, I am a person and I am waving!
Constructors and Attributes
1. Constructors
Constructors are a special method that you can define that is called when you first create an instance of a class. In Python it is given the name __init__()
(for initialise the object), again we put the word self
as the first parameter.
class Pet:
""" A pet class """
def __init__(self):
print("Creating a new pet")
def talk(self):
print("Hi, I am an instance of pet")
if __name__ == "__main__":
# create an instance of Pet, which will invoke the constructor
pet_one = Pet()
# create another instance of Pet, which will invoke the constructor
pet_two = Pet()
pet_one.talk()
pet_two.talk()
Creating a new pet
Creating a new pet
Hi, I am an instance of pet
Hi, I am an instance of pet
OK, again that is all very nice, but why!?
2. Attributes
Attributes are names (variables) that we can define in classes, this is where the real power starts to show.
It allows us to create instances of the class
with different data for the attributes. Hence we can now have different instances (objects) of Pet
that have different names.
2.1 Initialising Attributes
We do this by creating a name attribute that our constructor __init__()
takes as a parameter and then sets for the instance of the class using self
.
It is key to note the use of self
. In both __init__()
and talk()
it is the first parameter. You don't have to call it self
, but it is convention.
self
gives us access to the data for the given instance. That is self.name
contains different data for the instance pet_one
and the instance pet_two
.
The class
now looks as follows:
class Pet:
""" A pet class """
# take 1 parameter into the constructor - name
def __init__(self, name):
print("Creating a new pet")
# set the instance attribute to the one passed into the constructor
self.name = name
def talk(self):
""" An instance method that says hi """
print(f"Hi, my name is {self.name}")
if __name__ == "__main__":
# create an instance of Pet, which will invoke the constructor
pet_one = Pet("Fidget")
# create another instance of Pet, which will invoke the constructor
pet_two = Pet("Rex")
pet_one.talk()
pet_two.talk()
This now prints out:
Creating a new pet
Creating a new pet
Hi, my name is Fidget
Hi, my name is Rex
We can extend this so that our pets have an age.
class Pet:
""" A pet class """
def __init__(self, name, age):
self.name = name
self.age = age
def talk(self):
""" An instance method that says hi """
print(f"Hi, my name is {self.name} and I am {self.age} years old.")
if __name__ == "__main__":
pet_one = Pet("Fidget", 12)
pet_two = Pet("Rex", 5)
pet_one.talk()
pet_two.talk()
Prints out:
Hi, my name is Fidget and I am 12 years old.
Hi, my name is Rex and I am 5 years old.
2.2 Keyword Attributes
We can also add attributes that have a default value by using a keyword argument in our constructor __init__()
.
Here we add an attribute for the number of legs a Pet
has. We will assume a Pet
has 4
legs unless overridden.
class Pet:
""" A pet class """
def __init__(self, name, age, no_legs=4):
self.name = name
self.age = age
self.no_legs = no_legs
def talk(self):
""" An instance method that says hi """
print(f"Hi, my name is {self.name} and I am {self.age} years old.")
print(f"I have {self.no_legs} legs.")
if __name__ == "__main__":
pet_one = Pet("Fidget", 12)
pet_two = Pet("Cuckoo", 5, 2)
pet_one.talk()
pet_two.talk()
Prints out:
Hi, my name is Fidget and I am 12 years old.
I have 4 legs.
Hi, my name is Cuckoo and I am 5 years old.
I have 2 legs.
2.3 Accessing Attributes
We can also access the name and age via the class instance (object).
print(pet_one.name) # prints Fidget
print(pet_one.age) # prints 12
2.4 Editing Attributes
All attributes are public in Python and can be changed. We will talk about how to make something private later (in Python it is more obfuscated and not really private).
Here we amend the age
attribute of pet_one
.
class Pet:
""" A pet class """
def __init__(self, name, age):
self.name = name
self.age = age
def talk(self):
print(f"Hi, my name is {self.name} and I am {self.age} years old.")
if __name__ == "__main__":
pet_one = Pet("Fidget", 12)
pet_one.talk()
pet_one.age = 13
pet_one.talk()
Hi, my name is Fidget and I am 12 years old.
Hi, my name is Fidget and I am 13 years old.
=== TASK ===
Create a class called Book
.
Your class should have the following attributes:
Attribute Name | Description |
---|---|
title | Title of the book |
author | Author of the book |
price | Price of the book |
And optional parameters:
Attribute Name | Description | Default Value |
---|---|---|
no_pages | Number of pages in the book | None |
year | Year first published | None |
It should also have a method called description()
- HINT: Remember self
.
When called, description()
should return the description.
Here is an example of how Book
should work.
Example 1
book_one = Book("1984", "George Orwell", 6.99)
print(book_one.description())
Should print:
Title: 1984
Author: George Orwell
Price: £6.99
Example 2
book_one = Book("1984", "George Orwell", 6.99, no_pages=328)
print(book_one.description())
Should print:
Title: 1984
Author: George Orwell
Price: £6.99
No. of Pages: 328
Example 3
book_one = Book("1984", "George Orwell", 6.99, year=1949)
print(book_one.description())
Should print:
Title: 1984
Author: George Orwell
Price: £6.99
Year Published: 1949
Example 4
book_one = Book("1984", "George Orwell", 6.99, no_pages=328, year=1949)
print(book_one.description())
Should print:
Title: 1984
Author: George Orwell
Price: £6.99
Year Published: 1949
No. of Pages: 328
Magic Methods
In Python methods that start with a double underscore (dunder) and end with a double underscore are known as magic methods or dunder methods.
These methods are not supposed to be used by the programmer, but they are used by Python for special operations on the class.
For example, when you add two numbers of type int
, python invokes (calls) the __add__()
method for the int
class.
If you type dir(int)
into the terminal you will get a list of all the method names defined in the int
class:
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
You can see that int
has an __add__()
method, this is the method that is actually invoked (called) when you use the +
operator. e.g. 5 + 3
.
We can even do this manually (try it in the terminal):
x = 5
x.__add__(3) # will return a new int object 8
What Python does is provide you with the +
operator because it makes sense too, but really it is doing the above.
NOTE: You should never invoke (call) a magic method yourself, they are used by other methods or special symbols like +
.
1. __init__() Method
We have already seen this method. This is the method that gets invoked when an instance of a class is created.
For example,
class Person:
""" A simple person class """
def __init__(self, name):
self.name = name
if __name__ == "__main__":
person_one = Person("Sarah") # Here we call __init__("Sarah") i.e. name = "Sarah"
Here Person("Sarah")
creates an instance of the class by passing __init__()
the value Sarah
, this is then bound to name
and in turn, this is then set as the instance attribute self.name
.
2. __str__() Method
The __str__()
method is the method called when we try and print an object. It returns a str
(string).
class Person:
""" A simple person class """
def __init__(self, name):
self.name = name
if __name__ == "__main__":
person_one = Person("Sarah") # Here we call __init__("Sarah") i.e. name = "Sarah"
print(person_one) # prints something like <__main__.Person object at 0x7fdf15fe3640>
The default printing of an object is just its name and the memory location of the object.
Python knows to call the __str()__
method, but we can do it manually:
print(person_one.__str__()) # prints something like <__main__.Person object at 0x7fdf15fe3640>
We can choose to overwrite this to print something more sensible.
class Person:
""" A simple person class """
def __init__(self, name):
self.name = name
def __str__(self):
desc = "Instance of Person\n\n"
desc += f"Name: {self.name}"
return desc
if __name__ == "__main__":
person_one = Person("Sarah") # Here we call __init__("Sarah") i.e. name = "Sarah"
print(person_one)
Prints out:
Instance of Person
Name: Sarah
3. __eq__() Method
The __eq__()
method is the method called when we use the ==
operator.
We can make use of this for our Person
class. While this is a bit odd, it demonstrates how to use it. We will say an instance of a Person
is equal to another instance if the name
and age
are the same in both instances.
class Person:
""" A simple person class """
def __init__(self, name, age):
self.name = name
self.age = age
# test if the name and age are the same.
def __eq__(self, other):
# test if this instance (self) has the same name and age as other.
if self.name == other.name and self.age == other.age:
return True
else:
return False
if __name__ == "__main__":
person_one = Person("Sarah", 27)
person_two = Person("Sarah", 29)
person_three = Person("Sarah", 27)
print(person_one == person_two) # prints False because age is different
print(person_one == person_three) # prints True because name and age are the same
4. __add()__ Method
The last method we will look at (we also started with it) is __add__()
which is called when we use the +
operator.
Again we can make use of this for our Person
class. Again this is a bit odd, but it demonstrates how to use it. We will say that we add an instance of a Person
with another instance by adding the names and the ages and creating a new person.
Clearly this is nonsense, but it shows how we can add two instances of the same class
and get a new instance of a the same class
.
If we think about adding two ints, 1+2
will create a new int with value 3. This is essentially what we are doing here.
class Person:
""" A simple person class """
def __init__(self, name, age):
self.name = name
self.age = age
# add the ages of this instance and other
def __add__(self, other):
name = self.name + other.name
age = self.age + other.age
return Person(name, age) # clearly this is nonsense, but you are returning a new Person object by adding the other two person objects
if __name__ == "__main__":
person_one = Person("Sarah", 27)
person_two = Person("Bob", 35)
print(person_one + person_two) # prints 62
List of all Built-in Methods
You can find a comprehensive list of magic methods via the following link:
Tutorials Teacher - Magic Methods
=== TASK ===
Create a class called Point
.
Point
should have two attributes:
Attribute Name | Description |
---|---|
x | x-coordinate of the point |
y | y-cordinate of the point |
Make sure the attributes are named as per the table.
You should override the __init__()
, __str__()
, __add__()
, __sub__()
and __mul__()
methods.
__init__()
Method
You should be able to create a new point as follows.
point_one = Point(3,2) # creates a point with x=3, y=2
__str__()
Method
You should be able to print out a point as follows.
point_one = Point(3,2) # creates a point with x=3, y=2
print(point_one)
Should print:
Instance of Point
x: 3
y: 2
__add__()
Method
You should be able to add two points as follows. NOTE: This should return a new Point
object.
point_one = Point(3,2) # creates a point with x=3, y=2
point_two = Point(5,3) # creates a point with x=5, y=3
point_three = point_one + point_two # creates a new instance of point (object) x=8, y=5, i.e. Point(8,5)
print(point_three)
Should print:
Instance of Point
x: 8
y: 5
__sub__()
Method
You should be able to subtract two points as follows. NOTE: This should return a new Point
object.
point_one = Point(3,2) # creates a point with x=3, y=2
point_two = Point(5,3) # creates a point with x=5, y=3
point_three = point_one - point_two # creates a new instance of point (object) x=-2, y=-1, i.e. Point(-2,-1)
print(point_three)
Should print:
Instance of Point
x: -2
y: -1
__mul__()
Method
You should be able to multiply two points as follows. NOTE: This should return a new Point
object.
point_one = Point(3,2) # creates a point with x=3, y=2
point_two = Point(5,3) # creates a point with x=5, y=3
point_three = point_one * point_two # creates a new instance of point (object) x=15, y=6, i.e. Point(15,6)
print(point_three)
Should print:
Instance of Point
x: 15
y: 6
__eq__()
Method
You should be able to test if two points are equal as follows. NOTE: This should return a new bool
object.
point_one = Point(3,2) # creates a point with x=3, y=2
point_two = Point(5,2) # creates a point with x=5, y=2
point_three = Point(3,2) # creates a point with x=3, y=2
print(point_one == point_two) # prints False because x is different
print(point_one == point_three) # prints True because x and y are the same
Getting Started
You can copy and paste the following into a new Python file to get started.
class Point:
""" A simple representation of a point in 2d space"""
pass
if __name__ == "__main__":
point_one = Point(3,2)
print(point_one)
print()
point_two = Point(5,3)
print(point_two)
print()
point_three = point_one + point_two
print(point_three)
print()
point_four = point_one - point_two
print(point_four)
print()
point_five = point_one * point_two
print(point_five)
print()
print(point_one == point_two)
If your class is implemented correctly it will print out the following.
Instance of Point
x: 3
y: 2
Instance of Point
x: 5
y: 3
Instance of Point
x: 8
y: 5
Instance of Point
x: -2
y: -1
Instance of Point
x: 15
y: 6
False
References
Tutorials Teacher - Magic Methods
Class Attributes vs Instance Attributes
So far our attributes have belonged to the instance of a class (object).
We can do something else and make an attribute that belongs to the class (type) itself.
1. Instance Attributes
Instance attributes are those that belong to the instance of a class (object). An instance attribute is attached to the instance, by convention, using the word self
.
There are used to access the data within a given instance of the class.
You will also hear them referred to as member variables or member fields, especially in other languages.
For example:
class Pet:
""" A pet class """
def __init__(self, name, age):
self.name = name
self.age = age
if __name__ == "__main__":
pet_one = Pet("Fidget", 12)
pet_two = Pet("Rex", 5)
# access the pet_one instance attribute name
print(pet_one.name) # prints Fidget
print(vars(pet_one)) # print pet_one instance attributes
print(vars(pet_two)) # print pet_two instance attributes
Prints out:
Fidget
{'name': 'Fidget', 'age': 12}
{'name': 'Rex', 'age': 5}
You will notice that we printed out vars(pet_one)
and vars(pet_two)
.
vars()
returns the __dict__
(dictionary mapping) attribute of the given object. __dict__
is a dictionary that stores the attributes associated with the instance of the class (object). So pet_one.__dict__
stores the attributes associated with pet_one
and pet_two.__dict__
stores the attributes associated with pet_two
.
You can see that both contain name
and age
but they have different data because they are separate instances of the class Pet
.
2. Class Attributes
As well as having attributes that belong to instances of the class, we can also have attributes that belong to the class.
This simple example now defines a no_pets
attribute that belongs to the class, it will track how many instances have been created, i.e. the number of pets.
Notice that it is not attached using self
.
class Pet:
""" A pet class """
# define a class variable
no_pets = 0 # number of pets
def __init__(self, name, age):
self.name = name
self.age = age
# increment class attribute by 1
Pet.no_pets += 1
if __name__ == "__main__":
pet_one = Pet("Fidget", 12)
pet_two = Pet("Rex", 5)
# access the class attribute directly
print(Pet.no_pets) # prints 2
pet_three = Pet("Indie", 7)
# access the class attribute via instance
print(pet_one.no_pets) # prints 3
print(pet_two.no_pets) # prints 3
# access the class attribute directly
print(Pet.no_pets) # prints 3
Prints out:
2
3
3
3
Each time a new instance is created, the __init__()
method (constructor) is called. This then increments the class attribute by 1
using Pet.no_pets += 1
. Notice that instead of using self
, we used the class name Pet
.
The example above demonstrates that you can access a class attribute via either an instance or the class itself.
We can list all the attributes and methods of the class using vars()
.
print(vars(Pet))
You will see this has an attribute no_pets
, confirming it is a class attribute. You can look up the mappingproxy, but it is sort of a wrapped read-only version of the dictionary. Just see it as a dictionary unless you are overly interested.
mappingproxy({'__module__': '__main__', '__doc__': ' A pet class ', 'no_pets': 3, '__init__': <function Pet.__init__ at 0x7ff0bd764b80>, '__dict__': <attribute '__dict__' of 'Pet' objects>, '__weakref__': <attribute '__weakref__' of 'Pet' objects>})
3. Instance Namespace vs Class Namespace
Beneath the hood, Python is using something called namespaces.
"A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future.
Examples of namespaces are the set of built-in names (containing functions such as abs(),
and built-in exception names); the global names in a module; and the local names in a function invocation. In a sense, the set of attributes of an object also forms a namespace.
The important thing to know about namespaces is that there is no relation between names in different namespaces; for instance, two different modules may both define a function maximize without confusion — users of the modules must prefix it with the module name."
In the following example, we access the class attribute no_pets
via the instance and the class.
class Pet:
""" A pet class """
# define a class variable
no_pets = 0 # number of pets
def __init__(self, name):
self.name = name
# increment class attribute by 1
Pet.no_pets += 1
if __name__ == "__main__":
pet_one = Pet("Fidget", 12)
# access the class attribute via instance
print(pet_one.no_pets)
# access the class attribute directly
print(Pet.no_pets) # prints 3
What is happening here is that when we try and access it using pet_one.no_pets
, Python looks in the instance namespace, that is it looks in pet_one.__dict__
. When it fails to find this, it then looks in the class namespace, that is Pet.__dict__
.
The instance namespace always takes precedence over the class namespace.
Do not do the following, it is just an example to illustrate a point.
We can do the following, define a class attribute and an instance attribute with the same name, e.g. name
.
class Pet:
""" A pet class """
# define a class variable
name = "Pet" # number of pets
def __init__(self, name):
self.name = name
# increment class attribute by 1
if __name__ == "__main__":
pet_one = Pet("Fidget")
# access the class attribute via instance
print(pet_one.name)
# access the class attribute directly
print(Pet.name)
Which will print:
Fidget
Pet
This is because the instance pet_one
looks at its namespace first and finds name
, so it uses that.
You can check by printing out the instance and the class namespaces.
print(vars(pet_one))
Prints outs,
{'name': 'Fidget'}
and,
print(vars(Pet))
prints out:
mappingproxy({'__module__': '__main__', '__doc__': ' A pet class ', 'name': 'Pet', '__init__': <function Pet.__init__ at 0x7fa1c82aeaf0>, '__dict__': <attribute '__dict__' of 'Pet' objects>, '__weakref__': <attribute '__weakref__' of 'Pet' objects>})
=== TASK ===
Create a class called Circle
.
Your class should have one instance attribute named radius
.
It should also have a class attribute called pi
set to a value of 3.14159
.
print(Circle.pi) # prints 3.14159
Also, create two methods called area()
and circumference()
.
area()
Method
You can calculate the area using pi * radius**2
. Remember that pi
will be a class attribute and radius an instance attribute, so you will have to amend the formula appropriately.
circle_one = Circle(10) # create an instance of a Circle with radius 10
print(circle_one.area())
circle_two = Circle(5) # create an instance of a Circle with radius 5
print(circle_two.area())
314.159
78.53975
circumference()
Method
You can calculate the area using 2 * pi * radius
. Again, remember that pi
will be a class attribute and radius an instance attribute, so you will have to amend the formula appropriately.
circle_one = Circle(10) # create an instance of a Circle with radius 10
print(circle_one.circumference())
circle_two = Circle(5) # create an instance of a Circle with radius 5
print(circle_two.circumference())
62.8318
31.4159
Getting Started
You can get started by copying and pasting the following into a new Python file.
class Circle:
pass
if __name__ == "__main__":
c1 = Circle(10) # create an instance of a Circle with radius 10
print(c1.area())
print(c1.circumference())
c2 = Circle(5) # create an instance of a Circle with radius 5
print(c2.area())
print(c2.circumference())
Instance Methods vs Static Methods
To date we have been using instance methods, these methods are bound to an instance of a class (object) and can access the attributes attached to that instance.
In this lesson, we will take a look at another type, static methods.
Instance Methods
Instance methods are methods that are bound to a given instance of a class (object) and therefore have access to the instance data, the data attached to self
.
The first parameter, by convention, should be self
which is a reference to the instance.
For example,
class Pet:
""" A pet class """
def __init__(self, name, age):
## instance attributes
self.name = name
self.age = age
# instance method, self should be the first parameter
def talk(self):
print(f"Hi, my name is {self.name} and I am {self.age} years old.")
if __name__ == "__main__":
pet_one = Pet("Fidget", 17)
pet_one.talk()
will print out:
Hi, my name is Fidget and I am 17 years old.
We can also have other parameters in a method, but self
is still the first parameter. For example, we can add another parameter n
to talk()
, this will actually be the first parameter of the method call.
For example,
class Pet:
""" A pet class """
def __init__(self, name, age):
## instance attributes
self.name = name
self.age = age
# instance method, self should be the first parameter
def talk(self, n):
for _ in range(n):
print(f"Hi, my name is {self.name} and I am {self.age} years old.")
if __name__ == "__main__":
pet_one = Pet("Fidget", 17)
pet_one.talk(3) # call the method pet_one with n=3
will print out:
Hi, my name is Fidget and I am 17 years old.
Hi, my name is Fidget and I am 17 years old.
Hi, my name is Fidget and I am 17 years old.
We say talk()
takes 1 parameter, self
is not included.
Static Methods
A static method is a method that does not have access to the instance attributes, but it can access a class attribute (read-only). It is bound to the class. Therefore, we do not need an instance of the class to use the method
Here we have a single class attribute no_pets
that tracks the number of pets (instances) of Pet
created. We also create a static method called print_no_pets
, which uses the class attribute.
class Pet:
""" A pet class """
# define a class attribute
no_pets = 0
def __init__(self, name, age):
self.name = name
self.age = age
# increment class attribute by 1
Pet.no_pets += 1
@staticmethod
def print_no_pets():
print(f"There are {Pet.no_pets} pets.")
if __name__ == "__main__":
# call the static method on the class
Pet.print_no_pets() # prints There are 0 pets.
pet_one = Pet("Fidget", 12)
pet_two = Pet("Rex", 5)
pet_three = Pet("Indie", 7)
# call the static method on the class
Pet.print_no_pets() # prints There are 3 pets.
# call the static method via the instance
pet_one.print_no_pets() # prints There are 3 pets.
You will notice that we have used the decorator @staticmethod
. This tells the method that the first parameter should not be treated as the instance reference (normally self
).
We also did not need an instance of the class to call the method, but we could also call the method using an instance. This is the same as with class attributes, please read the part about instance and class namespaces in the Class Attribute lesson.
Here is another example of a Mathematics
class, normally we would do this as a module in Python, but in other languages such as Java and C#, classes like this with static methods are quite common. Normally referred to as a utility (helper) class.
It is also a pretty useless class as all of these are built into Python.
The point is we do not need an instance of the class to use it!
class Mathematics:
@staticmethod
def add(x,y):
return x + y
@staticmethod
def sub(x,y):
return x - y
@staticmethod
def pow(x,n):
return x**n
if __name__ == "__main__":
print(Mathematics.add(10,5)) # prints 15
print(Mathematics.sub(10,5)) # prints 5
print(Mathematics.pow(10,3)) # prints 100
=== TASK ===
Create a class called Person
. This should have a first_name
, surname
and age
attribute.
Create a static method called format_name_comma()
. The reason we want this to be static is we can imagine that it has a use even when we don't have an instance of the class.
You should be able to invoke format_name_comma()
as follows:
print(Person.format_name_comma("Joe", "Bloggs")) # prints Bloggs, Joe
You should also implement the __str__()
method which should return the formatted name using the instance attributes first_name
and surname
. Really you should reuse the static method format_name_comma()
from within __str__()
.
You should then be able to run the following code.
person_one = Person("Joe", "Bloggs", 27)
print(person_one) # prints Bloggs, Joe
Getting Started
You can copy and paste the following into a new Python file to get started.
class Person:
pass
if __name__ == "__main__":
print(Person.format_name_comma("Joe", "Bloggs")) # prints Bloggs, Joe
person_one = Person("Joe", "Bloggs", 27)
print(person_one) # prints Bloggs, Joe
The Critter Program
The following is a very simple program that demonstrates creating an instance of a class.
The class also contains:
__init__()
- constructor for the class.total
- a class attribute which tracks the number of instances of the class created.status
- a static method that prints out how many instances of the class are created using the class attributetotal
.
class Critter:
"""A virtual critter class"""
total = 0
def __init__(self, name):
print("A critter has been born!")
self.name = name
Critter.total += 1
# this stops the first argument being the instance
@staticmethod
def status():
print("\nThe total number of critters is", Critter.total)
def main():
print("Accessing the class attribute Critter.total:", end=" ")
print(Critter.total)
print("\nCreating critters.")
crit_one = Critter("critter 1")
crit_two = Critter("critter 2")
crit_three = Critter("critter 3")
Critter.status()
print("\nAccessing the class attribute through an object:", end= " ")
print(crit_one.total)
input("\n\nPress the enter key to exit.")
if __name__ == "__main__":
main()
Encapsulation
In this lesson, we will learn about encapsulation and how to implement it in Python. Python differs from C-style languages in its implementation of this concept.
We will explore:
- Encapsulation
- Private Attributes and Private Methods
- Getters and Setters
- Properties
The Python way to do things now is properties, so please read the guidance at the end of the lesson on how to design classes.
1. Encapsulation
We say that an instance of a class (object) “can take care of itself”. In other words, you can ask an instance to do things (by invoking its methods), but you don’t have to know or care how the instance does it. You communicate with the instance – i.e., tell it to do things, or retrieve or change its data – through public methods and (in C# and some but not all other object-oriented languages) properties. The internal implementation details of the instance are kept hidden (i.e. private). This hiding of the internal implementation and data is often known as encapsulation.
Python has its interpretation and implementation of this concept. Different, but neither right nor wrong!
You should imagine your class as having publicly exposed things that let you interact with an instance. This is known as the classes API (application programming interface).
1.1 An Insightful Analogy
An analogy is that you have 5 senses that allow you to interact with the world, for example, I can communicate with you with sound (ear) or visually (eyes), but I can't directly access your brain. In a way, your senses are like public methods exposed to the outside world, but your brain and other internal parts are private, they are used internally by your body.
2. Private Attributes and Private Methods
In Python there is no such thing as privacy, everything is public, and you can always access it. So no point in this section right?
Well not quite. Python has two ways of doing this.
- Use one leading underscore only for non-public methods and instance variables.
- To avoid name clashes with subclasses, use two leading underscores to invoke Python’s name-mangling rules.
No 2. does not make sense in this unit as we have not yet introduced inheritance. We will come back to this when we do.
2.1 Single Leading Underscore (Attribute)
By convention when you want to indicate that an attribute or method should only be used within a class itself, you name it with a single leading underscore.
This is directly from the official Python documentation.
"a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API"
Here is an example that represents a student that uses a single leading underscore to indicate that _first_name
, _surname
and _id
are private and should not be accessed or edited outside the class. We leave age
and email
as public as we want to access these from outside the class.
We also include a public method (no leading underscore) that returns the full name of the student.
class Student:
def __init__(self, first_name, surname, age, email, id):
self._first_name = first_name
self._surname = surname
self.age = age
self.email = email
self._id = id
def get_full_name(self):
return f"{self._first_name} {self._surname}"
if __name__ == "__main__":
student_one = Student("Ada", "Lovelace", 36, "a.lovelace@derby.ac.uk", "10010101")
# DO NOT DO THIS! You can, but the convention is this is now a non-public (private) attribute
print(student_one._id) # prints 10010101
print(student_one._first_name) # prints Ada
# DO NOT DO THIS! You can, but now you are messing with something that should only be messed with inside the class!
student_one._id = "12345678" # overwrites the non-public (private) attribute
print(student_one.get_full_name())
Here the public API for the class is the instance attributes age
and email
and the instance method get_full_name()
.
Question: Should age
be public?
3. Getters and Setters
How do we give access to our private attributes then?
The answer in most languages is to use getters (accessors) and setters (mutators). These are methods that get the value of our private attribute and set the value of our private attribute. They do this internally.
The following is an example of a class that represents a food item on a menu for a restaurant.
Here we are happy to let the title
be updated and thus we leave it public. However, we have a condition on price
, that is it cannot be less than a pound.
We can create a getter and setter method that manages a non-public (private) attribute _price
.
We can access _price
via the method get_price()
and we can set the _price
via the method set_price()
.
set_price()
takes in a single parameter value
(remember self
is just the instance reference) and checks the value
to see if it is less than 1
, if it is it raises a ValueError
.
class FoodItem:
def __init__(self, title, price):
self.title = title
# use the setter to set the _price
self.set_price(price)
# getter
def get_price(self):
print("Getting value...")
return self._price
# setter
def set_price(self, value):
print("Setting value...")
if value < 1:
raise ValueError("Cannot set a price less than 1")
self._price = value
if __name__ == "__main__":
food_one = FoodItem("Smoked Salmon on Toast", 8.99)
print(food_one.get_price())
food_one.set_price(6.99) # getting cheaper
print(food_one.get_price())
food_one.set_price(0.5) # too cheap, raises an error!
print(food_one.get_price())
will print out:
Setting value...
Getting value...
8.99
Setting value...
Getting value...
6.99
Setting value...
Traceback (most recent call last):
File "main.py", line 24, in <module>
food_one.set_price(-2) # too cheap, raises an error!
File "main.py", line 16, in set_price
raise ValueError("Cannot set a negative price")
ValueError: Cannot set a negative price
This demonstrates the nature of encapsulation, we control the private variables through the public getters and setters. This means that if we change things in the future. People interacting with our class can still just call get_price()
and set_price()
.
For example, we decide to lower our limit on the price to 0
. We update our set_price()
method as follows:
# setter
def set_price(self, value):
print("Setting value...")
if value < 0:
raise ValueError("Cannot set a negative price")
self._price = value
The code would now print out:
Setting value...
Getting value...
8.99
Setting value...
Getting value...
6.99
Setting value...
Getting value...
0.5
4. Properties
Python allows us to do something much neater using properties.
Here we can create a property
with our getter and setter methods that act like an attribute.
class FoodItem:
def __init__(self, title, price):
self.title = title
# use the property (actually calls the method set_price) to set the _price
self.price = price
# getter
def get_price(self):
print("Getting value...")
return self._price
# setter
def set_price(self, value):
print("Setting value...")
if value < 1:
raise ValueError("Cannot set a price less than 1")
self._price = value
# creating a property object, this is key
price = property(get_price, set_price)
if __name__ == "__main__":
food_one = FoodItem("Smoked Salmon on Toast", 8.99)
# we can now use price like an attribute
# it is actually calling get_price()
print(food_one.price)
# we can now use price like an attribute
# it is actually calling set_price()
food_one.price = 6.99 # getting cheaper
print(food_one.price)
food_one.price = 0.5 # too cheap, raises an error!
print(food_one.price)
Now food_one.price
looks and acts like an attribute returning a value, but it is calling get_value()
. Similarly, food_one.price = 6.99
is acting like an attribute, but it is really calling set_value(6.99)
.
4.1 The Python Way (Decorators)
Python lets us do this in a more Pythonic way with a decorator.
We call our getter and setter the same as the attribute. For example, our private attribute is _price
, so our getter and setter are both called price
. We also put @property
above the getter and @price.setter
above the setter.
Make sure you look at the code in if __name__ == "__main__"
as we can now access our getters and setters using .price
as if they are attributes! e.g. food_one.price = 10.99
. As far as the person using the class is concerned, they just use them as attributes with no idea of the internal working.
The beauty of this is that you can internally change your properties (getter/setter methods) and not affect the code that uses them. They still access them with the attribute name, e.g. .price
.
class FoodItem:
def __init__(self, title, price):
self.title = title
# use the property (actuall calls the method set_price) to set the _price
self.price = price
# getter use the name of the attribute
@property
def price(self):
print("Getting value...")
return self._price
# setter uses the name of the attribute
@price.setter
def price(self, value):
print("Setting value...")
if value < 0:
raise ValueError("Cannot set a negative price")
self._price = value
if __name__ == "__main__":
food_one = FoodItem("Smoked Salmon on Toast", 8.99)
print(food_one.price)
food_one.price = 6.99 # getting cheaper
print(food_one.price)
food_one.price = 0.5 # too cheap, raises an error!
print(food_one.price)
What is really cool is that you can use the +=
and -=
operators.
Try this out:
if __name__ == "__main__":
food_one = FoodItem("Smoked Salmon on Toast", 8.99)
print(food_one.price)
food_one.price += 1 # getting cheaper
print(food_one.price)
food_one.price += -10
print(food_one.price)
You will not get the following.
Setting value...
Getting value...
8.99
Getting value...
Setting value...
Getting value...
9.99
Getting value...
Setting value...
Traceback (most recent call last):
File "main.py", line 26, in <module>
food_one.price += -10
File "main.py", line 18, in price
raise ValueError("Cannot set a negative price")
ValueError: Cannot set a negative price
We can also create properties that don't directly manage a single attribute. For example,
class Student:
def __init__(self, first_name, surname, age, email, id):
self._first_name = first_name
self._surname = surname
self.age = age
self.email = email
self._id = id
# this is now a property that we can access with .full_name
@property
def full_name(self):
return f"{self._first_name} {self._surname}"
if __name__ == "__main__":
student_one = Student("Ada", "Lovelace", 36, "a.lovelace@derby.ac.uk", "10010101")
# DO NOT DO THIS! You can, but the convention is this is now a non-public (private) attribute
print(student_one._id) # prints 10010101
print(student_one._first_name) # prints Ada
# DO NOT DO THIS! You can, but now you are messing with something that should only be messed with inside the class!
student_one._id = "12345678" # overwrites the non-public (private) attribute
# print the full name via the property
print(student_one.full_name)
5. Guidance for Designing Classes
The following guidance is given on Real Python.
Because of properties, Python developers tend to design their classes’ APIs using a few guidelines:
- Use public attributes whenever appropriate, even if you expect the attribute to require functional behaviour in the future.
- Avoid defining setter and getter methods for your attributes. You can always turn them into properties if needed.
- Use properties when you need to attach behaviour to attributes and keep using them as regular attributes in your code.
- Avoid side effects in properties because no one would expect operations like assignments to cause any side effects. By side effects we mean slow operations, you expect an assignment like
food_one.price = 10.99
to be instantaneous. For slow operations use getters and setters.
=== TASK ==
Create a class called Position
that manages the position x
and y
.
Your constructor should take in the initial position of x
and of y
and upper limits for x
and y
and then use properties to manage x
and y
so that they cannot be set above these limits. Note you will need a property (getter/setter) for both x
and y
.
If an attempt to assign a value above the limit is made then it should raise a ValueError
.
Use the following examples as a reference. Note the message of the ValueError in both.
p = Position(0,0,10,10) # x=0, y=0, upper limits of 10 and 10
print(f"x={p.x} and y={p.y}") # prints x=0 and y=0
p.x = 2
print(f"x={p.x} and y={p.y}") # prints x=2 and y=0
p.y += 3
print(f"x={p.x} and y={p.y}") # prints x=2 and y=3
p.x = 11 # raises ValueError: x cannot be bigger than 10
p = Position(0,0,10,15) # x=0, y=0,
print(f"x={p.x} and y={p.y}") # prints x=0 and y=0
p.x = 2
print(f"x={p.x} and y={p.y}") # prints x=2 and y=0
p.y += 3
print(f"x={p.x} and y={p.y}") # prints x=2 and y=3
p.y += 13 # raises ValueError: y cannot be bigger than 15
Getting Started
You can copy and paste the following into a new Python file to get started.
class Position:
pass
if __name__ == "__main__":
p = Position(0,0,10,10) # x=0, y=0,
print(f"x={p.x} and y={p.y}") # prints x=0 and y=0
p.x = 2
print(f"x={p.x} and y={p.y}") # prints x=2 and y=0
p.y += 3
print(f"x={p.x} and y={p.y}") # prints x=2 and y=3
p.x = 11 # raises ValueError: x cannot be bigger than 10
References
The Critter Caretaker Program
This program is from Chapter 8: Python Programming for the Absolute Beginner, Third Edition - Michael Dawson. It has some minor amendments.
It extends the previous critter program to allow us to interact with the critter. The idea is that the critter can get hungry and bored which makes them frustrated or mad.
We can then feed or play with our critter to improve its hunger and boredom to improve its mood.
Our new Critter
class adds instance attributes hunger
and boredom
to each critter, this allows them to have state. We can then manage and interact with this state through our program.
Create a file called critter.py and copy in the following:
#critter.py
# A virtual pet to care for
class Critter(object):
"""A virtual pet"""
def __init__(self, name, hunger=0, boredom=0):
self.name = name
self.hunger = hunger
self.boredom = boredom
def __str__(self):
desc = ""
desc += f"Name: {self.name}\n"
desc += f"Hunger: {self.hunger}\n"
desc += f"Boredom: {self.boredom}\n"
return desc
# indicate that the method should only be used internally
# using a single leading underscore
def _pass_time(self):
self.hunger += 1
self.boredom += 1
# indicate that the method should only be used internally
# using a single leading underscore
def _mood(self):
unhappiness = self.hunger + self.boredom
if unhappiness < 5:
m = "happy"
elif 5 <= unhappiness <= 10:
m = "okay"
elif 11 <= unhappiness <= 15:
m = "frustrated"
else:
m = "mad"
return m
def talk(self):
print(f"I'm {self.name} and I feel {self._mood()} now.\n")
self._pass_time()
def eat(self, food=5):
print("Brruppp. Thank you.")
self.hunger -= food
if self.hunger < 0:
self.hunger = 0
self._pass_time()
def play(self, fun=5):
print("Wheee!")
self.boredom -= fun
if self.boredom < 0:
self.boredom = 0
self._pass_time()
You will see a bunch of instance methods that allow us to interact with our critter. You should spend some time trying to understand what this class is doing.
Our main.py file contains the following:
from critter import Critter
def main():
crit_name = input("What do you want to name your critter?: ")
crit = Critter(crit_name)
choice = None
while choice != "0":
print \
("""
Critter Caretaker
0 - Quit
1 - Listen to your critter
2 - Feed your critter
3 - Play with your critter
4 - Print Critter Stats (does not pass time)
""")
choice = input("Choice: ")
print()
# exit
if choice == "0":
print("Good-bye.")
# listen to your critter
elif choice == "1":
crit.talk()
# feed your critter
elif choice == "2":
crit.eat()
# play with your critter
elif choice == "3":
crit.play()
# print critter stats
elif choice == "4":
print(crit)
# some unknown choice
else:
print(f"\nSorry, but {choice} isn't a valid choice.")
if __name__ == "__main__":
main()
The Critter Zoo Program
This program extends the previous critter caretaker program to allow us to manage lots of critters, we create a critter zoo!
We can then feed or play with our critters to improve their hunger and boredom to improve their mood.
Our new CritterZoo
class has an attribute called critters
which is a dictionary that stores each of the Critter
instances.
We can then interact with individual Critter
instances through this dictionary.
In the next unit, we will see a different way of doing this by extending Python's built-in dictionary type dict
.
Create a file called critter.py and copy in the following:
# critter.py
# A virtual pet to care for
class Critter:
"""A virtual pet"""
total = 0
def __init__(self, name, hunger=0, boredom=0):
self.name = name
self.hunger = hunger
self.boredom = boredom
Critter.total += 1
def __str__(self):
desc = ""
desc += f"Name: {self.name}\n"
desc += f"Hunger: {self.hunger}\n"
desc += f"Boredom: {self.boredom}\n"
return desc
def pass_time(self):
self.hunger += 1
self.boredom += 1
if self.hunger > 16:
self.hunger = 16
if self.boredom > 16:
self.boredom = 16
# indicate that the method is internal (non-public)
def _mood(self):
unhappiness = self.hunger + self.boredom
if unhappiness < 5:
m = "happy"
elif 5 <= unhappiness <= 10:
m = "okay"
elif 11 <= unhappiness <= 15:
m = "frustrated"
else:
m = "mad"
return m
def talk(self):
print(f"I'm {self.name} and I feel {self._mood()} now.\n")
def eat(self, food=5):
print("Brruppp. Thank you.")
self.hunger -= food
if self.hunger < 0:
self.hunger = 0
def play(self, fun=5):
print("Wheee!")
self.boredom -= fun
if self.boredom < 0:
self.boredom = 0
class CritterZoo:
""" A critter zoo"""
def __init__(self, no_critters):
self.critters = {}
for i in range(no_critters):
crit = Critter(f"Critter {i+1}")
self.critters[f"Critter {i+1}"] = crit
def __str__(self):
returnstring = ""
for critter in self.critters.values():
returnstring += critter.__str__()
return returnstring
def feed_critter(self, name):
self.critters[name].eat()
self._pass_time()
def play_with_critter(self, name):
self.critters[name].play()
self._pass_time()
def list_critter_names(self):
for name in self.critters.keys():
print(name)
def list_critter_moods(self):
for critter in self.critters.values():
critter.talk()
self._pass_time()
# indicate that the method is internal (non-public)
def _pass_time(self):
for critter in self.critters.values():
critter.pass_time()
Take some time to look at the CritterZoo
class and some of the amendments made to the Critter
class. For, example pass_time()
is now public and is called from within the CritterZoo
class so that we can pass the time for all of the critters in one go.
Our main.py file contains the following:
# main.py
# Critter Zoo
from critter import CritterZoo
def main():
no_critters = input("How many critters would you like?: ")
zoo = CritterZoo(int(no_critters))
choice = None
while choice != "0":
print \
("""
Critter Caretaker
0 - Quit
1 - Listen to your critters
2 - Feed a critter
3 - Play with a critter
4 - List critters (does not pass time)
""")
choice = input("Choice: ")
print()
# exit
if choice == "0":
print("Good-bye.")
# listen to your critter
elif choice == "1":
zoo.list_critter_moods()
# feed your critter
elif choice == "2":
print("Which critter do you want to feed?")
zoo.list_critter_names()
crit_num = input("Please enter the number you wish to feed.\n")
zoo.feed_critter(f"Critter {crit_num}")
# play with your critter
elif choice == "3":
print("Which critter do you want to play with?")
zoo.list_critter_names()
crit_num = input("Please enter the number you wish to play with.\n")
zoo.play_with_critter(f"Critter {crit_num}")
# list critters
elif choice == "4":
print(zoo)
else:
print(f"\nSorry, but {choice} isn't a valid choice.")
if __name__ == "__main__":
main()
Unit 10 - Object Oriented Programming 2 (OOP)
The following unit is about some more advanced features of object-oriented programming (OOP).
It will cover:
- Sending and Receiving Messages
- Combining Objects
- Inheritance
- Polymorphism
- Modules and Classes
There will also be a follow along project that builds an OOP version of Blackjack.
Sending and Receiving Messages
So far we have had classes that sort of don't interact with each other.
Now we will start to get these classes to combine into a sort of ecosystem of objects that talk to each other.
We will start with a simple example based on the Critter Zoo Program.
We will first create a simple class called Critter
which will contain instance attributes name
and no_feed
(number of times fed), and an instance method feed()
.
class Critter:
""" A very simple critter class"""
def __init__(self, name):
self.name = name
self.no_feeds = 0
def __str__(self):
desc = ""
desc += "Instance of Critter\n"
desc += f"Name: {self.name}\n"
desc += f"Fed: {self.no_feeds} times\n"
return desc
def feed(self):
print("Yummy! Thanks! Brruppp...")
self.no_feeds += 1
We will also create a class called Caretaker
which represents a person who looks after critters.
class Caretaker:
""" A very simple critter class"""
def __init__(self, name):
self.name = name
def __str__(self):
desc = ""
desc += "Instance of Caretaker\n"
desc += f"Name: {self.name}\n"
return desc
def feed_criter(self, critter):
critter.feed()
The key method here is feed_critter()
, this is now a method that takes in an instance of Critter
and then feeds the little thing by calling the instance method feed()
.
You can think about it like this, feed_critter()
takes in a Critter
instance and then sends a message to the Critter
instance by invoking (calling) its method feed()
. The Critter
instance in turn receives the message because its method has been invoked, it will then run whatever code is in feed()
.
Here is the whole program in action to show you how it works.
class Critter:
""" A very simple critter class"""
def __init__(self, name):
self.name = name
self.no_feeds = 0
def __str__(self):
desc = ""
desc += "Instance of Critter\n"
desc += f"Name: {self.name}\n"
desc += f"Fed: {self.no_feeds} times\n"
return desc
def feed(self):
print("Yummy! Thanks! Brruppp...")
self.no_feeds += 1
class Caretaker:
""" A very simple critter class"""
def __init__(self, name):
self.name = name
def __str__(self):
desc = ""
desc += "Instance of Caretaker\n"
desc += f"Name: {self.name}\n"
return desc
def feed_criter(self, critter):
critter.feed()
if __name__ == "__main__":
alan = Critter("Alan")
ava = Critter("Ava")
print(alan) # print out alan
print(ava) # print out ava
bob = Caretaker("Bob")
bob.feed_criter(alan) # feed alan
bob.feed_criter(alan) # feed alan
bob.feed_criter(alan) # feed alan
bob.feed_criter(ava) # feed ava
print()
print(alan) # print out alan
print(ava) # print out ava
And the output.
Instance of Critter
Name: Alan
Fed: 0 times
Instance of Critter
Name: Ava
Fed: 0 times
Yummy! Thanks! Brruppp...
Yummy! Thanks! Brruppp...
Yummy! Thanks! Brruppp...
Yummy! Thanks! Brruppp...
Instance of Critter
Name: Alan
Fed: 3 times
Instance of Critter
Name: Ava
Fed: 1 times
We can see that we have asked the instance of Caretaker
, bob
, to do the feeding. We pass in either alan
or ava
, and bob
will feed them by invoking (calling) their feed()
method.
=== TASK ===
Create a Plumber
class and a Roomba
class.
You might want to look at a Goomba...
An instance of Plumber
should be able to squash the Roomba
.
The Roomba
class should have an attribute name
and an instance method called squish()
. It should also have a non-public attribute _squashed
which should be set to False
when the instance is created (i.e. in the __init__()
method).
Invoking the method squish()
should set _squashed
to True
.
You should also override the __str__()
method so that it prints out Hi my name is NAME and I am feeling fine
or Hi my name is NAME and I am squashed
depending on the value of _squashed
.
hetti = Roomba("Hetti")
print(hetti) # Hi my name is Hetti and I am feeling fine
hetti.squish()
print(hetti) # Hi my name is Hetti and I am squashed
The Plumber
class should have a name
and an instance method called squash()
.
The squash()
method for the Plumber
class should accept a Roomba
as a parameter. squash()
should then invoke (call) the Roomba
's instance method squish()
.
Here is an example of how the two classes should work.
hetti = Roomba("Hetti")
olga = Roomba("Olga")
bob = Roomba("Bob")
print(hetti) # Hi my name is Hetti and I am feeling fine
print(olga) # Hi my name is Olga and I am feeling fine
print(bob) # Hi my name is Bob and I am feeling fine
merio = Plumber("Merio")
merio.squash(olga) # ask merio to squash olga
print(hetti) # Hi my name is Hetti and I am feeling fine
print(olga) # Hi my name is Olga and I am squashed
print(bob) # Hi my name is Bob and I am feeling fine
Which would output the following to the terminal.
Hi my name is Hetti and I am feeling fine
Hi my name is Olga and I am feeling fine
Hi my name is Bob and I am feeling fine
Hi my name is Hetti and I am feeling fine
Hi my name is Olga and I am squashed
Hi my name is Bob and I am feeling fine
Getting Started
You can get started by copying and pasting the following into main.py.
class Roomba:
pass
class Plumber:
pass
def main():
hetti = Roomba("Hetti")
olga = Roomba("Olga")
bob = Roomba("Bob")
print(hetti) # Hi my name is Hetti and I am feeling fine
print(olga) # Hi my name is Olga and I am feeling fine
print(bob) # Hi my name is Bob and I am feeling fine
merio = Plumber("Merio")
merio.squash(olga)
print(hetti) # Hi my name is Hetti and I am feeling fine
print(olga) # Hi my name is Olga and I am squashed
print(bob) # Hi my name is Bob and I am feeling fine
if __name__ == "__main__":
main()
Composition and Aggregation
This lesson briefly introduces the fundamental concept of association in object-oriented programming that allows us to create connections between objects.
We often refer to classes as composite data types in that they are made up of lots of different data types, whether they are primitive data types like int
, float
, str
, bool
or user-defined data types (classes in Python).
There are two types of associations:
- Composition
- Aggregation
1. Composition
Composition is one of the fundamental concepts of object-oriented programming (OOP). It is often confused with inheritance which we will talk about in the next unit.
The idea of composition is that you can make up an object from other objects. Think about a car, a car is an object, but it has an engine, it has a gearstick, and it has some seats.
In a sense, the car is now a composition of all these other objects. Those in turn might also be composed of other objects, an engine certainly has many parts.
A good rule of thumb is to say out loud "a car has an engine" and see if it makes sense. Yes, a car does have an engine. If you can say X has a Y, and if Y cannot exist without X, then you should probably use composition. If Y can exist without X, then you probably need aggregation. It is often said to have ownership over the other object.
A good example of this is a room in a house. Here X is the house and Y is the room. Clearly a house has a room but a room cannot exist without the house.
Here we define two classes, an Engine
class which has a horse_power
attribute of type int
and a Car
which has an engine
attribute of type Engine
.
So an instance of Car
is now made up of an instance of another class.
Importantly we pass a horse_power
value to the constructor of Car
and then construct the instance of an Engine
within the Car
instance. Crucially this means that the Engine
instance belongs to the Car
instance, it is said to be tightly coupled. This means if you delete the Car
instance, the Engine
instance is deleted as well.
Here is the example:
class Car:
def __init__(self, horse_power):
self.engine = Engine(horse_power)
def __str__(self):
return f"A car that has: \n{self.engine}"
class Engine:
def __init__(self, horse_power):
self.horse_power = horse_power
def __str__(self):
return f"Engine with {self.horse_power} bhp"
if __name__ == "__main__":
car_one = Car(200)
print(car_one.engine) # prints Engine with 200 bhp
print(car_one)
# prints
# A car that has:
# Engine with 200 bhp
del car_one
When we run the code, we create an instance of Car
which in turn has a reference to an instance of Engine
, you can see this visualised using Python Tutor. When we delete the instance car_one
, both are removed from memory.
Before deletion of the Car
instance car_one
.
After deletion of the Car
instance car_one
.
2. Aggregation
Aggregation is very similar to composition except that the object is not explicitly tied to the instance, it is loosely coupled.
You can think of it using the object, but not owning it.
Here we pass an instance of an Engine
to the Car
constructor, this will act the same apart from when you delete the instance of the Car, the engine will remain in memory, it exists in its own right!
Which is the right choice depends on the objects and what they represent and how they are associated.
class Car:
def __init__(self, engine):
self.engine = engine
def __str__(self):
return f"A car that has: \n{self.engine}"
class Engine:
def __init__(self, horse_power):
self.horse_power = horse_power
def __str__(self):
return f"Engine with {self.horse_power} bhp"
if __name__ == "__main__":
engine_one = Engine(200)
car_one = Car(engine_one)
print(car_one.engine) # prints Engine with 200 bhp
print(car_one)
# prints
# A car that has:
# Engine with 200 bhp
del car_one
Before deletion of the Car
instance car_one
.
After deletion of the Car
instance car_one
.
=== TASK ===
Student Class
Create a Student
class that has a first_name
, surname
, age
, email
and id
.
These can all be public attributes.
Override the __str__()
method so that it prints out the as follows.
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
print(student_one)
Name: Joe Bloggs
Age: 25
Email: j.bloggs@derby.ac.uk
ID: 12345678
Records Class
Create a Records
class that manages a non-public list of _students
(i.e. a list that will contain instances of Student
).
Records
should have two methods:
add_student()
that adds an instance ofStudent
to_students
.
records = Records()
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
records.add_student(student_one)
add_students_from_list()
that takes a list as a parameter and adds this list to the internal list of students_students
.
records = Records()
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
student_two = Student("Ada", "Lovelace", 36, "a.lovelace@derby.ac.uk", 87654321)
records.add_student_from_list([student_one, student_two])
You should also override the Record __str__()
method so that it prints out each of the students in the student list _students
by using the Student
__str__()
method.
Instead of accessing the Student __str__()
method directly, you should use the str()
method which will get the value returned from __str__()
.
e.g.
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
student_one_str = str(student_one)
# student_one_str should contain - "Name: Joe Bloggs\nAge: 25\nEmail: j.bloggs@derby.ac.uk\nID: 12345678\n"
print(student_one)
Name: Joe Bloggs
Age: 25
Email: j.bloggs@derby.ac.uk
ID: 12345678
Some Examples
The below code snippets provide examples of how the classes should operate.
Adding a Single Student
records = Records()
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
records.add_student(student_one)
print(records)
Name: Joe Bloggs
Age: 25
Email: j.bloggs@derby.ac.uk
ID: 12345678
Adding a List of Students
records = Records()
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
student_two = Student("Ada", "Lovelace", 36, "a.lovelace@derby.ac.uk", 87654321)
records.add_student_from_list([student_one, student_two])
print(records)
Name: Joe Bloggs
Age: 25
Email: j.bloggs@derby.ac.uk
ID: 12345678
Name: Ada Lovelace
Age: 36
Email: a.lovelace@derby.ac.uk
ID: 87654321
Getting Started
You can get started by copying the following into main.py.
class Student:
pass
class Records:
pass
if __name__ == "__main__":
records = Records()
student_one = Student("Joe", "Bloggs", 25, "j.bloggs@derby.ac.uk", 12345678)
student_two = Student("Ada", "Lovelace", 36, "a.lovelace@derby.ac.uk",
87654321)
records.add_student_from_list([student_one, student_two])
print(records)
References
Composition and Aggregation in Java
The Playing Cards Program 1.0
This is an example from:
Chapter 9: Python Programming for the Absolute Beginner, Third Edition - Michael Dawson.
It is a simple program that demonstrates combining objects.
You will find two simple classes Card
and Hand
.
The Card
class represents a playing card.
The Hand
class represents a player's hand of cards.
The key point of this program is that the Hand
class internally manages a list of cards
that are in the player's hand.
Copy the following into main.py
.
# Playing Cards 1.0
# Demonstrates combining objects
class Card(object):
""" A playing card. """
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __str__(self):
rep = self.rank + self.suit
return rep
class Hand(object):
""" A hand of playing cards. """
def __init__(self):
self.cards = []
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + "\t"
else:
rep = "<empty>"
return rep
def clear(self):
self.cards = []
def add(self, card):
self.cards.append(card)
def give(self, card, other_hand):
self.cards.remove(card)
other_hand.add(card)
def main():
card1 = Card(rank = "A", suit = "c")
print("Printing a Card object:")
print(card1)
card2 = Card(rank = "2", suit = "c")
card3 = Card(rank = "3", suit = "c")
card4 = Card(rank = "4", suit = "c")
card5 = Card(rank = "5", suit = "c")
print("\nPrinting the rest of the objects individually:")
print(card2)
print(card3)
print(card4)
print(card5)
my_hand = Hand()
print("\nPrinting my hand before I add any cards:")
print(my_hand)
my_hand.add(card1)
my_hand.add(card2)
my_hand.add(card3)
my_hand.add(card4)
my_hand.add(card5)
print("\nPrinting my hand after adding 5 cards:")
print(my_hand)
your_hand = Hand()
my_hand.give(card1, your_hand)
my_hand.give(card2, your_hand)
print("\nGave the first two cards from my hand to your hand.")
print("Your hand:")
print(your_hand)
print("My hand:")
print(my_hand)
my_hand.clear()
print("\nMy hand after clearing it:")
print(my_hand)
input("\n\nPress the enter key to exit.")
if __name__ == "__main__":
main()
Inheritance
Let's imagine we were creating a simple university record system that keeps track of both students and staff.
Consider the following class
that represents a student.
1. Student and Staff (No Inheritance)
class Student:
""" Represents a student """
def __init__(self, first_name, surname, age, student_id, email):
self.first_name = first_name
self.surname = surname
self.age = age
self.student_id = student_id
self.email = email
def __str__(self):
return_string = ""
return_string += "Instance of Student\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.student_id}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
def is_minor(self):
return self.age < 18
def main():
student_one = Student("Bradley", "Davis", 25, 87654321, "b.davis@derby.ac.uk")
student_two = Student("Joe", "Bloggs", 17, 12345678, "j.bloggs@derby.ac.uk")
print(student_one)
print(student_two)
student_one.send_email()
student_two.send_email()
print(student_one.is_minor())
print(student_two.is_minor())
if __name__ == "__main__":
main()
We could add another class
that represents a staff member. Add this class to main.py and update the main()
method.
class Staff:
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
self.first_name = first_name
self.surname = surname
self.age = age
self.staff_id = staff_id
self.email = email
self.NI_no = NI_no
def __str__(self):
return_string = ""
return_string += "Instance of Staff\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.staff_id}\n"
return_string += f"NI_no: {self.NI_no}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
def is_pensionable(self):
return self.age >= 65
def main():
student_one = Student("Bradley", "Davis", 25, 87654321, "b.davis@derby.ac.uk")
student_two = Student("Joe", "Bloggs", 17, 12345678, "j.bloggs@derby.ac.uk")
staff_one = Staff("Sam", "O'Neill", 38, 12345678, "s.oneill@derby.ac.uk", "ABCDEF123")
print(student_one)
print(student_two)
print(staff_one)
student_one.send_email()
student_two.send_email()
staff_one.send_email()
print(student_one.is_minor())
print(student_two.is_minor())
print(staff_one.is_pensionable())
if __name__ == "__main__":
main()
If we look at the Student
and Staff
classes, we will see that they share a lot of characteristics and behaviour. They share a lot of code!
Object orientation tries to deal with this type of code duplication using inheritance. Inheritance is the idea that some classes might have things in common with a base (parent) class.
Sometimes the derived classes are called child classes. Much like you and I inherit genetics from our parents, you can think of derived (child) classes inheriting from their base (parent) class.
Some simple examples are:
Base (child) class | Derived (child) class |
---|---|
Shape | Circle , Square , Triangle |
Animal | Dog , Cat , Cow |
Person | Student , Staff |
The last example is exactly what is happening with the above. Both students and staff are people and they all have a name and age. In this case, they also share an email, and university ID and they have an identical method called send_email()
. What they don't share is the national insurance number NI_no
and the methods is_minor()
and is_pensionable()
.
2. Person Class
We will create what is called a base (parent) class that will have all the shared characteristics (attributes) and behaviour (methods). We can then use this to define derived (child) classes that inherit all of these.
class Person:
""" Represents a person """
def __init__(self, first_name, surname, age, id, email):
self.first_name = first_name
self.surname = surname
self.age = age
self.id = id
self.email = email
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.id}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
Now we have a person class, the only difference is that it does not have either of the methods is_minor()
or is_pensionable()
and does not have NI_no
.
person_one = Person("Bradley", "Davis", 25, 87654321)
# person_one.is_minor() # This will result in an error
# person_one.is_pensionable() # This will result in an error
3. Updating Student and Staff (with Inheritance)
Now that we have our Parent
class, we can now use this to further define newly derived (child) classes.
Here are the updated Student
and Staff
classes that inherit from Person
.
class Student(Person):
""" Represents a student """
def __init__(self, first_name, surname, age, student_id, email):
# this calls the base (parent) constructor
super().__init__(first_name, surname, age, student_id, email)
def is_minor(self):
return self.age < 18
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
# this calls the base (parent) constructor
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
def is_pensionable(self):
return self.age >= 65
As you can see, our Student
and Staff
classes are now much more simple, they inherit from Person
(notice Person is now in paratheses ()
e.g. class Student(Person)
) and they then define their attributes and methods on top of that.
Also, notice the use of super()
. This is a special method that gives the derived (child) class access to the base (parent) class methods. Here we call the base (parent) constructor __int__()
from within the derived (child) classes constructor __init__()
.
Here is the whole code.
class Person:
""" Represents a person """
def __init__(self, first_name, surname, age, id, email):
print("Person constructor called.")
self.first_name = first_name
self.surname = surname
self.age = age
self.id = id
self.email = email
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.id}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
class Student(Person):
""" Represents a student """
def __init__(self, first_name, surname, age, student_id, email):
print("Creating a staff member")
super().__init__(first_name, surname, age, student_id, email)
def is_minor(self):
return self.age < 18
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
print("Creating a staff member")
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
def is_pensionable(self):
return self.age >= 65
def main():
student_one = Student("Bradley", "Davis", 25, 87654321, "b.davis@derby.ac.uk")
student_two = Student("Joe", "Bloggs", 17, 12345678, "j.bloggs@derby.ac.uk")
staff_one = Staff("Sam", "O'Neill", 38, 12345678, "s.oneill@derby.ac.uk", "ABCDEF123")
print(student_one)
print(student_two)
print(staff_one)
student_one.send_email()
student_two.send_email()
staff_one.send_email()
print(student_one.is_minor())
print(student_two.is_minor())
print(staff_one.is_pensionable())
if __name__ == "__main__":
main()
The Student
and Staff
classes are now much more concise and only contain the things that are particular to each.
3.1 Class Hierarchy
Class | Inherits From |
---|---|
Person | object Python's base class |
Student | Person |
Staff | Person |
4. object
Class
It also turns out that all classes inherit from Python's base class object
. To see this we can write this explicitly. Instead of class Person:
we can write class Person(object)
. object
has a bunch of things that all classes inherit and can be overridden like __init__()
.
class Person(object):
""" Represents a person """
def __init__(self, first_name, surname, age, id, email):
print("Person constructor called.")
self.first_name = first_name
self.surname = surname
self.age = age
self.id = id
self.email = email
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.id}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
You don't need to write this explicitly, but it is worth knowing.
5. Multiple Levels of Inheritance
It is possible to keep inheriting from classes.
Here we create a slightly different version of Person
but we have a new class UniversityMember
which inherits from Person
and then Student
and Staff
will inherit from UniversityMember
.
5.1 Class Hierarchy
The following table now summarises the classes, which class they inherit from and their instance attributes and methods.
class | Inherits From | Instance Attributes | Instance Methods |
---|---|---|---|
Person | object (Python's base class) | first_name , surname , age , home_address | |
UniversityMember | Person | id , email | send_email() |
Student | UniversityMember | uni_address | is_minor() |
Staff | UniversityMember | NI_no | is_pensionable() |
5.2 Implementaion
Here is our new code that implements this class hierarchy. Copy and paste this into main.py to see it working.
class Person:
""" Represents a person """
def __init__(self, first_name, surname, age, home_address):
self.first_name = first_name
self.surname = surname
self.age = age
self.home_address = home_address
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"Address: {self.home_address}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
class UniversityMember(Person):
""" Represents a University Member"""
def __init__(self, first_name, surname, age, home_address, student_id, email):
super().__init__(first_name, surname, age, home_address)
self.id = id
self.email = email
class Student(UniversityMember):
""" Represents a student """
def __init__(self, first_name, surname, age, home_address, uni_address, student_id, email):
super().__init__(first_name, surname, age, home_address, student_id, email)
self.uni_address = uni_address
def is_minor(self):
return self.age < 18
class Staff(UniversityMember):
""" Represents a staff member """
def __init__(self, first_name, surname, age, home_address, staff_id, email, NI_no):
super().__init__(first_name, surname, age, home_address, staff_id, email)
self.NI_no = NI_no
def is_pensionable(self):
return self.age >= 65
def main():
student_one = Student("Bradley", "Davis", 25, "Somewhere he calls home", "XR lab, he sleeps here", 87654321, "b.davis@derby.ac.uk",)
student_two = Student("Joe", "Bloggs", 17, "Home sweet home", "Uni digs", 12345678, "j.bloggs@derby.ac.uk")
staff_one = Staff("Sam", "O'Neill", 38, "His house", 12345678, "s.oneill@derby.ac.uk", "ABCDEF123")
print(student_one)
print(student_two)
print(staff_one)
student_one.send_email()
student_two.send_email()
staff_one.send_email()
print(student_one.is_minor())
print(student_two.is_minor())
print(staff_one.is_pensionable())
if __name__ == "__main__":
main()
6. A Simple Animal Example
The following example creates an Animal
class with instance attributes name
, age
, no_legs
and an instance method talk()
.
We then inherit from Animal
to create a Dog
and a Duck
class.
Notice that when we call super().__init__()
in the Dog
and Duck
constructor (__init__()
) we hardcode the no_legs
to 4
and 2
, respectively.
Why bother asking the person creating the instance of Dog
or Duck
the number of legs when you know the answer?
Here is the code.
class Animal:
""" class representing an animal"""
def __init__(self, name, age, no_legs):
self.name = name
self.age = age
self.no_legs = no_legs
def talk(self):
print(f"Hi I am {self.name}, I am {self.age} years old and I have {self.no_legs} legs")
class Dog(Animal):
""" class representing a dog"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 4)
def bark(self):
print("Woof")
class Duck(Animal):
""" class representing a duck"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 2)
def quack(self):
print("Quack")
if __name__ == "__main__":
animal_list = []
dog_one = Dog("Rex", 11)
duck_one = Duck("Daffy", 84)
animal_list.append(dog_one)
animal_list.append(duck_one)
# because they both inherit from the Animal class we know they have a talk() method
for animal in animal_list:
animal.talk()
# however they have their own instance methods bark() and ``quack``
dog_one.bark()
duck_one.quack()
This will print out:
Hi I am Rex, I am 11 years old and I have 4 legs
Hi I am Daffy, I am 84 years old and I have 2 legs
Woof
Quack
We will see a better way to do this using polymorphism in a later lesson.
=== TASK ===
Please note that we could probably do this TASK differently and there are arguments for that, however, it is a simple exercise to help test inheritance.
Create a RegularPolygon
class that has an attributes no_sides
, side_length
and instance methods area()
and perimeter()
.
You can compute the area and perimeter of a regular polygon as follows.
You should compute the area and perimeter to 2 decimal places.
Area of A Regular Polygon
AREA = (1/4)*no_sides * (side_length**2 / math.tan(math.pi/no_sides))
Note that you will need the math
module to use pi
and tan
.
Perimeter of A Regular Polygon
PERIMETER = no_sides * side_length
Maths is Fun - Regular Polygons
Extending RegularPolygon
Extend the RegularPolygon
class to create EquilateralTriangle
, Square
and Pentagon
. Each class should correctly set no_sides
to 3
, 4
and 5
, respectively.
Implement the __str__()
method so that it returns the following string.
class | __str()__ returns |
---|---|
EquilateralTriangle | "EquilateralTriangle\n\nArea: {AREA}\n\nPerimeter: {PERIMETER}\n" |
Square | "Square\n\nArea: {AREA}\n\nPerimeter: {PERIMETER}\n" |
Pentagon | "Pentagon\n\nArea: {AREA}\n\nPerimeter: {PERIMETER}\n" |
Equilateral Triangle Example
tri = EquilateralTriangle(1)
print(tri)
Prints out:
EquilateralTriangle
Area: 0.43
Perimeter: 3
Square Example
square = Square(1)
print(square)
Prints out:
Square
Area: 1.0
Perimeter: 4
Pentagon Example
pent = Pentagon(1)
print(pent)
Prints out:
Pentagon
Area: 1.72
Perimeter: 5
You can generate more examples using Calculator Soup - Regular Polygon Calculator.
Getting Started
You can get started by copying and pasting the following into main.py.
import math
class RegularPolygon:
pass
class EquilateralTriangle(RegularPolygon):
pass
class Square(RegularPolygon):
pass
class Pentagon(RegularPolygon):
pass
if __name__ == "__main__":
tri = EquilateralTriangle(1)
square = Square(1)
pent = Pentagon(1)
print(tri)
print(square)
print(pent)
References
Maths is Fun - Regular Polygons
Calculator Soup - Regular Polygon Calculator
The Playing Cards Program 2.0
This is an example from:
Chapter 9: Python Programming for the Absolute Beginner, Third Edition - Michael Dawson.
It is a simple program that extends the previous playing cards program using inheritance.
Along with the two simple classes Card
and Hand
you will find a new class called Deck
.
Deck
represents a card deck. Interestingly if you think of a deck, it is quite like a hand, it is just a special hand that has all 52 different cards in to start with.
Hence our deck just extends (inherits) from Hand
.
It then has three additional methods to the standard Hand
class - populate()
(populate the deck with all 52 cards), shuffle()
(shuffle the card order in the list) and finally deal()
which given a list of hands will deal out a number of cards to each by calling its give()
method which gives a card from the deck to the other hand.
Copy the following into main.py
.
import random
# Playing Cards 2.0
# Demonstrates inheritance - class extension
class Card(object):
""" A playing card. """
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __str__(self):
rep = self.rank + self.suit
return rep
class Hand(object):
""" A hand of playing cards. """
def __init__(self):
self.cards = []
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + "\t"
else:
rep = "<empty>"
return rep
def clear(self):
self.cards = []
def add(self, card):
self.cards.append(card)
def give(self, card, other_hand):
self.cards.remove(card)
other_hand.add(card)
class Deck(Hand):
""" A deck of playing cards. """
def populate(self):
for suit in Card.SUITS:
for rank in Card.RANKS:
self.add(Card(rank, suit))
def shuffle(self):
random.shuffle(self.cards)
def deal(self, hand_list, per_hand=1):
for rounds in range(per_hand):
for hand in hand_list:
if self.cards:
top_card = self.cards[0]
self.give(top_card, hand)
else:
print("Can't continue deal. Out of cards!")
def main():
# create a deck
deck_one = Deck()
print("Created a new deck.")
print("Deck:")
print(deck_one)
# populate the deck
deck_one.populate()
print("\nPopulated the deck.")
print("Deck:")
print(deck_one)
# shuffle the deck
deck_one.shuffle()
print("\nShuffled the deck.")
print("Deck:")
print(deck_one)
# create my hand
my_hand = Hand()
# create your hand
your_hand = Hand()
# create a list of hands
hand_list = [my_hand, your_hand]
# deal 5 cards to each of the hands in the hand_list
deck_one.deal(hand_list, per_hand = 5)
print("\nDealt 5 cards to my hand and your hand.")
# print out the hands
print("My hand:")
print(my_hand)
print("Your hand:")
print(your_hand)
print("Deck:")
print(deck_one)
if __name__ == "__main__":
main()
Method Overriding and Polymorphism
Method overriding allows a derived (child) class to override a method defined in a base (parent) class.
Polymorphism means "many forms". In computer science, it is often used to mean reusing the same interface (function) for many meanings. In particular, we will be discussing subtyping polymorphism.
1. A Simple Example
Let's revisit our animal example from the lesson on inheritance.
This example creates an Animal
class with instance attributes name
, age
, no_legs
and an instance method talk()
.
We then inherit from Animal
to create a Dog
and a Duck
class.
Notice that when we call super().__init__()
in the Dog
and Duck
constructor (__init__()
) we hardcode the no_legs
to 4
and 2
, respectively.
Why bother asking the person creating the instance of Dog
or Duck
the number of legs when you know the answer?
Here is the code.
class Animal:
""" class representing an animal"""
def __init__(self, name, age, no_legs):
self.name = name
self.age = age
self.no_legs = no_legs
def talk(self):
print(f"Hi I am {self.name}, I am {self.age} years old and I have {self.no_legs} legs")
class Dog(Animal):
""" class representing a dog"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 4)
def bark(self):
print("Woof")
class Duck(Animal):
""" class representing a duck"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 2)
def quack(self):
print("Quack")
if __name__ == "__main__":
animal_list = []
dog_one = Dog("Rex", 11)
duck_one = Duck("Daffy", 84)
animal_list.append(dog_one)
animal_list.append(duck_one)
# because they both inherit from the Animal class we know they have a talk() method
for animal in animal_list:
animal.talk()
# however they have their own instance methods bark() and ``quack``
dog_one.bark()
duck_one.quack()
This will print out:
Hi I am Rex, I am 11 years old and I have 4 legs
Hi I am Daffy, I am 84 years old and I have 2 legs
Woof
Quack
1.1 Using Polymorphism
Currently, this does not override the talk()
method in either Duck
or Dog
, let's rewrite this so the talk()
method now prints out "Woof"
and "Quack"
, respectively. We will remove the bark()
and quack()
methods.
We will also create a list of animals that contain both Dog
instances and Duck
instances, iterate over this list and call the talk()
method. This is polymorphism!
Why? It is because we know that Dog
and Duck
inherit from Animal
, so we know they have a talk()
method. However, we have overridden the method in both derived (child) classes which gives us different behaviour using the same method name, i.e. talk()
.
class Animal:
""" class representing an animal"""
def __init__(self, name, age, no_legs):
self.name = name
self.age = age
self.no_legs = no_legs
# override the base talk() method
def talk(self):
print(f"Hi I am {self.name}, I am {self.age} years old and I have {self.no_legs} legs")
class Dog(Animal):
""" class representing a dog"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 4)
def talk(self):
print("Woof")
class Duck(Animal):
""" class representing a duck"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 2)
# override the base talk() method
def talk(self):
print("Quack")
if __name__ == "__main__":
animal_list = []
dog_one = Dog("Rex", 11)
duck_one = Duck("Daffy", 84)
animal_list.append(dog_one)
animal_list.append(duck_one)
# because they both inherit from the Animal class we know they have a talk() method
# this is polymorphism
for animal in animal_list:
animal.talk()
The last two lines,
for animal in animal_list:
animal.talk()
show polymorphism in action as we ask two diffent types, Dog
and Duck
to invoke its talk()
method.
The above example now prints out:
Woof
Quack
2. Student and Staff Example
Based on our previous lesson we had the following code to represent a Staff
and a Student
class which inherit from a Person
class.
class Person:
""" Represents a person """
def __init__(self, first_name, surname, age, id, email):
print("Person constructor called.")
self.first_name = first_name
self.surname = surname
self.age = age
self.id = id
self.email = email
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.id}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
class Student(Person):
""" Represents a student """
def __init__(self, first_name, surname, age, student_id, email):
print("Creating a staff member")
super().__init__(first_name, surname, age, student_id, email)
def is_minor(self):
return self.age < 18
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
print("Creating a staff member")
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
def is_pensionable(self):
return self.age >= 65
def main():
student_one = Student("Bradley", "Davis", 25, 87654321, "b.davis@derby.ac.uk")
student_two = Student("Joe", "Bloggs", 17, 12345678, "j.bloggs@derby.ac.uk")
staff_one = Staff("Sam", "O'Neill", 38, 12345678, "s.oneill@derby.ac.uk", "ABCDEF123")
print(student_one)
print(student_two)
print(staff_one)
student_one.send_email()
student_two.send_email()
staff_one.send_email()
print(student_one.is_minor())
print(student_two.is_minor())
print(staff_one.is_pensionable())
if __name__ == "__main__":
main()
One thing you will notice if you run this code is that both Student
and Staff
instances can invoke (call) the send_email()
method because it is inherited from Person
. They can also invoke (call) the methods they define is_minor()
and is_pensionable()
.
Also when we print the instances, e.g. print(student_one)
and print(staff_one)
, this is invoking the __str__()
method defined in Person
.
2.1 Using Polymorphism
However, because __str__()
is defined in Person
, when invoked for Staff
, it won't print out the national insurance number. We can choose to override this method.
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
print("Creating a staff member")
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
# override the base __str__ method
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.id}\n"
# also adds NI_no
return_string += f"National Insurance No.: {self.NI_no}\n"
return return_string
def is_pensionable(self):
return self.age >= 65
Now when we do print(staff_one)
it will include the national insurance number. Nice.
Except, we are repeating the code. We can again use super()
to simplify this. This time super()
will be used to call the instance method __str__()
.
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
print("Creating a staff member")
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
# override the base __str__ method but use it's functionality via super()
def __str__(self):
# call the __str__() method of Person (base) and assign to return_string
return_string = super().__str__()
# also adds NI_no
return_string += f"National Insurance No.: {self.NI_no}\n"
return return_string
def is_pensionable(self):
return self.age >= 65
We can override any method defined in the base (parent) class. Let's also override the send_email()
method for the Staff
class.
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
print("Creating a staff member")
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
# override the base __str__ method but use it's functionality via super()
def __str__(self):
# call the __str__() method of Person (base) and assign to return_string
return_string = super().__str__()
# also adds NI_no
return_string += f"National Insurance No.: {self.NI_no}\n"
return return_string
def is_pensionable(self):
return self.age >= 65
# override the base send_email method but use it's functionality
def send_email(self):
super().send_email()
print(f"Staff ID: {self.id}")
Our final code looks like this.
class Person:
""" Represents a person """
def __init__(self, first_name, surname, age, id, email):
print("Person constructor called.")
self.first_name = first_name
self.surname = surname
self.age = age
self.id = id
self.email = email
def __str__(self):
return_string = ""
return_string += f"Instance of {self.__class__}\n\n"
return_string += f"Name: {self.first_name} {self.surname}\n"
return_string += f"Age: {self.age}\n"
return_string += f"ID: {self.id}\n"
return return_string
def send_email(self):
print(f"Email sent to {self.email}")
class Student(Person):
""" Represents a student """
def __init__(self, first_name, surname, age, student_id, email):
print("Creating a staff member")
super().__init__(first_name, surname, age, student_id, email)
def is_minor(self):
return self.age < 18
class Staff(Person):
""" Represents a staff member """
def __init__(self, first_name, surname, age, staff_id, email, NI_no):
print("Creating a staff member")
super().__init__(first_name, surname, age, staff_id, email)
self.NI_no = NI_no
def __str__(self):
# call the __str__() method of Person (base) and assign to return_string
return_string = super().__str__()
# also adds NI_no
return_string += f"National Insurance No.: {self.NI_no}\n"
return return_string
def is_pensionable(self):
return self.age >= 65
def send_email(self):
super().send_email()
print(f"Staff ID: {self.id}")
def main():
student_one = Student("Bradley", "Davis", 25, 87654321, "b.davis@derby.ac.uk")
student_two = Student("Joe", "Bloggs", 17, 12345678, "j.bloggs@derby.ac.uk")
staff_one = Staff("Sam", "O'Neill", 38, 12345678, "s.oneill@derby.ac.uk", "ABCDEF123")
person_list = []
person_list.append(student_one, student_two, staff_one)
# polymorphism, we do not know where the __str__() method is implemented, we just know it is.
for person in person_list:
print(person)
# polymorphism, we do not know where the send_email() method is implemented, we just know it is.
for person in person_list:
person.send_email()
print(student_one.is_minor())
print(student_two.is_minor())
print(staff_one.is_pensionable())
if __name__ == "__main__":
main()
The following lines in the above code demonstrate polymorphism.
# polymorphism, we do not know where the __str__() method is implemented, we just know it is.
for person in person_list:
print(person)
# polymorphism, we do not know where the send_email() method is implemented, we just know it is.
for person in person_list:
person.send_email()
That is we know that instances of Staff
and Student
inherit from Person
and therefore have the methods __str__()
and send_email()
. However, we have overridden them with gives us different behaviour using the same method name. In fact, we didn't override send_email()
in Student
, so it uses the one defined in Person
.
3. Abstract Classes
An abstract class allows us to template methods that derived classes must implement.
We can demonstrate this with our earlier animal example.
Here we make Animal
an abstract class by inheriting the ABC
class (Abstract Base Class).
We can then mark methods as abstract using the decorator @abstractmethod
, this forces any derived class such as Dog
to override this method.
from abc import ABC, abstractmethod
class Animal(ABC):
""" class representing an animal"""
def __init__(self, name, age, no_legs):
self.name = name
self.age = age
self.no_legs = no_legs
# abstract method, derived classes must implement!
@abstractmethod
def talk(self):
return 1
class Dog(Animal):
""" class representing a dog"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 4)
def talk(self):
print("Woof")
class Duck(Animal):
""" class representing a duck"""
def __init__(self, name, age):
# here we hardcode the no of legs to be 4
super().__init__(name, age, 2)
# override the base talk() method
def talk(self):
print("Quack")
if __name__ == "__main__":
animal_list = []
dog_one = Dog("Rex", 11)
duck_one = Duck("Daffy", 84)
animal_list.append(dog_one)
animal_list.append(duck_one)
# because they both inherit from the Animal class we know they have a talk() method
# this is polymorphism
for animal in animal_list:
animal.talk()
The above example will again print out:
Woof
Quack
Try removing the talk()
method from Duck
and you will get the following error.
Traceback (most recent call last):
File "main.py", line 34, in <module>
duck_one = Duck("Daffy", 84)
TypeError: Can't instantiate abstract class Duck with abstract method talk
=== TASK ===
Create an abstract class called Shape
, it should have two abstract methods; area()
and perimeter()
and one attribute colour
.
You should create classes for the following shapes given in the table below. You should inherit from Shape
and override the area()
and perimeter()
methods. Round both area and circumference to 2 decimal places.
Shape | Class Name | Attributes | Area Formula | Perimeter Formula |
---|---|---|---|---|
Right Triangle | RightTriangle | base , height | (1/2) * base * height | base + height + math.sqrt(base**2 + height**2) |
Square | Square | side | side**2 | 4 * side |
Rectangle | Rectangle | base , height | base * height | 2 * (base + height) |
HINT: Remember you will need to use self
to access the instance attributes.
If done correctly you should be able to run this code.
right_tri = RightTriangle(1,1, "Red")
square = Square(1, "Blue")
rectangle = Rectangle(1,2, "Green")
shape_list = []
shape_list.append(right_tri)
shape_list.append(square)
shape_list.append(rectangle)
for shape in shape_list:
print(shape.area())
print()
for shape in shape_list:
print(shape.perimeter())
print()
for shape in shape_list:
print(shape.colour)
Which will print out the following.
0.5
1
2
3.41
4
6
Red
Blue
Green
Extra Thought
What could Square
inherit from other than Shape
and how would this work?
The Playing Cards Program 3.0
This is an example from:
Chapter 9: Python Programming for the Absolute Beginner, Third Edition - Michael Dawson.
It is a simple program that extends the previous playing cards program using inheritance and overriding methods.
We now introduce two extra card classes that extend from Card
- UnprintableCard
, PositionableCard
Both of these override the base (Card
) class __str__()
method.
PositionableCard
also manages an attribute is_face_up
that keeps track of which way up a card is, face up or face down. It also has a method called flip()
, that allows us to change is_face_up
to True
or False
.
Copy the following into main.py
.
import random
# Playing Cards 3.0
# Demonstrates inheritance - overriding methods
class Card(object):
""" A playing card. """
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __str__(self):
rep = self.rank + self.suit
return rep
class UnprintableCard(Card):
""" A Card that won't reveal its rank or suit when printed. """
def __str__(self):
return "<unprintable>"
class PositionableCard(Card):
""" A Card that can be face up or face down. """
def __init__(self, rank, suit, face_up=True):
super().__init__(rank, suit)
self.is_face_up = face_up
def __str__(self):
if self.is_face_up:
rep = super().__str__()
else:
rep = "XX"
return rep
def flip(self):
self.is_face_up = not self.is_face_up
class Hand(object):
""" A hand of playing cards. """
def __init__(self):
self.cards = []
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + "\t"
else:
rep = "<empty>"
return rep
def clear(self):
self.cards = []
def add(self, card):
self.cards.append(card)
def give(self, card, other_hand):
self.cards.remove(card)
other_hand.add(card)
class Deck(Hand):
""" A deck of playing cards. """
def populate(self):
for suit in Card.SUITS:
for rank in Card.RANKS:
self.add(Card(rank, suit))
def shuffle(self):
random.shuffle(self.cards)
def deal(self, hand_list, per_hand=1):
for rounds in range(per_hand):
for hand in hand_list:
if self.cards:
top_card = self.cards[0]
self.give(top_card, hand)
else:
print("Can't continue deal. Out of cards!")
def main():
card_one = Card("A", "c")
card_two = UnprintableCard("A", "d")
card_three = PositionableCard("A", "h")
print("Printing a Card object:")
print(card_one)
print("\nPrinting an UnprintableCard object:")
print(card_two)
print("\nPrinting a PositionableCard object:")
print(card_three)
print("Flipping the PositionableCard object.")
card_three.flip()
print("Printing the PositionableCard object:")
print(card_three)
input("\n\nPress the enter key to exit.")
if __name__ == "__main__":
main()
Modules and Classes
This is a fairly straightforward lesson.
Basically, you can create modules which contain both functions and classes.
A module should contain related code, whether that be in the form of functions or classes.
For example, we could create a Critter
class and also create a function called print_critter_list()
.
Here are the contents of critter.py
.
# critter.py
# demonstrates having a class and a function in the same module
class Critter:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Hi I am {self.name}"
# simple function that prints out a list of critters
# note this is not in the class Critter
def print_critter_list(critter_list):
for critter in critter_list:
print(critter)
Here are the contents of main.py.
# main.py
from critter import Critter, print_critter_list
# create a list of critters using the imported Critter class
critter_list =[Critter("Bob"), Critter("Sue"), Critter("Ava"), Critter("Alan")]
# print the list of critters using the imported print_critter_list function
print_critter_list(critter_list)
Running main.py will print out the following:
Hi I am Bob
Hi I am Sue
Hi I am Ava
Hi I am Alan
If you are interested in how to structure larger projects you can search on the internet and you will find lots of best practices and tutorials.
However, it is out of the scope of this course.
=== TASK ===
Create a module called person.py
. It should contain a single class Person
and a single function say_hello()
.
The class Person
should have attributes name
and age
and you should override the __str__()
method so that it returns Hi I am NAME and I am AGE years old
.
The function say_hello()
should take in an instance of Person
and print out the instance of Person
.
You can copy the following into main.py
to get started, but you will need to have the Person
class and say_hello
function set up in hello.py
.
# main.py
from person import Person, say_hello
terry = Person("Terry", 45)
say_hello(terry)
Which will print out:
Hi I am Terry and I am 45 years old
The Blackjack Game
This is an example from:
Chapter 9: Python Programming for the Absolute Beginner, Third Edition - Michael Dawson.
This is the full Blackjack program, it uses the classes we have built up so far, but also adds a number of other classes. It also groups functionality into three modules blackjack
, cards
and games
.
cards.py
# Cards Module
# Basic classes for a game with playing cards
import random
class Card(object):
""" A playing card. """
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
SUITS = ["c", "d", "h", "s"]
def __init__(self, rank, suit, face_up=True):
self.rank = rank
self.suit = suit
self.is_face_up = face_up
def __str__(self):
if self.is_face_up:
rep = self.rank + self.suit
else:
rep = "XX"
return rep
def flip(self):
self.is_face_up = not self.is_face_up
class Hand(object):
""" A hand of playing cards. """
def __init__(self):
self.cards = []
def __str__(self):
if self.cards:
rep = ""
for card in self.cards:
rep += str(card) + "\t"
else:
rep = "<empty>"
return rep
def clear(self):
self.cards = []
def add(self, card):
self.cards.append(card)
def give(self, card, other_hand):
self.cards.remove(card)
other_hand.add(card)
class Deck(Hand):
""" A deck of playing cards. """
def populate(self):
for suit in Card.SUITS:
for rank in Card.RANKS:
self.add(Card(rank, suit))
def shuffle(self):
random.shuffle(self.cards)
def deal(self, hands, per_hand=1):
for rounds in range(per_hand):
for hand in hands:
if self.cards:
top_card = self.cards[0]
self.give(top_card, hand)
else:
print("Can't continue deal. Out of cards!")
if __name__ == "__main__":
print("This is a module with classes for playing cards.")
input("\n\nPress the enter key to exit.")
games.py
# Games
# Demonstrates module creation
class Player(object):
""" A player for a game. """
def __init__(self, name, score=0):
self.name = name
self.score = score
def __str__(self):
rep = self.name + ":\t" + str(self.score)
return rep
def ask_yes_no(question):
"""Ask a yes or no question."""
response = None
while response not in ("y", "n"):
response = input(question).lower()
return response
def ask_number(question, low, high):
"""Ask for a number within a range."""
response = None
while response not in range(low, high):
response = int(input(question))
return response
if __name__ == "__main__":
print("You ran this module directly (and did not 'import' it).")
input("\n\nPress the enter key to exit.")
blackjack.py
import cards, games
class BJ_Card(cards.Card):
""" A Blackjack Card. """
ACE_VALUE = 1
@property
def value(self):
if self.is_face_up:
v = BJ_Card.RANKS.index(self.rank) + 1
if v > 10:
v = 10
else:
v = None
return v
class BJ_Deck(cards.Deck):
""" A Blackjack Deck. """
def populate(self):
for suit in BJ_Card.SUITS:
for rank in BJ_Card.RANKS:
self.cards.append(BJ_Card(rank, suit))
class BJ_Hand(cards.Hand):
""" A Blackjack Hand. """
def __init__(self, name):
super().__init__()
self.name = name
def __str__(self):
rep = self.name + ":\t" + super().__str__()
if self.total:
rep += "(" + str(self.total) + ")"
print("here")
return rep
@property
def total(self):
# if a card in the hand has value of None, then total is None
for card in self.cards:
if not card.value:
return None
# add up card values, treat each Ace as 1
t = 0
for card in self.cards:
t += card.value
# determine if hand contains an Ace
contains_ace = False
for card in self.cards:
if card.value == BJ_Card.ACE_VALUE:
contains_ace = True
# if hand contains Ace and total is low enough, treat Ace as 11
if contains_ace and t <= 11:
# add only 10 since we've already added 1 for the Ace
t += 10
return t
def is_busted(self):
return self.total > 21
class BJ_Player(BJ_Hand):
""" A Blackjack Player. """
def is_hitting(self):
response = games.ask_yes_no("\n" + self.name +
", do you want a hit? (Y/N): ")
return response == "y"
def bust(self):
print(self.name, "busts.")
self.lose()
def lose(self):
print(self.name, "loses.")
def win(self):
print(self.name, "wins.")
def push(self):
print(self.name, "pushes.")
class BJ_Dealer(BJ_Hand):
""" A Blackjack Dealer. """
def is_hitting(self):
return self.total < 17
def bust(self):
print(self.name, "busts.")
def flip_first_card(self):
first_card = self.cards[0]
first_card.flip()
class BJ_Game(object):
""" A Blackjack Game. """
def __init__(self, names):
self.players = []
for name in names:
player = BJ_Player(name)
self.players.append(player)
self.dealer = BJ_Dealer("Dealer")
self.deck = BJ_Deck()
self.deck.populate()
self.deck.shuffle()
@property
def still_playing(self):
sp = []
for player in self.players:
if not player.is_busted():
sp.append(player)
return sp
def __additional_cards(self, player):
while not player.is_busted() and player.is_hitting():
self.deck.deal([player])
print(player)
if player.is_busted():
player.bust()
def play(self):
# deal initial 2 cards to everyone
self.deck.deal(self.players + [self.dealer], per_hand=2)
self.dealer.flip_first_card() # hide dealer's first card
for player in self.players:
print(player)
print(self.dealer)
# deal additional cards to players
for player in self.players:
self.__additional_cards(player)
self.dealer.flip_first_card() # reveal dealer's first
if not self.still_playing:
# since all players have busted, just show the dealer's hand
print(self.dealer)
else:
# deal additional cards to dealer
print(self.dealer)
self.__additional_cards(self.dealer)
if self.dealer.is_busted():
# everyone still playing wins
for player in self.still_playing:
player.win()
else:
# compare each player still playing to dealer
for player in self.still_playing:
if player.total > self.dealer.total:
player.win()
elif player.total < self.dealer.total:
player.lose()
else:
player.push()
# remove everyone's cards
for player in self.players:
player.clear()
self.dealer.clear()
main.py
# Blackjack
# From 1 to 7 players compete against a dealer
import games, blackjack
def main():
print("\t\tWelcome to Blackjack!\n")
names = []
number = games.ask_number("How many players? (1 - 7): ", low=1, high=8)
for i in range(number):
name = input("Enter player name: ")
names.append(name)
print()
game = blackjack.BJ_Game(names)
again = None
while again != "n":
game.play()
again = games.ask_yes_no("\nDo you want to play again?: ")
if __name__ == "__main__":
main()
input("\n\nPress the enter key to exit.")