diff --git a/sprint-5-stretch/README.md b/sprint-5-stretch/README.md new file mode 100644 index 000000000..a5888c97d --- /dev/null +++ b/sprint-5-stretch/README.md @@ -0,0 +1,14 @@ +# Sprint 5 Stretch - Radio Control App + +This is part of an app that can be used to control an online radio station + +Your task is to: +1. Upgrade everything use dataclasses +1. Observe how the given `__str__` method works, and add one to the other classes +1. When the `__str__` method is called, have it print the duration in formatted mm:ss rather than just seconds +1. Add a new class of `Person` - you can use your imagination what this includes, but it should have at least a string name +1. Turn the fields for _artists_, _host_, and _guest_ into `Person` classes +1. Make a new class `Podcast` that _extends_ the `TalkShow` class, with an extra string field for _sponsor_ +1. Finish implementing the class `Playlist`, and set it to takes a generic, e.g. either `Song` or `TalkShow` or `Podcast` + +As you implement, make sure to keep checking with mypy and add your own code at the end to test it works correctly diff --git a/sprint-5-stretch/requirements.txt b/sprint-5-stretch/requirements.txt new file mode 100644 index 000000000..f0aa93ac8 --- /dev/null +++ b/sprint-5-stretch/requirements.txt @@ -0,0 +1 @@ +mypy diff --git a/sprint-5-stretch/stretch-radio.py b/sprint-5-stretch/stretch-radio.py new file mode 100644 index 000000000..562a13500 --- /dev/null +++ b/sprint-5-stretch/stretch-radio.py @@ -0,0 +1,43 @@ +class Song: + def __init__(self, artist, duration, title): + self.artist = artist # should be a string + self.duration = duration # should be an integer in seconds + self.title = title # should be a string + + def __str__(self): + return f"{self.artist} - {self.title} ({self.duration})" + +class TalkShow: + def __init__(self, host, guests, duration, title): + self.host = host # should be a string + self.guests = guests # should be a list of strings + self.duration = duration # should be an integer in seconds + self.title = title # should be a string + +class Playlist: + def __init__(self, items): + self.items = items # A list of Song or TalkShow + self.currently_playing_index = 0 + + def play_next(self): + pass + # TODO: increment the currently_playing_index, when you get to the end loop back to 0 + + def get_currently_playing(self): + return "todo" + # TODO: return a string representation of whatever is playing at the current index + # See the __str__ method in Song for an example of how to make a string representation of a class + + def get_total_duration(self): + return 0 + # TODO: return + +song_a = Song("Rick Astley", 210, "Never Gonna Give You Up") +print(song_a) + +song_b = Song("Psy", 219, "Gangnam Style") +funny_songs = Playlist([song_a, song_b]) +print(funny_songs.get_currently_playing()) + +show_a = TalkShow("Melvyn Bragg", ["Jim Al-Khalili", "Sheila Rowan", "Carolin Crawford"], 2700, "Gravitational Waves") + diff --git a/sprint-5/01-predict.py b/sprint-5/01-predict.py new file mode 100644 index 000000000..5c9b9105e --- /dev/null +++ b/sprint-5/01-predict.py @@ -0,0 +1,41 @@ +def half(value): + return value / 2 + +def double(value): + return value * 2 + +def second(value): + return value[1] + +# TASK 1: +# 1. Predict what you think will happen with each of the following functions +# 2. Then, test it, and explain in your own words what is actually happening and why +# (feel free to comment out lines if you think they cause errors or crashes while testing) + +print(half(22)) +# Prediction: +# What actually happens and why: + +print(half("22")) +# Prediction: +# What actually happens and why: + +print(double(22)) +# Prediction: +# What actually happens and why: + +print(double("22")) +# Prediction: +# What actually happens and why: + +print(second(22)) +# Prediction: +# What actually happens and why: + +print(second(0x16)) +# Prediction: +# What actually happens and why: + +print(second("22")) +# Prediction: +# What actually happens and why: diff --git a/sprint-5/02-playcomputer.js b/sprint-5/02-playcomputer.js new file mode 100644 index 000000000..b0bc1c733 --- /dev/null +++ b/sprint-5/02-playcomputer.js @@ -0,0 +1,29 @@ +import process from "node:process"; +import readline from "node:readline"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +rl.question("What URL should we fetch?\n> ", async (url) => { + const response = await fetch(url); + if (!response.ok) { + if (response.body.toLowerCase().includes("permission")) { + console.error("You didn't have permission to get that URL"); + } else { + console.error(`The request failed - body: ${response.body}`); + } + process.exit(1); + } + + const contents = await response.json(); + + console.log(contents); + + rl.close(); +}); + +// TASK 2 +// Read through this file and predict what it does. +// Leave a comment on any lines if you spot any errors, offering an explanation of the problem. diff --git a/sprint-5/03-fix.py b/sprint-5/03-fix.py new file mode 100644 index 000000000..3a558945c --- /dev/null +++ b/sprint-5/03-fix.py @@ -0,0 +1,9 @@ +def double(number): + return number * 3 + +print(double(10)) + +# TASK 3: +# 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? diff --git a/sprint-5/04-addmypy.py b/sprint-5/04-addmypy.py new file mode 100644 index 000000000..57f00cfee --- /dev/null +++ b/sprint-5/04-addmypy.py @@ -0,0 +1,37 @@ +def open_account(balances, name, amount): + balances[name] = amount + +def sum_balances(accounts): + total = 0 + for name, pence in accounts.items(): + print(f"{name} had balance {pence}") + total += pence + return total + +def format_pence_as_string(total_pence): + if total_pence < 100: + return f"{total_pence}p" + pounds = int(total_pence / 100) + pence = total_pence % 100 + return f"£{pounds}.{pence:02d}" + +balances = { + "Sima": 700, + "Linn": 545, + "Georg": 831, +} + +open_account("Tobi", 9.13) +open_account("Olya", "£7.13") + +total_pence = sum_balances(balances) +total_string = format_pence_as_str(total_pence) + +print(f"The bank accounts total {total_string}") + +# TASK 4 +# 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. diff --git a/sprint-5/05-predict.py b/sprint-5/05-predict.py new file mode 100644 index 000000000..a66f71253 --- /dev/null +++ b/sprint-5/05-predict.py @@ -0,0 +1,24 @@ +imran = { + "name": "Imran", + "age": 22, + "preferred_operating_system": "Ubuntu", +} + +eliza = { + "name": "Eliza", + "age": 34, + "preferred_operating_system": "Arch Linux", +} + + +print(imran["name"]) +print(imran["address"]) + +# TASK 5: +# 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. +# +# Prediction: +# +# Can you explain what actualy happens? +# diff --git a/sprint-5/06-classes.py b/sprint-5/06-classes.py new file mode 100644 index 000000000..9f0814104 --- /dev/null +++ b/sprint-5/06-classes.py @@ -0,0 +1,27 @@ +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) + +# TASK 6.1: +# Run mypy and fix any errors. + +# TASK 6.2: +# Create a new function in this file called likes_apple +# It should take a person as parameter +# It returns true if the preferred operating system is "macOS" or "iOS" +# It should return false for any other preferred os +# Add all the appropriate type annotations and test it has no errors in mypy + +# TASK 6.3: +# Compare objects and classes +# What are some advantages and disadvantages of each? diff --git a/sprint-5/07-methods.txt b/sprint-5/07-methods.txt new file mode 100644 index 000000000..13325e967 --- /dev/null +++ b/sprint-5/07-methods.txt @@ -0,0 +1,5 @@ +TASK 7: +Answer the following questions: +What is the difference between a method and a function? +Can you give some advantages of methods over functions? + diff --git a/sprint-5/08-implement.py b/sprint-5/08-implement.py new file mode 100644 index 000000000..f29a5ddff --- /dev/null +++ b/sprint-5/08-implement.py @@ -0,0 +1,28 @@ +class Person: + def __init__(self, name: str, age: int, preferred_car_brand: str): + self.name = name + self.age = age + self.preferred_car_brand = preferred_car_brand + +def drivers_license_check(person: Person): + if person.is_adult() == True: + return 'Valid drivers license' + + return 'This person is underage!' + +imran = Person("Imran", 22, "Mercedes") +print(drivers_license_check(imran)) # should return 'Valid drivers license' + +# TASK 8: +# +# Add an `is_adult` method into the class, and make sure your code gives the expected output. +# +# Change the `Person` class to take a date of birth using +# the standard library's `datetime.date` class +# https://docs.python.org/3/library/datetime.html#datetime.date)) +# and store the `date of birth` instead of `age`. +# +# Try to run your code now and observe how this change breaks your code. +# What kind of error do you get? Is it helpful in identifying where your next change needs to be? +# +# Now update only the `is_adult` method to fix the error and check everything works correctly. diff --git a/sprint-5/09-encapsulation.py b/sprint-5/09-encapsulation.py new file mode 100644 index 000000000..115c01c90 --- /dev/null +++ b/sprint-5/09-encapsulation.py @@ -0,0 +1,33 @@ +# TASK 9 +# +# Having done some research on encapsulation, think about the benefits. +# +# Think of some examples and in your own words write down some benefits and trade-offs of using encapsulation in classes: +# +# +# + +# STRETCH TASK 9.1 +# +# In the following class, make the name property private +# Add a `get_name()` method to allow read-only access. + +class Person: + def __init__(self, name: str, age: int): + self.name = name + self.__age = age + + def is_adult(self): + return self.__age >= 18 + +imran = Person("Imran", 22) +print(imran.name) +print(imran.age) # fails +print(imran.__age) # fails +print(imran.is_adult()) # works and prints True + +eliza = Person("Eliza", 12) +print(eliza.name) +print(eliza.age) # fails +print(imran.__age) # fails +print(eliza.is_adult()) # works and prints False diff --git a/sprint-5/10-dataclass.py b/sprint-5/10-dataclass.py new file mode 100644 index 000000000..3847d07d8 --- /dev/null +++ b/sprint-5/10-dataclass.py @@ -0,0 +1,14 @@ +# TASK 10.1: +# Copy the code you have so far from task 08-implement.py +# Convert your existing `Person` class from task 8 into a value type using `@datatype` +# so you can print the class (and see it's type and properties) +# and compare class instances that are identical. +# Make sure your `is_adult` method and `drivers_license_check` free function both work as normal. + +# TASK 10.2: +# Make a new method on your Person class - `greet()` which should return `"Hello !"` when used. + +# TASK 10.3: +# Read the @datatype documentation here: https://docs.python.org/3/library/dataclasses.html +# Explain what `frozen=True` does to the class? +# What other options could you play around with and explore? Offer suggestions for any that would be useful here. diff --git a/sprint-5/11-predict.py b/sprint-5/11-predict.py new file mode 100644 index 000000000..b022f3b43 --- /dev/null +++ b/sprint-5/11-predict.py @@ -0,0 +1,35 @@ +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) + +# TASK 11 +# 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. diff --git a/sprint-5/12-fix.py b/sprint-5/12-fix.py new file mode 100644 index 000000000..c1b35296f --- /dev/null +++ b/sprint-5/12-fix.py @@ -0,0 +1,59 @@ +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() + +# TASK 12: +# We are going to improve the printing in the above code, you can find a copy in `12-fix.py`. +# +# Experiment with mypy and make sure that the family tree only takes `Person` types and the species tree only takes `Animal` types. +# +# Currently the `Tree.print_tree()` method doesn't look very pretty. +# +# Change only the Animal and Person classes 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 12.2 +# +# 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. diff --git a/sprint-5/13-refactor.py b/sprint-5/13-refactor.py new file mode 100644 index 000000000..4050324cf --- /dev/null +++ b/sprint-5/13-refactor.py @@ -0,0 +1,53 @@ +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}") + +# Task 13 +# Change 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 remove any errors. + +# Now we changed the types, we probably also want to _rename_ our field 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. diff --git a/sprint-5/14-implement.py b/sprint-5/14-implement.py new file mode 100644 index 000000000..75a9bba7b --- /dev/null +++ b/sprint-5/14-implement.py @@ -0,0 +1,57 @@ +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"), +] + +laptops = [ + Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system="arch"), + 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}") + +# TASK 14 +# The above code 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 +# https://docs.python.org/3/library/functions.html#input +# 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. diff --git a/sprint-5/15-analyse.py b/sprint-5/15-analyse.py new file mode 100644 index 000000000..2204fb934 --- /dev/null +++ b/sprint-5/15-analyse.py @@ -0,0 +1,82 @@ +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. + + +# TASK 15 +# 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? +# diff --git a/sprint-5/16-playcomputer.py b/sprint-5/16-playcomputer.py new file mode 100644 index 000000000..20197341b --- /dev/null +++ b/sprint-5/16-playcomputer.py @@ -0,0 +1,42 @@ +class Parent: + def __init__(self, first_name: str, last_name: str): + self.first_name = first_name + self.last_name = last_name + + def get_name(self) -> str: + return f"{self.first_name} {self.last_name}" + + +class Child(Parent): + def __init__(self, first_name: str, last_name: str): + super().__init__(first_name, last_name) + self.previous_last_names = [] + + def change_last_name(self, last_name) -> None: + self.previous_last_names.append(self.last_name) + self.last_name = last_name + + def get_full_name(self) -> str: + suffix = "" + if len(self.previous_last_names) > 0: + suffix = f" (née {self.previous_last_names[0]})" + return f"{self.first_name} {self.last_name}{suffix}" + +# TASK 16: +# Play computer with this code +# Describe what is happening and why on each line below here +# If any lines cause errors, comment out the line and explain why the error happens + +person1 = Child("Elizaveta", "Alekseeva") +print(person1.get_name()) +print(person1.get_full_name()) +person1.change_last_name("Tyurina") +print(person1.get_name()) +print(person1.get_full_name()) + +person2 = Parent("Elizaveta", "Alekseeva") +print(person2.get_name()) +print(person2.get_full_name()) +person2.change_last_name("Tyurina") +print(person2.get_name()) +print(person2.get_full_name()) diff --git a/sprint-5/README.md b/sprint-5/README.md new file mode 100644 index 000000000..ffd9d59d8 --- /dev/null +++ b/sprint-5/README.md @@ -0,0 +1,11 @@ +# Sprint 5 + +For this task, read through the prep material for sprint 5. + +Sprint 5 is all about types in prorgamming. + +This directory has some example code and exercises the prep will refer to. + +As you read through the prep, complete the tasks here. + +Then submit as a PR once you are finished. diff --git a/sprint-5/requirements.txt b/sprint-5/requirements.txt new file mode 100644 index 000000000..f0aa93ac8 --- /dev/null +++ b/sprint-5/requirements.txt @@ -0,0 +1 @@ +mypy