diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c72191 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python +__pycache__/ +.mypy_cache/ +.venv/ + +# Node +node_modules/ + +# Mac +.DS_Store \ No newline at end of file diff --git a/classes_and_objects.py b/classes_and_objects.py new file mode 100644 index 0000000..501049a --- /dev/null +++ b/classes_and_objects.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") +eliza = Person("Eliza", 34, "Arch Linux") + +print(imran.name) +# print(imran.address) # mypy error: Person has no attribute "address" + +print(eliza.name) +# print(eliza.address) # mypy error again + +def is_adult(person: Person) -> bool: + return person.age >= 18 + +print(is_adult(imran)) # no mypy error + +def print_address(person: Person) -> None: + print(person.address) # mypy will catch this too + +# I learned that a class defines what attributes each object will have. +# Mypy can check if I try to access an attribute that doesn't exist. +# It helps to avoid mistakes like typing person.addres instead of person.address. diff --git a/dataclasses_.py b/dataclasses_.py new file mode 100644 index 0000000..fefc9ac --- /dev/null +++ b/dataclasses_.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from datetime import date + +@dataclass(frozen=True) +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, 5, 15), "Ubuntu") +imran2 = Person("Imran", date(2002, 5, 15), "Ubuntu") + +print(imran) # Person(name='Imran', date_of_birth=datetime.date(2002, 5, 15), preferred_operating_system='Ubuntu') +print(imran == imran2) # True +print(imran.is_adult()) # True diff --git a/enums.py b/enums.py new file mode 100644 index 0000000..ffb81a4 --- /dev/null +++ b/enums.py @@ -0,0 +1,56 @@ +import sys +from enum import Enum +from dataclasses import dataclass + +class OperatingSystem(Enum): + MACOS = "macOS" + ARCH = "Arch Linux" + UBUNTU = "Ubuntu" + +@dataclass(frozen=True) +class Laptop: + id: int + manufacturer: str + model: str + screen_size_in_inches: float + operating_system: OperatingSystem + +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="Apple", model="MacBook", screen_size_in_inches=13, operating_system=OperatingSystem.MACOS), + Laptop(id=4, manufacturer="Apple", model="MacBook Air", screen_size_in_inches=15, operating_system=OperatingSystem.MACOS), +] + +# user input +try: + name = input("Enter your name: ") + age = int(input("Enter your age: ")) + os_input = input("Enter your preferred operating system (macOS / Arch Linux / Ubuntu): ") + + # Convert string to enum + preferred_os = OperatingSystem(os_input) +except ValueError: + print("Invalid input. Please check your age or operating system name.", file=sys.stderr) + sys.exit(1) + +# count available laptops +matching_laptops = [l for l in laptops if l.operating_system == preferred_os] +print(f"{name}, there are {len(matching_laptops)} laptops available with {preferred_os.value}.") + +# suggest alternative OS if another has more laptops +os_counts = { + os: sum(1 for l in laptops if l.operating_system == os) + for os in OperatingSystem +} + +most_common_os = max(os_counts, key=lambda os: os_counts[os]) + +if most_common_os != preferred_os: + print(f"If you can use {most_common_os.value}, you have a higher chance of getting a laptop.") + + +# I learned how to use Enums to make my code safer and avoid typos. +# I also learned how to handle invalid user input safely using try and except. +# Using lambda helps mypy understand the type more clearly. +# Without it, mypy didn't know what os_counts.get returns. diff --git a/generics.py b/generics.py new file mode 100644 index 0000000..908eafb --- /dev/null +++ b/generics.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import List +from datetime import date + +@dataclass(frozen=True) +class Person: + name: str + birth_year: int + children: List["Person"] + + @property + def age(self) -> int: + current_year = date.today().year + return current_year - self.birth_year + +fatma = Person(name="Fatma", birth_year=2018, children=[]) +aisha = Person(name="Aisha", birth_year=2020, children=[]) +imran = Person(name="Imran", birth_year=1990, children=[fatma, aisha]) + +def print_family_tree(person: Person) -> None: + print(f"{person.name} ({person.age})") + for child in person.children: + print(f"- {child.name} ({child.age})") + +print_family_tree(imran) diff --git a/inheritance.py b/inheritance.py new file mode 100644 index 0000000..05a4712 --- /dev/null +++ b/inheritance.py @@ -0,0 +1,48 @@ +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): + # Call the parent class constructor + super().__init__(first_name, last_name) + self.previous_last_names = [] + + 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()) # from Parent class +print(person1.get_full_name()) # from Child class + +person1.change_last_name("Tyurina") +print(person1.get_name()) +print(person1.get_full_name()) + +person2 = Parent("Elizaveta", "Alekseeva") +print(person2.get_name()) + +# The next lines will cause errors, because Parent doesn't have these methods +# print(person2.get_full_name()) +# person2.change_last_name("Tyurina") + +print(person2.get_name()) +# print(person2.get_full_name()) + + +# I learned that Parent and Child classes can have different methods — Parent can’t access methods only defined in Child +# I understood that the child class can add new methods or override existing ones + diff --git a/methods_advantages.py b/methods_advantages.py new file mode 100644 index 0000000..9a043b9 --- /dev/null +++ b/methods_advantages.py @@ -0,0 +1,19 @@ +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) -> bool: + return self.age >= 18 + + +imran = Person("Imran", 22, "Ubuntu") + +print(imran.is_adult()) # True + +# Advantages of using methods instead of free functions: +# 1. Easier documentation - all related behaviors are attached to the class. +# 2. Encapsulation - data and logic are kept together in one place. +# 3. Easier maintenance - when class details change, methods can be updated easily. +# 4. Clearer code - you can write person.is_adult() instead of is_adult(person). \ No newline at end of file diff --git a/methods_with_date_of_birth.py b/methods_with_date_of_birth.py new file mode 100644 index 0000000..613491f --- /dev/null +++ b/methods_with_date_of_birth.py @@ -0,0 +1,21 @@ +from datetime import date + +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, 5, 15), "Ubuntu") +print(imran.is_adult()) # True + +# I learned that methods can easily adapt to changes inside a class. +# Now the class stores a date of birth instead of age, but the method still works correctly. diff --git a/type_checking_with_mypy.py b/type_checking_with_mypy.py new file mode 100644 index 0000000..76e29a4 --- /dev/null +++ b/type_checking_with_mypy.py @@ -0,0 +1,30 @@ +def open_account(balances: dict[str, int], name: str, amount: int) -> None: + balances[name] = amount + +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 + +def format_pence_as_string(total_pence: int) -> str: + 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(balances, "Tobi", 913) +open_account(balances, "Olya", 713) + +total_pence = sum_balances(balances) +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/type_guided_refactorings.py b/type_guided_refactorings.py new file mode 100644 index 0000000..16703b8 --- /dev/null +++ b/type_guided_refactorings.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from typing import List + +@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 + + +def find_possible_laptops(laptops: List[Laptop], person: Person) -> List[Laptop]: + possible_laptops = [] + for laptop in laptops: + if laptop.operating_system in person.preferred_operating_systems: + possible_laptops.append(laptop) + return possible_laptops + + +people = [ + Person(name="Imran", age=22, preferred_operating_systems=["Ubuntu", "Arch Linux"]), + 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}") + + +# I learned how type checking helps when refactoring code. +# Mypy shows exactly which parts of the code need to change when we rename or change a field type. +# This makes large codebases easier to update safely. \ No newline at end of file diff --git a/why_we_use_types.py b/why_we_use_types.py new file mode 100644 index 0000000..fe01a68 --- /dev/null +++ b/why_we_use_types.py @@ -0,0 +1,33 @@ +def half(value): + return value / 2 + +def double(value): + return value * 2 + +def second(value): + return value[1] + + +print(half(22)) # My prediction was correct. It returned 11 +# print(half("hello")) # My prediction was correct. It gave an error since it is a string +# print(half("22")) # # I thought maybe Python would see it as a number and return 11 + + +print(double(22)) # My prediction was correct. It returned 44 +print(double("hello")) # I expected an error +print(double("22")) # I thought maybe Python would treat it as a number and return 44 + +# print(second(22)) # # My prediction was correct. It gave an error since it is a number +# print(second(0x16)) # # My prediction was correct. It gave an error +print(second("hello")) # My prediction was correct. It returned 'e' +print(second("22")) # My prediction was correct. It returned '2' + +def double1(number): + return number * 3 + +print(double1(10)) +# When you check the function name, it doesn’t fit what we have to expect. +# A better name would be 'triple' or 'multiply_by_three'. +# If you use this func you would expect it to give you double like 20, not 30. +# It might cause a problem for your code. +