Why we use types
Learning Objectives
In real life, as well as programming, there are some impossible operations. Can you divide seven by yellow? Can you set fire to a sound? These don’t make sense. The same is true in programming.
We are going to look at some functions which you can find in the file SDC-Tools/sprint-5 directory.
✍️exercise
Task 1
Have a look now at 01-predict.py.
Take a moment to make predictions about what function calls will and will not work.
Then try running the file and see what happens.
In that file, is half("22") hoping to return 11 (because the string should be converted to a number)? Or return 2 (because it’s the first half of the string)? Or error, because it doesn’t make sense?
What if we tried to run half("hello")? Try to give part of a word, or error because it can’tbe split evenly in half? Does this input even make sense?
What if we did double("hello") instead? What do you expect it to return?
How about second(22)? Should it treat 22 like a stringified version of the decimal representation of the number 22 and return 2? If so - 22 is the same as 0x16. Should second(0x16) convert 0x16 to decimal before returning the second character? Or should it remember that the original number was input as hexadecimal and return 6?
Intent
The intent of these functions is probably that half and double are expected to operate on numbers, and second is expected to operate on strings (and/or maybe lists). We don’t know for sure what the author intended just by looking at the function names.
But Python lets us write all of these things. Some of them, like half("hello") will error when they run, maybe breaking our program. Others, like double("22") will succeed but in surprising ways which may cause our program to give more subtly incorrect results later on.
In such a simple program as in 01-predict.py, it’s easy for us to run the program manually and see the errors (if we add enough logging). But as programs get bigger, these things get harder to spot, especially if there are branches and code only executes sometimes.
✍️exercise
Task 2
Have a look now at 02-playcomputer.py.
Read through this file and predict what it does.
Leave a comment if you spot any errors.
How many errors did you find in your testing? There is one big bug here which doesn’t always show. response.body is a stream not a string. So if a user ever tries to fetch a URL which returns a non-200 status code, our program will crash:
% node fetch.js
What URL should we fetch?
> http://www.google.com/beepboop
file:///Users/dwh/tmp/jsplay/fetch.js:12
if (response.body.toLowerCase().includes("permission")) {
^
TypeError: response.body.toLowerCase is not a function
at file:///Users/dwh/tmp/jsplay/fetch.js:12:23
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
Node.js v22.11.0
How easy was it to spot this bug in your testing?
The code in this file was wrong. It could never have been correct. After a fetch, response.body.toLowerCase() never makes sense. Ideally we shouldn’t have needed to wait until running the code, and using that exact input, to find this out.
Types
This is where types come in.
Imagine if we could analyse our code and find out “You’re calling double with a string, but double expects a number, you have a bug”. Or that “You’re calling response.body.toLowerCase() but response.body is a ReadableStream which doesn’t have a method toLowerCase, you have a bug”.
We wouldn’t need to keep executing our program with lots of different inputs every time we change it. The type analysis could tell us “You have a bug here, you should fix it”. Without having to run the program, and without having to think about different possible inputs.
Limits of type checking
Types can be really useful for detecting bugs. But there are limits to what kind of bugs type checking can detect.
✍️exercise
Task 3:
Look at file 03-fix.py.
Read the code and see if you can find any bugs.
Write down what the bug is, and how would you fix it?
Are there multiple ways you could fix it?
Type checking can’t catch this type of bug - as long as you give it a number as input, it gives you a number as output. All of the types are correct. Not all bugs are type errors. But checking for type errors can get rid of a lot of them.
Type checking with mypy
Learning Objectives
Support for type checking
Different languages have different levels of support for checking types.
Some languages, like Java, C++, Rust, and Go, require you to write what types you expect function parameters to have.
Other languages, like JavaScript and Python, don’t require this but they have tools which allow you to add this information by using a tool like mypy or JSDoc.
Some very low level machine languages like assembly don’t have any typing at all.
Languages with optional type checking perform good checks when you add this type information. If you don’t add type annotations in your code, they will perform fewer checks. Sometimes they will infer the correct types based on what you have annotated. Other times they will just ignore code with no annotations and not give you errors about it even if it’s wrong.
Trying out Mypy
Mypy is a tool which enables type checking in Python code.
Reading
✍️exercise
Task 4:
Have a look at 04-addmypy.py
This code contains bugs related to types. They are bugs mypy can catch.
Read this code to understand what it’s trying to do. Add type annotations to the method parameters and return types of this code. Run the code through mypy, and fix all of the bugs that show up. When you’re confident all of the type annotations are correct, and the bugs are fixed, run the code and check it works.
Classes and objects
Learning Objectives
We’ve already seen that objects can group together related, named data. We can write:
imran = {
"name": "Imran",
"age": 22,
"preferred_operating_system": "Ubuntu",
}
eliza = {
"name": "Eliza",
"age": 34,
"preferred_operating_system": "Arch Linux",
}This allows us to pass around the values of imran or eliza, and access all of the related information while we do.
We now know that typing can tell us if we make errors like calling .lower() on the numeric value 2.
It would be useful for a type checker to tell us if we try to access a property of an object that that object doesn’t have. Can mypy help us here?
✍️exercise
Task 5:
Have a look at 05-explain.py
This code contains some untyped objects.
Try checking it with mypy before running the code and predict what you think will happen when you run the code.
This code doesn’t work, but mypy can’t tell us this. Remember how we said that type checking has its limits? As far as mypy is concerned, a dictionary is a dictionary - it could contain any keys!
Instead, we can use a
💡Tip
The word object has a lot of uses.
In JavaScript, we don’t have a “dictionary” type, we call them objects. Sometimes these objects are just dictionaries - collections of key-value pairs. Other times they are instances of a specific class.
In general, people use the word object both to mean “collection of key-value pairs” and “instance of a class”. You often need to work out which they mean from context.
class Person:
def __init__(self, name: str, age: int, preferred_operating_system: str):
self.name = name
self.age = age
self.preferred_operating_system = preferred_operating_system
imran = Person("Imran", 22, "Ubuntu")
print(imran.name)
print(imran.address)
eliza = Person("Eliza", 34, "Arch Linux")
print(eliza.name)
print(eliza.address)This code is saying: “There’s a category of object called Person. Every instance of Person has a name, an age, and a preferred_operating_system”. It then makes two instances of Person, and uses them.
The method called __init__ is called a constructor - it is what is called when we construct a new instance of the class.
💬 No, these are called class attributes
💬 Yes, an instance is one specific copy of a class
💬 init is the constructor of a class in python
💬 No, a class already is a description of what it contains. An instance is more specific.
You can use the names of classes in type annotations just like you can use types like str or int:
def is_adult(person: Person) -> bool:
return person.age >= 18
print(is_adult(imran))Exercise
Task 6
Have a look at file 06-classes.py.
Run mypy and fix any errors.
Add a new function called likes_apple which takes a person as parameter and returns true only if the preferred operating system is either iOS or macOS. Add all the appropriate type annotations and make sure mypy has no errors.
Compare objects and classes and explain some advantages and disadvantages of each.
Methods
Learning Objectives
We’ve seen that we can take instances of classes as function parameters:
def is_adult(person: Person) -> bool:
return person.age >= 18We’ve also seen types that have methods on them, e.g. "abc".upper(). This looks a bit different from functions we define ourselves (which may look like upper("abc")).
Methods are just like functions, but they are attached to a class.
We could rewrite our is_adult function as a method on Person:
class Person:
def __init__(self, name: str, age: int, preferred_operating_system: str):
self.name = name
self.age = age
self.preferred_operating_system = preferred_operating_system
def is_adult(self):
return self.age >= 18
imran = Person("Imran", 22, "Ubuntu")
print(imran.is_adult()) # TrueThis has a few advantages over
✍️exercise
Task 7
What is the difference between methods and free functions?
Do some research and think of the advantages of using methods instead of free functions.
Write your thoughts down in 07-methods.txt
Expand for some answers after you've listed your own.
- Encapsulation - if we change the implementation of
Person(e.g. we start storing a date of birth instead of an age), it’s more obvious what things we need to change. - Ease of documentation - it makes it easier to find all of the things related to a string (or a Person) if they’re attached to that type.
Consider this free function called drivers_license_check which uses the Person class method is_adult outside of the class:
def drivers_license_check(person: Person):
if person.is_adult() == True:
return 'Valid drivers license'
return 'This person is underage!'
print(drivers_license_check(imran)) # returns 'Valid drivers license'✍️exercise
Task 8
Work inside the 08-implement.py file for this task.
- Add the
drivers_license_checkfree function and theis_adultmethod into your code, and make sure your code currently gives the expected output. - Change the
Personclass to take a date of birth (using the standard library’sdatetime.dateclass) and store thedate of birthin a field instead ofage(it should be astr). Don’t change anything else. - Try to run your code, how does this change break your code. What kind of error do you get? Is it helpful in identifying where your next change needs to be?
- Update the
is_adultmethod so the error is fixed. Using thedrivers_license_checkfunction check everything runs as expected, it should return “Valid drivers license”. You should not changedrivers_license_check.
Encapsulation in play 👀
Take a moment to consider what we’ve done here. How has encapsulation helped us make changes to our class?
We’ve changed a property of Person, seen errors inform us about how that change affected a method on the class, and then amended that method so we were maintaining the behaviour of the class. The behaviour of drivers_license_check did not need to change - we can change the internal implementation of the class without affecting external code.
Encapsulation is a widely known principle in object-oriented programming, consider reading around online to find out more
Dataclasses
Learning Objectives
We’ve seen that grouping together fields and methods into a class can help us encapsulate them. We can define a class whose purpose is just to group together related data, and provide access to it.
Our Person class is an example of this. We just store some data in it (and maybe add some methods that just read that data).
If a class is just a place to group related data, it is sometimes called a
There are several functions we can implement on classes that have obvious implementations for value objects.
Equality is one: ideally two value objects are the same if their fields are the same. But this is not the case with objects by default:
class Person:
def __init__(self, name: str, age: int, preferred_operating_system: str):
self.name = name
self.age = age
self.preferred_operating_system = preferred_operating_system
imran = Person("Imran", 22, "Ubuntu")
imran2 = Person("Imran", 22, "Ubuntu")
print(imran == imran2) # Prints FalseSimilarly, it’s useful when we print a value object to see its type and fields. But this is not the case with objects by default:
class Person:
def __init__(self, name: str, age: int, preferred_operating_system: str):
self.name = name
self.age = age
self.preferred_operating_system = preferred_operating_system
imran = Person("Imran", 22, "Ubuntu")
print(imran) # Prints <__main__.Person object at 0x1048b5a90>Python has a useful
dataclass which generates some of these functions for us. In fact, it even generates the constructor for us.
from dataclasses import dataclass
@dataclass(frozen=True)
class Animal:
name: str
species: str
age: int
noise: str
indigo = Animal("indigo", "cat", 2, "meow") # We can call this constructor - @dataclass generated it for us.
print(indigo) # Prints Animal(name='Indigo', species='cat', age=2, noise='meow')
indigo2 = Animal("indigo", "cat", 2, "meow")
print(indigo == indigo2) # Prints TrueThe dataclass decorator generated a constructor, a __str__ method (which is called when string formatting the value), and a custom __eq__ method (which is called when comparing two values). This saves us having to write all of that code.
Other languages have a similar idea of a value type, and tools to help make them, such as Java’s record classes and C#’s’ structure types.
✍️exercise
Task 9
Work in file 09-implement.py for this task.
Convert your existing Person class into a value type using @datatype so you can print the class (and see it’s type and fields) and compare class instances that are identical. Make sure your is_adult method and drivers_license_check free function both work as normal.
Make a new method on your Person class - greet which should return "Hello <person name>!" when used.
Take a look at the @datatype documentation - what does frozen=True do to the class? What other options could you play around with and explore?
Generics
Learning Objectives
A problem type checking can’t spot
Sometimes we want to reason about more complicated type relationships than “this field is a string”. Lists and dicts are examples of this. We may want to reason that every value in a list is a string.
Consider this code:
from dataclasses import dataclass
@dataclass(frozen=True)
class Animal:
name: str
species: str
@dataclass(frozen=True)
class Person:
name: str
age: int
@dataclass(frozen=True)
class FamilyTree:
parent: Person
members: list
pet = Animal(name="Gromit", species="Dog")
fatma = Person(name="Fatma", age=4)
aisha = Person(name="Aisha", age=6)
imran = Person(name="Imran", age=30)
family = FamilyTree(parent=imran, members=[fatma, aisha, pet])
def print_family_tree(family: FamilyTree):
print(family.parent.name)
for child in family.members:
print(f"{child.name} ({child.age} years old)")
print_family_tree(family)✍️exercise
Task 10
Have a look at the above code, you can find a copy in 10-predict.py
There is a bug in this code. Can you spot it?
Run your code through mypy. Does mypy spot it?
Offer an explanation for what is happening.
In some languages, like Java, C#, Rust, or Go, type information is required - you can’t write code without it. This means those languages can do more checks, and give better error messages. We call these
In other languages, like Python and JavaScript, type information is optional. Because of this, tools that check types are sometimes less strict. If they don’t know what type something has, they stop doing any checks.
That’s what’s happening here. FamilyTree.members is a list, but mypy doesn’t know what type of thing is in the list. It doesn’t even know that everything in the list has the same type = ["hello", 7, True] is a legal list in Python. Many people would consider a pet to be a member of the family, so it seems correct, but due to the different types, this code breaks down and mypy can’t spot the problem.
Using Generics
We can use
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Animal:
name: str
species: str
@dataclass(frozen=True)
class Person:
name: str
age: int
@dataclass(frozen=True)
class FamilyTree:
parent: Person
members: List[Person]
pet = Animal(name="Gromit", species="Dog")
fatma = Person(name="Fatma", age=4)
aisha = Person(name="Aisha", age=6)
imran = Person(name="Imran", age=30)
family = FamilyTree(parent=imran, members=[fatma, aisha, pet])
def print_family_tree(family: FamilyTree):
print(family.parent.name)
for child in family.members:
print(f"{child.name} ({child.age} years old)")
print_family_tree(family)Try updating the code with this change and see if mypy spots the problem
Run this code through mypy.
Now that we’ve told mypy FamilyTree.members is a list of type Person, it can identify that the child variable printed out must be of type Person. Because of this, it can tell us that child.age on doesn’t exist when the pet is accidentally included in the list.
📝Note
Most generics don’t need the types to be quoted. For example, you can write List[Person].
But if you want to recursively reference a type within the class, before the class has been defined, we need to quote it for mypy to recognise it.
So for example, if we wanted a family tree to go several levels deep, e.g. to include grandchildren, we would write it as List["FamilyTree"].
It’s kind of annoying, but don’t worry about it too much.
Writing our own classes that use generics
The kind of relationship structure we created with families and members, a
Thinking about keeping our code reusable, is there a way we could define such structures, and be able to force them to work with certain types, without needing to write a special class for each individual data type? Just like lists can takea generic to force them to be a certain type, we can write classes that accept generics.
Look at the following code:
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Animal:
name: str
size: str
@dataclass(frozen=True)
class Person:
name: str
age: int
@dataclass(frozen=True)
class Tree[T]:
parent: T
children: List[T]
def print_tree(self):
print(self.parent)
for child in self.children:
print(child)
fatma = Person(name="Fatma", age=4)
aisha = Person(name="Aisha", age=6)
imran = Person(name="Imran", age=30)
family_tree = Tree[Person](parent=imran, children=[fatma, aisha])
cats = Animal(name="Cat", size="Small")
dogs = Animal(name="Dog", size="Medium")
mammals = Animal(name="Mammals", size="Variable")
species_tree = Tree[Animal](parent=mammals, children=[cats, dogs])
family_tree.print_tree()
species_tree.print_tree()The Tree here has a special type annotation given by T. This is a generic, telling python that whatever type is given, every reference to T within the class becomes that type.
Observe that we then create two different trees: a Tree<Person> and a Tree<Animal>. In these trees, the parent and list of children must contain Person and Animal types respectively.
It also means instead of having to create a new function torpint out every single tree type, we can create a single function - Tree.print_tree().
✍️exercise
Task 11
Experiment with mypy and make sure that the family tree only takes Person types and the species tree only takes Animal types.
We are going to improve the printing in the above code, you can find a copy in 11-fix.py
Currently the Tree.print_tree() function doesn’t look very pretty.
Change the Animal and Person classes, using whichever approach you think is best, to allow the Tree.print_tree() method to display an output that looks like this:
Imran (30 years old)
- Fatma (4 years old)
- Aisha (6 years old))
Mammals (Variable size)
- Cat (Small size)
- Dog (Medium size)Stretch task
Think of another type of data that can be organised into a tree.
Add a new class for this, instantiate some variables, and have the existing Tree class print it out.
Type-guided refactorings
Learning Objectives
Using classes and objects can help us to understand and maintain codebases, particularly as they grow. The process of taking some old code, and updating it in a maintainable way is called “refactoring”.
We previously saw that using methods instead of free functions can help us to encapsulate information. But changing functions into methods, and modifying classes can be tricky, as it is easy to forget places where code needs to be updated during refactoring.
Type checking can help us with this. If you have some code which accesses imran.age, and we remove the age field, we can run mypy: It will tell us “Here are all of the places that reference age you also need to change your code”.
Take this file as an example. It is a program that works out what laptops could be allocated to what people based on their preferred operating system.
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Person:
name: str
age: int
preferred_operating_system: str
@dataclass(frozen=True)
class Laptop:
id: int
manufacturer: str
model: str
screen_size_in_inches: float
operating_system: str
def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]:
possible_laptops = []
for laptop in laptops:
if laptop.operating_system == person.preferred_operating_system:
possible_laptops.append(laptop)
return possible_laptops
people = [
Person(name="Imran", age=22, preferred_operating_system="Ubuntu"),
Person(name="Eliza", age=34, preferred_operating_system="Arch Linux"),
]
laptops = [
Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system="Arch Linux"),
Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="Ubuntu"),
Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system="ubuntu"),
Laptop(id=4, manufacturer="Apple", model="macBook", screen_size_in_inches=13, operating_system="macOS"),
]
for person in people:
possible_laptops = find_possible_laptops(laptops, person)
print(f"Possible laptops for {person.name}: {possible_laptops}")Let’s imagine we want to change our code. We don’t want to say “Every person has one preferred operating system” any more. We want to let people have a list of operating systems they prefer (in order). So we could say “Imran prefers Ubuntu most of all, and then Arch Linux, but will not use macOS”.
✍️exercise
Task 12
A copy of this file is present in 12-refactor.py.
Try changing the type annotation of Person.preferred_operating_system from str to List[str].
Run mypy on the code.
It tells us different places that our code is now wrong. Fix it to remov eany errors.
Now we changed the types, we probably also want to rename our fields to something appropriate.
Run mypy again.
Fix all of the places that mypy tells you need changing.
Then, make sure the program works as you’d expect.
The bigger (and more complicated) our codebase is, the more useful it is that mypy tells us what code needs changing. This is even more useful when we start working with code we didn’t write ourselves, or we wrote long ago. Instead of needing to read all of the code and search around to try to work out where we need to change an age to date_of_birth, or how to access a single variable that has become a list of many, mypy can tell us “here are all of the places that are wrong”.
Enums
Learning Objectives
In the laptops example, we were using strings to store operating systems. Using strings is often problematic because they can take lots of different values. When we have a known set of possible values it is useful to ensure only those values can occur.
Some common problems with strings:
- Case sensitivity - are
"macOS"and"MacOS"the same? Should they be? - Spaces - are
"ArchLinux"and"Arch Linux"the same? Should they be? - Normalised values - are
"Arch Linux"and"Arch"the same? Should they be? - Typos - is
"Arc Linux"meant to be"Arch Linux"? Or is it a separate operating system?
Did you spot in the bug in task 12? The laptop with id 3 was never put in anyone’s preferred list, because its operating system was spelled Ubuntu not ubuntu.
We can use enums to represent that one some values are allowed, and make sure we’re always using the same ones. This is similar to how in HTML we can use a <select> menu with <options> instead of an <input type="text"> to restrict what a user can enter into a form.
In Python, we can define an enum as a new type. This is like bool - bool is a type which has two possible values (True and False). We can make enums that have any number of possible values, and we can choose the values’ names.
from enum import Enum
class OperatingSystem(Enum):
MACOS = "macOS"
ARCH = "Arch Linux"
UBUNTU = "Ubuntu"This defines a new type called OperatingSystem which has three possible values - MACOS, ARCH, and UBUNTU. We can use this type in a type annotation to make sure that we’re only passed one of these values. If someone makes a typo in one of these values, mypy will catch it and tell us that UBUNT or macOS or NIX doesn’t exist.
📝Note
There are lots of ways different programming deal with the concept enums. Some, like JavaScript, have no built-in way to use enums. Python treats enums as a special kind of class mapping definitions to a value. Others, like Rust, have more advanced typing systems that can treat enumerations as standalone types.
We know that when we save data, transfer it across a network, or take user input, everything comes in as bytes. A typical pattern in software is to accept a string in the user input, and convert it to an enum before passing it into any other function. If the string wasn’t a valid operating system we know about, we will reject it and give an error when we first accept it. All of our other functions can take an OperatingSystem as a parameter, and know that any value it’s given must be a valid operating system. This restricts where we need to worry about incorrect input - once we’ve checked that the string was correct one time, the rest of our code doesn’t have to worry about incorrect strings.
✍️exercise
Task 13
Look at file 13-implement.py
It currently handles operating systems as strings.
Refactor the code to use enums for operating systems.
Check with mypy and test it to ensure the program still works correctly.
Replace the list of existing people with the input function to read a person’s name, age, and preferred operating system.
Make sure your implementation has a good user experience, and properly validates the inputs, mapping an OS to one of the enum values.
If an operating system can’t be matched at all, your script should handle it appropriately and not crash.
Inheritance
Learning Objectives
In this prep we have seen how add methods to classes to encapsulate functionality. We have seen how to use generics to force classes to work with certain types. Keeping code reusability and maintainability in mind, what if we wanted to add a new class that did mostly the same as an existing class, but with some slight changes?
Classes can extend other classes to share most of their functionality but add or replace some of it. A class that carries over something from another class is called inheritance.
Read the following code:
from typing import Iterable, Optional
class ImmutableNumberList:
# We accept any `Iterable[int]` here, so can construct with a list, a set, or anything else that can be iterated.
def __init__(self, elements: Iterable[int]):
# We copy the elements so that if someone mutates the passed in elements list, our copy won't be mutated.
self.elements = [element for element in elements]
def first(self) -> Optional[int]:
if not self.elements:
return None
return self.elements[0]
def last(self) -> Optional[int]:
if not self.elements:
return None
return self.elements[-1]
def length(self) -> int:
return len(self.elements)
def largest(self) -> Optional[int]:
# To find the largest element, we need to go through the entire list (which may take some time).
if not self.elements:
return None
largest = self.elements[0]
for element in self.elements:
if element > largest:
largest = element
return largest
# A SortedImmutableNumberList is the same as an ImmutableNumberList,
# but it changes some aspects.
class SortedImmutableNumberList(ImmutableNumberList):
def __init__(self, elements: Iterable[int]):
# We do extra work here when constructing the list,
# to make sure the elements are sorted.
# This takes more time than the ImmutableNumberList version would.
super().__init__(sorted(elements))
# This method overrides (replaces) the method with the same name on the super-class.
def largest(self) -> Optional[int]:
# Because we know the elements were already sorted in the constructor,
# we can implement finding the largest number faster.
# We don't need to look through every element - we know the largest element is at the end.
# Because we did extra work one time before (in the constructor),
# we can avoid re-doing that work every time someone calls `largest()`.
return self.last()
def max_gap_between_values(self) -> Optional[int]:
if not self.elements:
return None
previous_element = None
max_gap = -1
for element in self.elements:
if previous_element is not None:
gap = element - previous_element
if gap > max_gap:
max_gap = gap
previous_element = element
return max_gap
values = SortedImmutableNumberList([1, 19, 7, 13, 4])
print(values.largest())
print(values.max_gap_between_values())
unsorted_values = ImmutableNumberList([1, 19, 7, 13, 4])
print(unsorted_values.largest())
print(unsorted_values.max_gap_between_values()) # This doesn't work - the superclass doesn't define this method.We have two classes that behave the same. They both have a constructor, and four methods (first, last, largest, length). SortedImmutableNumberList also has an extra method: max_gap_between_values which ImmutableNumberList does not have.
The method implementations are different for the two classes. They have different trade-offs to consider.
✍️exercise
Task 14
A copy of this code is in file 14-analyse.py
Try using this code and make sure you understand how it works and what it does
Answer the following questions, writing your answers in the file, before checking the answers.
Q1: If you know in advance you need to frequently access the largest item of the list, which class will be more efficient and why?
Q2: If you know in advance you will be initialising many of them repeatedly, which class will be more efficient and why?
Expand for some answers after you've listed your own.
Q1: `SortedImmutableNumberList` sorts the numbers in advance, and the method implementation for largest item only needs to look at the final item of the sorted list. This means accessing it is faster. Q2: `ImmutableNumberList` doesn't need to sort the numbers immediately on creation. If you only intended to use `first` and `last`, it may be faster.Of course, it all depends on which functions you think you will need. You will learn more about these efficiency concepts in the upcoming complexity module.
Many programming libraries will have different versions of classes optimised for different tasks, and even if the API to use them is the same, you should be careful considering which one is appropriate for your specific use case. As you develop your own code, you may find it beneficial to extend certain classes to assist with certain tasks, and this may help you maintain your code or make it more efficient. Inheritance is a great way of helping you achieve this.
✍️exercise
Task 15
Look at file 15-playcomputer.py
Play computer with this code
Describe what is happening and why on each line that accesses the person objects
If any lines cause errors, comment out the line and explain why the error happens
Reading
Inheritance is only one way of extending classes. Another technique is called “composition” and this allows you to combine behaviours from many different classes.
Have a read of this article describing the differences between composition and inheritance and this article exploring when each makes sense.