diff --git a/.gitignore b/.gitignore index 3c3629e6..35fe5acc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ node_modules +.venv +.mypy_cache +requirements.txt \ No newline at end of file diff --git a/prep-exercises/a_why_use_types.py b/prep-exercises/a_why_use_types.py new file mode 100644 index 00000000..db2e96f4 --- /dev/null +++ b/prep-exercises/a_why_use_types.py @@ -0,0 +1,42 @@ +# ------------------------ +# Exercise 1 +# ------------------------ +# Q. What does double("22") return? Did it do what you expected? Why did it return the value it did? +# A. I expected the result to be 44 through coercion, like JavaScript. +# It returned the string repeated i.e. 2222 because Python is a strongly typed language and doesn't use coercion + +def double(input): + return input * 2 + +print(double("22")) # returns "2222" + + + +# ------------------------ +# Exercise 2 +# ------------------------ +# Q. Read the code and write down what the bug is. How would you fix it? +# A. In this function it returns the value of 30, which is the input * 3 which is logically correct, however you would +# expect the function to be called "triple" or you would expect the logic to be number * 2, so i would amend +# the function to either of these solutions + +# original function (buggy): +def double(number): + return number * 3 + +print(double(10)) # returns "30" + + +# solution 1 - fix the logic: +def double(number): + return number * 2 + +print(double(10)) # returns "20" + + + +# solution 2 - rename the function: +def triple(number): + return number * 3 + +print(triple(10)) # returns "30" \ No newline at end of file diff --git a/prep-exercises/article_test.py b/prep-exercises/article_test.py new file mode 100644 index 00000000..57d4db49 --- /dev/null +++ b/prep-exercises/article_test.py @@ -0,0 +1,158 @@ +# def double(n: int) -> int: +# return n * 2 + +# num = double(21) +# print(num) + + + +from typing import List, Dict, Counter, Set, Union, Any +from math import sqrt + + +def average(nums: List[int]) -> float: + total = sum(nums) + count = len(nums) + return total / count + +print(average([1, 2, 3, 4])) # 2.5 + + +# from typing import Dict +def get_total_marks(scorecard: Dict[str, int]) -> int: + marks = list(scorecard.values()) # marks : List[int] + return sum(marks) + +scores = {'english': 84, 'maths': 92, 'history': 75} +print(get_total_marks(scores)) # 251 + + +# from typing import Counter, List +def count_occurences(data: List[float]) -> Counter[float]: + occurences = Counter(data) + return occurences + +nums = [2.5, 1.0, 7, 1, 6, 2.5, 1.0] +print(count_occurences(nums)) # Counter({1.0: 3, 2.5: 2, 7: 1, 6: 1}) + + +# from typing import List +def unique_count(nums: List[int]) -> int: + """counts the number of unique items in the list""" + uniques = set() # How does mypy know what type this is? + for num in nums: + uniques.add(num) + + return len(uniques) + +print(unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])) # 4 + + +# from typing import List, Set +def unique_count(nums: List[int]) -> int: + """counts the number of unique items in the list""" + uniques: Set[int] = set() # Manually added type information + for num in nums: + uniques.add(num) + + return len(uniques) + +print(unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1])) # 4 + + +# from typing import List, Set +def unique_count(nums: List[int]) -> int: + """counts the number of unique items in the list""" + uniques: Set[int] = set() + for num in nums: + uniques.add(num) + + return len(uniques) + +counts = unique_count([1, 2, 1, 3, 1, 2, 4, 3, 1]) + +reveal_type(counts) # The special magic reveal_type method - +# you should only use reveal_type to debug your code, and remove it when you're done debugging. + + +def print_favorite_color(person): + fav_color = person.get('favorite-color') + if fav_color is None: + print("You don't have a favorite color. 😿") + else: + print(f"Your favorite color is {fav_color}! 😸.") + +me = {'name': 'Tushar', 'favorite-color': 'Purple'} +print_favorite_color(me) + + +def print_favorite_color(person: Dict[str, str]) -> None: # added types to function definition + fav_color = person.get('favorite-color') + reveal_type(fav_color) # added this line here + if fav_color is None: + print("You don't have a favorite color. 😿") + else: + print(f"Your favorite color is {fav_color}! 😸.") + +me = {'name': 'Tushar', 'favorite-color': 'Purple'} +print_favorite_color(me) + + +def print_item(item): + if isinstance(item, list): + for data in item: + print(data) + else: + print(item) + +print_item('Hi!') +print_item(['This is a test', 'of polymorphism']) + + +def print_item(item: Union[str, List[str]]) -> None: + reveal_type(item) + + if isinstance(item, list): + for data in item: + reveal_type(item) + print(data) + else: + reveal_type(item) + print(item) + +print_item('Hi!') +print_item(['This is a test', 'of polymorphism']) + + +#a type-annotated Python implementation of the builtin function abs: +def my_abs(num: Union[int, float, complex]) -> float: + if isinstance(num, complex): + # absolute value of a complex number is sqrt(i^2 + j^2) + return sqrt(num.real ** 2 + num.imag ** 2) + + else: + return num if num > 0 else -num + +print(my_abs(-5.6)) # 5.6 +print(my_abs(42)) # 42 +print(my_abs(0)) # 0 +print(my_abs(6-8j)) # 10.0 + + +# if you ever try to run reveal_type inside an untyped function, this is what happens: +def average(nums): + total = sum(nums) + count = len(nums) + + ans = total / count + reveal_type(ans) # revealed type says it is 'Any' - 'Any' turns off type checking - so, avoid it if poss! + + +def post_data_to_api(data: Any) -> None: + requests.post('https://example.com/post', json=data) + +data = '{"num": 42, "info": null}' +parsed_data = json.loads(data) +reveal_type(parsed_data) # Revealed type is 'Any' + +post_data_to_api(data) \ No newline at end of file diff --git a/prep-exercises/b_type_checking.py b/prep-exercises/b_type_checking.py new file mode 100644 index 00000000..0e41bca7 --- /dev/null +++ b/prep-exercises/b_type_checking.py @@ -0,0 +1,51 @@ +# ------------------------ +# Exercise 1 +# ------------------------ +# Read the 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. + + + +# added type annotations to balances of dictionary[string, integer] to represent the structure needed, string to name and integer to amount, +# with the return type of none (as there is nothing to return) +def open_account(balances: dict[str, int], name: str, amount: int) -> None: + balances[name] = amount + +# added type annotation of dictionary to the accounts argument because it reflects the balances argument, +# and a return type of integer. +def sum_balances(accounts: dict[str, int]) -> int: + total = 0 + for name, pence in accounts.items(): + print(f"{name} had balance {pence}") + total += pence + return total + +# added integer type annotation and a return type of string for the output and renamed the function +# parameter to avoid shadowing issue with variable name in the outer scope +def format_pence_as_string(total_pence_amount: int) -> str: + if total_pence_amount < 100: + return f"{total_pence_amount}p" + pounds = int(total_pence_amount / 100) + pence = total_pence_amount % 100 + return f"£{pounds}.{pence:02d}" + +# renamed variable from 'balances' to 'account_balances' to avoid shadowing the function parameter +# 'balances' in open_account function. +account_balances = { + "Sima": 700, + "Linn": 545, + "Georg": 831, +} + +# added the balances dictionary as first argument (as required above) +# and altered the amounts types from float 9.13 and string "7.13" to integers +open_account(account_balances, "Tobi", 913) +open_account(account_balances, "Olya", 713) + +total_pence = sum_balances(account_balances) +# corrected the function name (originally was "format_pence_as_str") +total_string = format_pence_as_string(total_pence) + +print(f"The bank accounts total {total_string}") \ No newline at end of file diff --git a/prep-exercises/c_classes_and_objects.py b/prep-exercises/c_classes_and_objects.py new file mode 100644 index 00000000..58fc8a8b --- /dev/null +++ b/prep-exercises/c_classes_and_objects.py @@ -0,0 +1,36 @@ +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) # mypy: Person has no attribute address + + +eliza = Person("Eliza", 34, "Arch Linux") +print(eliza.name) +# print(eliza.address) # mypy: Person has no attribute address + + +def is_adult(person: Person) -> bool: + return person.age >= 18 + + +print(is_adult(imran)) + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Write a new function in the file that accepts a Person as a parameter and tries to access +# a property that doesn’t exist. Run it through mypy and check that it does report an error. + +def is_located_in(person: Person) -> bool: + return person.is_located_in == "Manchester" + +print(is_located_in(eliza)) # prints: AttributeError: 'Person' object has no attribute 'is_located_in' + diff --git a/prep-exercises/d_methods.py b/prep-exercises/d_methods.py new file mode 100644 index 00000000..b2dc0a8d --- /dev/null +++ b/prep-exercises/d_methods.py @@ -0,0 +1,47 @@ +from datetime import date + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Q. Advantages of methods over free functions +# A. Encapsulation - Grouping the data (the attributes) and the behaviour (the methods) together +# means that the code is more organised and easier to understand. It also allows you to hide +# the internal details of how the class works to other parts of the program, so changes +# to the class can be made without affecting external code. Plus, it prevents +# accidental changes to the class's data from outside the class. + +# Ease of documentation - When you use methods, all the actions (functions) related to a specific +# thing (like a Person or a String) are grouped inside the class for that thing. This makes it +# easier to find and understand what that thing can do because everything is in one place. + +# Inheritance and reusability - methods can be inherited and overridden in subclasses, making them +# reusable and offering customization. + +# Access to state - Methods can directly access and modify the data (attributes) of an object using +# self, which free functions cannot do as they are not tied to any specific object. + +# Polymorphism - Methods support this by allowing you to define methods with the same name in +# different classes, you call the same methods on different objects but each with behaviour, +# that is specific to that class. + + +# ------------------------ +# Exercise 2 +# ------------------------ +# Change the Person class to take a date of birth (using the standard library’s datetime.date class) +# and store it in a field instead of age. Update the is_adult method to act the same as before. + +class Person: + def __init__(self, name: str, date_of_birth: date, preferred_operating_system: str): + self.name = name + self.date_of_birth = date_of_birth + self.preferred_operating_system = preferred_operating_system + + def is_adult(self) -> bool: + today = date.today() + age = today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) + return age >= 18 + +imran = Person("Imran", date(2002, 1, 12), "Ubuntu") +print(imran.is_adult()) \ No newline at end of file diff --git a/prep-exercises/e_dataclasses.py b/prep-exercises/e_dataclasses.py new file mode 100644 index 00000000..a5700086 --- /dev/null +++ b/prep-exercises/e_dataclasses.py @@ -0,0 +1,47 @@ +# 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 False +# print(imran) # <__main__.Person object at 0x74b1986e2990> + +from dataclasses import dataclass +from datetime import date + +# @dataclass(frozen=True) +# class Person: +# name: str +# age: int +# preferred_operating_system: str + +# imran = Person("Imran", 22, "Ubuntu") # We can call this constructor - @dataclass generated it for us. +# print(imran) # Prints Person(name='Imran', age=22, preferred_operating_system='Ubuntu') + +# imran2 = Person("Imran", 22, "Ubuntu") +# print(imran == imran2) # Prints True + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Q.Write a Person class using @datatype which uses a datetime.date for date of birth, rather than an int for age. +# Re-add the is_adult method to it. + +@dataclass +class Person: + name: str + date_of_birth: date + preferred_operating_system: str + + def is_adult(self) -> bool: + today = date.today() + age = today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) + return age >= 18 + +imran = Person("Imran", date(2002, 1, 12), "Ubuntu") +print(imran.is_adult()) \ No newline at end of file diff --git a/prep-exercises/f_generics.py b/prep-exercises/f_generics.py new file mode 100644 index 00000000..ab7f9594 --- /dev/null +++ b/prep-exercises/f_generics.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import List, Optional + +# @dataclass(frozen=True) +# class Person: +# name: str +# children: List["Person"] + +# fatma = Person(name="Fatma", children=[]) +# aisha = Person(name="Aisha", children=[]) + +# imran = Person(name="Imran", children=[fatma, aisha]) + +# def print_family_tree(person: Person) -> None: +# print(person.name) +# for child in person.children: +# print(f"- {child.name} ({child.age})") + +# print_family_tree(imran) + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Fix the above code so that it works. You must not change the print on line 17 +# - we do want to print the children’s ages. (Feel free to invent the ages of Imran’s children.) + +@dataclass(frozen=True) +class Person: + name: str + children: List["Person"] + age: Optional[int] = None # add the optional import so that age is optional with a default of None + + +fatma = Person(name="Fatma", age=13, children=[]) # order of arguments doesn't matter when named +aisha = Person("Aisha", [], 8) #however it does if you don't strictly name them + +imran = Person(name="Imran", children=[fatma, aisha]) + +def print_family_tree(person: Person) -> None: + print(person.name) + for child in person.children: + print(f"- {child.name} ({child.age})") + +print_family_tree(imran) \ No newline at end of file diff --git a/prep-exercises/g_type_guided_refactorings.py b/prep-exercises/g_type_guided_refactorings.py new file mode 100644 index 00000000..895040b1 --- /dev/null +++ b/prep-exercises/g_type_guided_refactorings.py @@ -0,0 +1,93 @@ +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}") + + +# ------------------------ +# Exercise 1 +# ------------------------ +# 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, because we’re passing values of the wrong type. +# We probably also want to rename our field - lists are plural. Rename the field to preferred_operating_systems. +# Run mypy again. +# Fix all of the places that mypy tells you need changing. Make sure the program works as you’d expect. + +@dataclass(frozen=True) +class Person: + name: str + age: int + preferred_operating_systems: List[str] + + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: str + +# avoid variable shadowing my changing the internal variable names so that they don't confuse the reader +def find_possible_laptops(available_laptops: List[Laptop], current_person: Person) -> List[Laptop]: + matching_laptops = [] + for laptop in available_laptops: + if laptop.operating_system in current_person.preferred_operating_systems: + matching_laptops.append(laptop) + return matching_laptops + + +people = [ + Person(name="Imran", age=22, preferred_operating_systems=["Ubuntu"]), + Person(name="Eliza", age=34, preferred_operating_systems=["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}") \ No newline at end of file diff --git a/prep-exercises/h_enums.py b/prep-exercises/h_enums.py new file mode 100644 index 00000000..e63e7bed --- /dev/null +++ b/prep-exercises/h_enums.py @@ -0,0 +1,128 @@ +import re +import sys +from dataclasses import dataclass +from enum import Enum +from typing import List +from collections import Counter + +class OperatingSystem(Enum): + MACOS = "macOS" + ARCH = "Arch Linux" + UBUNTU = "Ubuntu" + +@dataclass(frozen=True) +class Person: + name: str + age: int + preferred_operating_systems: OperatingSystem + + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + +def find_possible_laptops(available_laptops: List[Laptop], current_person: Person) -> List[Laptop]: + return [ + laptop for laptop in available_laptops + if laptop.operating_system == current_person.preferred_operating_systems + ] + +laptops = [ + Laptop(id=1, manufacturer="Dell", model="XPS", screen_size_in_inches=13, operating_system=OperatingSystem.ARCH), + Laptop(id=2, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), + Laptop(id=3, manufacturer="Dell", model="XPS", screen_size_in_inches=15, operating_system=OperatingSystem.UBUNTU), + Laptop(id=4, manufacturer="Apple", model="MacBook", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), + Laptop(id=5, manufacturer="Apple", model="MacBook Air", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), + Laptop(id=6, manufacturer="Lenovo", model="ThinkPad", screen_size_in_inches=14, operating_system=OperatingSystem.ARCH), + Laptop(id=7, manufacturer="Asus", model="ZenBook", screen_size_in_inches=13, operating_system=OperatingSystem.UBUNTU), + Laptop(id=8, manufacturer="HP", model="Spectre", screen_size_in_inches=14, operating_system=OperatingSystem.MACOS), + Laptop(id=9, manufacturer="Apple", model="MacBook Pro", screen_size_in_inches=16, operating_system=OperatingSystem.MACOS), +] + +# ------------------------ +# Exercise 1 +# ------------------------ +# Write a program which: +# - Already has a list of Laptops that a library has to lend out. +# - Accepts user input to create a new Person - it should use the input function to read a person’s name, age, +# and preferred operating system. +# - Tells the user how many laptops the library has that have that operating system. +# - If there is an operating system that has more laptops available, tells the user that if they’re willing +# to accept that operating system they’re more likely to get a laptop. +# You should convert the age and preferred operating system input from the user into more constrained types as quickly +# as possible, and should output errors to stderr and terminate the program with a non-zero exit code if the +# user input bad values. + +def user_prompt() -> Person: + try: + name = (input("Please enter your first name: ")).strip() + # use 're' import (regular expression) for regex validation for allowed name characters + if not re.fullmatch(r"[A-Za-z\- ]+", name): + raise ValueError("Name must contain only alphabetic characters, hyphens, or spaces.") + + age = int(input("Please enter your age: ")) + minimum_age = 18 + if age < minimum_age: + raise ValueError(f"Age must be {minimum_age} or over.") + + + # define valid OS options + valid_os = [os.value for os in OperatingSystem] + + # dynamically display the os options (no need for str type here as input always returns a string) + preferred_os = input(f"Please enter correct operating system name, either {', '.join(valid_os)}: ").strip() + + # validate the OS + if preferred_os not in valid_os: + raise ValueError("Invalid operating system.") + + # convert to enum + preferred_os_enum = OperatingSystem(preferred_os) + + return Person(name=name, age=age, preferred_operating_systems=preferred_os_enum) + + # throw an error and exit for invalid age and os input + except ValueError as error: + print(f"Invalid input: {error}", file=sys.stderr) + sys.exit(1) + + + +def main() -> None: + + user_details = user_prompt() + + matching_laptops = find_possible_laptops(laptops, user_details) + + # get the counts of the laptops for an OS e.g. os_counts = {OperatingSystem.MACOS: 4,OperatingSystem.ARCH: 2,OperatingSystem.UBUNTU: 3} + os_counts: Counter[OperatingSystem] = Counter(laptop.operating_system for laptop in laptops) + + print(f"\n{user_details.name}, there are {len(matching_laptops)} laptops available with your preferred operating system ({user_details.preferred_operating_systems.value}).\n") + + if matching_laptops: + print("Matching laptops:") + for laptop in matching_laptops: + print(f"- {laptop.manufacturer} {laptop.model} ({laptop.screen_size_in_inches}\" screen)") + else: + print("No laptops available with your preferred operating system.") + + # get the count of laptops for users preferred OS + user_os_count = os_counts[user_details.preferred_operating_systems] + + # sort alternative OS so most available OS is listed first + alternative_os = sorted([(os, count) for os, count in os_counts.items() if count > user_os_count], key=lambda x: x[1], reverse=True) + + if alternative_os: + print("\nHowever, there are more laptops available with the following operating systems:") + for os, count in alternative_os: + print(f"- {os.value}: {count} laptops") + print("\nIf you consider one of these options, you will have a greater chance of getting a laptop.") + else: + print("Your preferred operating system has the most laptops available.") + +if __name__ == "__main__": + main() diff --git a/prep-exercises/i_inheritance.py b/prep-exercises/i_inheritance.py new file mode 100644 index 00000000..6d32e830 --- /dev/null +++ b/prep-exercises/i_inheritance.py @@ -0,0 +1,74 @@ +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: list[str] = [] + + def change_last_name(self, last_name: str) -> 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}" + +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()) + + +# ------------------------ +# Exercise 1 +# ------------------------ +# Play computer with this code. Predict what you expect each line will do. Then run the code and check your predictions. ( +# If any lines cause errors, you may need to comment them out to check later lines). + +# Line 1: Create the class Parent +# Line 2: Define the constructor method (__init__) that takes self, a first_name value with a type of str, and last_name value with a type of str +# Line 3: Creates the first_name field of the object +# Line 4: Creates the last_name field of the object +# Line 6: Define a get_name method that returns a string +# Line 7: Returns a formatted string of the object's first_name and last_name +# Line 10: Create the subclass Child (inherits from the Parent class) +# Line 11: Define the Child class constructor method and pass in the same arguments of a first_name with a type of str, and last_name with a type of str +# Line 12: Call super() to run the Parent constructor method and set up first_name and last_name +# Line 13: Creates a previous_last_names field (which is unique to the Child and not seen in the Parent class) and sets it to an empty list +# Line 15: Define a change_last_name method that takes a new last_name of type str and doesn't return anything as annotated by the type None +# Line 16: Uses the append method to append the current last_name value to the previous_last_names list +# Line 17: Update the object's last_name field with the new last_name value that was passed in +# Line 19: Define a get_full_name method that returns a string type +# Line 20: Creates a local variable called suffix and sets it to an empty string value +# Line 21: Checks if previous_last_names list has any items (names) by checking the length (checks if it is not empty (i.e. greater than 0)) +# Line 22: If it does set the suffix variable to a formatted string that has the word 'née' first then the original last_name value of the previous last name (at the first position in the previous_last_name list) +# Line 23: Returns and formatted string of the Child object's first_name and last_name and adds the suffix at the end + +# For person1: I predict that we will see "Elizaveta Alekseeva" as the first print output for get.name() +# For get_full_name I predict we will see just "Elizaveta Alekseeva" as there has been no other last names passed to the Child as yet +# Once change_last_name has been passed a new name, I expect to see "Elizaveta Tyurina" +# For get_full_name I predict we will see "Elizaveta Tyurina (née Alekseeva)" + +# For person2: I predict that we will see "Elizaveta Alekseeva" for the get.name() +# I imagine there will be an error for print(person2.get_full_name()) because the Parent does not have a get_full_name method +# I predict it will fail for person2.change_last_name("Tyurina") for the same reason as the object is a parent object that has no change_last_name method +# By commenting out the previous 2 lines of code ,the next get.name() will repeat the first print output +# The final get_full_name output will fail again as before because this method doesn't exist in the Parent class.