commit 10fc3566bba38df4c666b359d2b7769df286f1a1 Author: balshaer1 Date: Tue Dec 30 08:50:00 2025 +0200 Initial commit: Python by Example structure diff --git a/01_basics/01_print.py b/01_basics/01_print.py new file mode 100644 index 0000000..324e7ee --- /dev/null +++ b/01_basics/01_print.py @@ -0,0 +1,223 @@ +""" +================================================================================ +File: 01_print.py +Topic: The print() Function in Python +================================================================================ + +This file demonstrates the print() function, which is used to display output +to the console. It's one of the most fundamental functions in Python and +essential for debugging and displaying information. + +Key Concepts: +- Basic printing +- Multiple arguments +- Separators and end characters +- Formatted strings (f-strings) +- Escape characters + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Simple Output +# ----------------------------------------------------------------------------- +# The most basic use of print() + +print("--- Simple Output ---") + +print("Hello, World!") +print("Welcome to Python!") +print("Learning is fun!") + +# ----------------------------------------------------------------------------- +# 2. Printing Different Data Types +# ----------------------------------------------------------------------------- +# print() can output any data type + +print("\n--- Different Data Types ---") + +print(42) # Integer +print(3.14159) # Float +print(True) # Boolean +print(None) # NoneType +print([1, 2, 3]) # List +print({"a": 1}) # Dictionary + +# ----------------------------------------------------------------------------- +# 3. Printing Multiple Items +# ----------------------------------------------------------------------------- +# Pass multiple arguments separated by commas + +print("\n--- Multiple Items ---") + +print("Hello", "World") +print("Python", "is", "awesome") +print("Name:", "Baraa", "| Age:", 25) +print(1, 2, 3, 4, 5) + +# ----------------------------------------------------------------------------- +# 4. The sep Parameter +# ----------------------------------------------------------------------------- +# sep defines what goes between multiple items (default is space) + +print("\n--- Separator Parameter ---") + +print("Python", "Java", "C++", sep=", ") +print("2025", "01", "15", sep="-") # Date format +print("192", "168", "1", "1", sep=".") # IP address +print("apple", "banana", "cherry", sep=" | ") +print("a", "b", "c", sep="") # No separator + +# ----------------------------------------------------------------------------- +# 5. The end Parameter +# ----------------------------------------------------------------------------- +# end defines what goes at the end (default is newline \n) + +print("\n--- End Parameter ---") + +print("Loading", end="") +print("...", end="") +print(" Done!") + +print("First line", end=" --> ") +print("Second line") + +# Creating a progress bar effect +print("\nProgress: ", end="") +for i in range(5): + print("█", end="") +print(" Complete!") + +# ----------------------------------------------------------------------------- +# 6. Variables in Print +# ----------------------------------------------------------------------------- +# Print variable values + +print("\n--- Variables ---") + +name = "Baraa" +age = 25 +city = "Gaza" +is_student = True + +print("Name:", name) +print("Age:", age) +print("City:", city) +print("Student:", is_student) + +# Print with variable calculations +x = 10 +y = 5 +print("Sum:", x + y) +print("Product:", x * y) + +# ----------------------------------------------------------------------------- +# 7. String Formatting - f-strings (Recommended) +# ----------------------------------------------------------------------------- +# Modern way to embed variables in strings (Python 3.6+) + +print("\n--- f-strings (Formatted String Literals) ---") + +name = "Baraa" +age = 25 +height = 1.75 + +print(f"My name is {name}") +print(f"I am {age} years old") +print(f"Next year I'll be {age + 1}") + +# Formatting numbers +pi = 3.14159265359 +print(f"Pi to 2 decimals: {pi:.2f}") +print(f"Pi to 4 decimals: {pi:.4f}") + +# Padding and alignment +print(f"{'Left':<10}|{'Center':^10}|{'Right':>10}") +print(f"{1:<10}|{2:^10}|{3:>10}") + +# Currency formatting +price = 1234.567 +print(f"Price: ${price:,.2f}") + +# ----------------------------------------------------------------------------- +# 8. String Formatting - Other Methods +# ----------------------------------------------------------------------------- +# Alternative formatting methods + +print("\n--- Other Formatting Methods ---") + +# .format() method +name = "Alice" +age = 30 +print("Hello, {}! You are {} years old.".format(name, age)) +print("Hello, {0}! You are {1} years old.".format(name, age)) +print("Hello, {n}! You are {a} years old.".format(n=name, a=age)) + +# % operator (older style) +print("Hello, %s! You are %d years old." % (name, age)) + +# ----------------------------------------------------------------------------- +# 9. Escape Characters +# ----------------------------------------------------------------------------- +# Special characters using backslash \ + +print("\n--- Escape Characters ---") + +print("Line 1\nLine 2\nLine 3") # \n = newline +print("Column1\tColumn2\tColumn3") # \t = tab +print("She said: \"Hello!\"") # \" = quote +print('It\'s a beautiful day') # \' = apostrophe +print("Path: C:\\Users\\Documents") # \\ = backslash +print("Bell sound: \a") # \a = bell (may not work) + +# Raw strings - ignore escape characters +print("\nRaw string:") +print(r"C:\Users\Baraa\Desktop") # r prefix for raw string + +# ----------------------------------------------------------------------------- +# 10. Multi-line Printing +# ----------------------------------------------------------------------------- + +print("\n--- Multi-line Strings ---") + +# Using triple quotes +message = """ +This is a multi-line message. +It spans across several lines. +Very useful for long text! +""" +print(message) + +# ASCII art example +print(""" + ╔═══════════════════════════╗ + ║ Welcome to Python! ║ + ║ Let's learn together! ║ + ╚═══════════════════════════╝ +""") + +# ----------------------------------------------------------------------------- +# 11. Practical Examples +# ----------------------------------------------------------------------------- + +print("--- Practical Examples ---") + +# Receipt example +print("\n========== RECEIPT ==========") +item1, price1 = "Coffee", 4.99 +item2, price2 = "Sandwich", 8.50 +item3, price3 = "Cookie", 2.25 +total = price1 + price2 + price3 + +print(f"{item1:.<20}${price1:.2f}") +print(f"{item2:.<20}${price2:.2f}") +print(f"{item3:.<20}${price3:.2f}") +print("=" * 30) +print(f"{'TOTAL':.<20}${total:.2f}") + +# Table example +print("\n| Name | Age | City |") +print("|----------|-----|------------|") +print(f"| {'Alice':<8} | {25:<3} | {'New York':<10} |") +print(f"| {'Bob':<8} | {30:<3} | {'London':<10} |") +print(f"| {'Charlie':<8} | {35:<3} | {'Tokyo':<10} |") diff --git a/01_basics/02_comments.py b/01_basics/02_comments.py new file mode 100644 index 0000000..75cf013 --- /dev/null +++ b/01_basics/02_comments.py @@ -0,0 +1,343 @@ +""" +================================================================================ +File: 02_comments.py +Topic: Comments in Python +================================================================================ + +This file demonstrates how to write comments in Python. Comments are essential +for code documentation, making your code readable, and helping others (and +your future self) understand what the code does. + +Key Concepts: +- Single-line comments +- Multi-line comments +- Docstrings +- Best practices for commenting + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Single-Line Comments +# ----------------------------------------------------------------------------- +# Use the hash symbol (#) to create single-line comments + +print("--- Single-Line Comments ---") + +# This is a single-line comment +print("Hello, World!") # This is an inline comment + +# Comments can explain complex logic +x = 10 # Store the initial value +y = 20 # Store the second value +result = x + y # Calculate the sum + +# Comments can be used to organize sections of code +# ---- Configuration ---- +debug_mode = True +max_retries = 3 + +# ---- Main Logic ---- +print(f"Debug mode is: {debug_mode}") + +# ----------------------------------------------------------------------------- +# 2. Multi-Line Comments +# ----------------------------------------------------------------------------- +# Python doesn't have true multi-line comments, but there are two approaches + +print("\n--- Multi-Line Comments ---") + +# Approach 1: Multiple single-line comments (preferred) +# This is a multi-line comment +# that spans across multiple lines. +# Each line starts with a hash symbol. + +# Approach 2: Triple-quoted strings (not recommended for comments) +# These are actually string literals, not true comments +""" +This looks like a multi-line comment, but it's actually +a string literal. If not assigned to a variable, +Python will ignore it, but it's still stored in memory. +Use this approach only for docstrings. +""" + +print("Multi-line comments explained above!") + +# ----------------------------------------------------------------------------- +# 3. Docstrings (Documentation Strings) +# ----------------------------------------------------------------------------- +# Docstrings are special strings used to document modules, functions, classes + +print("\n--- Docstrings ---") + + +def greet(name): + """ + Greet a person by their name. + + This function takes a person's name and prints a friendly + greeting message to the console. + + Args: + name (str): The name of the person to greet. + + Returns: + str: A greeting message. + + Examples: + >>> greet("Baraa") + 'Hello, Baraa! Welcome!' + """ + return f"Hello, {name}! Welcome!" + + +# Access the docstring +print(greet("Baraa")) +print(f"Function docstring: {greet.__doc__[:50]}...") + + +def calculate_area(length, width): + """ + Calculate the area of a rectangle. + + Args: + length (float): The length of the rectangle. + width (float): The width of the rectangle. + + Returns: + float: The area of the rectangle. + + Raises: + ValueError: If length or width is negative. + """ + if length < 0 or width < 0: + raise ValueError("Length and width must be non-negative") + return length * width + + +print(f"Area: {calculate_area(5, 3)}") + +# ----------------------------------------------------------------------------- +# 4. Class Docstrings +# ----------------------------------------------------------------------------- + +print("\n--- Class Docstrings ---") + + +class Rectangle: + """ + A class to represent a rectangle. + + This class provides methods to calculate the area and perimeter + of a rectangle, as well as other utility methods. + + Attributes: + length (float): The length of the rectangle. + width (float): The width of the rectangle. + + Methods: + area(): Returns the area of the rectangle. + perimeter(): Returns the perimeter of the rectangle. + """ + + def __init__(self, length, width): + """ + Initialize a Rectangle instance. + + Args: + length (float): The length of the rectangle. + width (float): The width of the rectangle. + """ + self.length = length + self.width = width + + def area(self): + """Calculate and return the area of the rectangle.""" + return self.length * self.width + + def perimeter(self): + """Calculate and return the perimeter of the rectangle.""" + return 2 * (self.length + self.width) + + +rect = Rectangle(10, 5) +print(f"Rectangle area: {rect.area()}") +print(f"Rectangle perimeter: {rect.perimeter()}") + +# ----------------------------------------------------------------------------- +# 5. Comment Best Practices +# ----------------------------------------------------------------------------- + +print("\n--- Comment Best Practices ---") + +# ✅ GOOD: Explain WHY, not WHAT +# Calculate discount for loyalty members (15% off for 2+ years) +years_as_member = 3 +discount = 0.15 if years_as_member >= 2 else 0 + +# ❌ BAD: Explains what the code obviously does +# x = x + 1 # Add 1 to x + +# ✅ GOOD: Document complex algorithms +# Using binary search for O(log n) time complexity +# instead of linear search O(n) for performance + +# ✅ GOOD: Mark TODO items for future work +# TODO: Implement caching for better performance +# FIXME: Handle edge case when input is empty +# NOTE: This function requires Python 3.8+ +# HACK: Temporary workaround for API limitation + +# ✅ GOOD: Use comments for code sections +# ============================================ +# DATABASE CONNECTION SETUP +# ============================================ + +# ============================================ +# USER AUTHENTICATION +# ============================================ + +print("Best practices demonstrated!") + +# ----------------------------------------------------------------------------- +# 6. Commenting Out Code +# ----------------------------------------------------------------------------- + +print("\n--- Commenting Out Code ---") + +# You can temporarily disable code by commenting it out +# print("This line won't execute") +# old_function() +# deprecated_code() + +# Useful during debugging +value = 100 +# value = 200 # Uncomment to test with different value +print(f"Current value: {value}") + +# Multiple lines can be commented at once +# line_1 = "first" +# line_2 = "second" +# line_3 = "third" + +# ----------------------------------------------------------------------------- +# 7. Module-Level Docstrings +# ----------------------------------------------------------------------------- + +print("\n--- Module-Level Docstrings ---") + +# At the top of a Python file, you can include a module docstring +# (like the one at the top of this file) + +# Access a module's docstring +print("This module's docstring starts with:") +print(__doc__[:100] + "...") + +# ----------------------------------------------------------------------------- +# 8. Type Hints with Comments +# ----------------------------------------------------------------------------- + +print("\n--- Type Hints with Comments ---") + + +def process_data( + data: list, # The input data to process + threshold: float = 0.5, # Minimum value to include (default: 0.5) + verbose: bool = False # Print progress if True +) -> list: + """ + Process a list of numerical data. + + Args: + data: List of numbers to process. + threshold: Minimum value to include in results. + verbose: Whether to print processing details. + + Returns: + Filtered list containing only values above threshold. + """ + if verbose: + print(f"Processing {len(data)} items...") + return [x for x in data if x > threshold] + + +result = process_data([0.1, 0.6, 0.3, 0.8, 0.4], threshold=0.5) +print(f"Filtered data: {result}") + +# ----------------------------------------------------------------------------- +# 9. Practical Example: Well-Commented Code +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example ---") + + +def calculate_shipping_cost(weight, distance, express=False): + """ + Calculate the shipping cost based on weight and distance. + + The cost is calculated using a base rate plus additional charges + for weight and distance. Express shipping doubles the final cost. + + Args: + weight (float): Package weight in kilograms. + distance (float): Shipping distance in kilometers. + express (bool): Whether to use express shipping. + + Returns: + float: Total shipping cost in dollars. + + Example: + >>> calculate_shipping_cost(2.5, 100) + 17.0 + >>> calculate_shipping_cost(2.5, 100, express=True) + 34.0 + """ + # Base shipping rate + BASE_RATE = 5.00 + + # Rate per kilogram of weight + WEIGHT_RATE = 2.00 + + # Rate per 100 kilometers + DISTANCE_RATE = 0.05 + + # Calculate component costs + weight_cost = weight * WEIGHT_RATE + distance_cost = distance * DISTANCE_RATE + + # Sum up total cost + total = BASE_RATE + weight_cost + distance_cost + + # Apply express multiplier if applicable + if express: + total *= 2 # Express shipping is 2x the normal rate + + return round(total, 2) + + +# Example usage +package_weight = 3.5 # kg +shipping_distance = 250 # km + +standard_cost = calculate_shipping_cost(package_weight, shipping_distance) +express_cost = calculate_shipping_cost(package_weight, shipping_distance, express=True) + +print(f"Standard shipping: ${standard_cost}") +print(f"Express shipping: ${express_cost}") + +# ----------------------------------------------------------------------------- +# Summary +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("COMMENTS SUMMARY") +print("=" * 60) +print(""" +1. Single-line: Use # for short comments +2. Multi-line: Use multiple # lines +3. Docstrings: Use triple quotes for documentation +4. Explain WHY, not WHAT +5. Keep comments up-to-date with code changes +6. Use TODO, FIXME, NOTE for special markers +7. Don't over-comment obvious code +""") diff --git a/01_basics/03_variables.py b/01_basics/03_variables.py new file mode 100644 index 0000000..da0fdb6 --- /dev/null +++ b/01_basics/03_variables.py @@ -0,0 +1,407 @@ +""" +================================================================================ +File: 03_variables.py +Topic: Variables in Python +================================================================================ + +This file demonstrates how to create and use variables in Python. Variables +are containers for storing data values. Unlike other programming languages, +Python has no command for declaring a variable - it's created the moment +you assign a value to it. + +Key Concepts: +- Variable creation and assignment +- Variable naming conventions +- Multiple assignments +- Variable types and type checking +- Constants +- Variable scope basics + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Creating Variables +# ----------------------------------------------------------------------------- +# Variables are created when you assign a value to them + +print("--- Creating Variables ---") + +# Simple variable assignments +name = "Baraa" +age = 25 +height = 1.75 +is_student = True + +print(f"Name: {name}") +print(f"Age: {age}") +print(f"Height: {height}m") +print(f"Is student: {is_student}") + +# Variables can be reassigned to different values +x = 10 +print(f"\nx = {x}") +x = 20 +print(f"x = {x} (reassigned)") + +# Variables can even change types (dynamic typing) +value = 100 +print(f"\nvalue = {value} (type: {type(value).__name__})") +value = "one hundred" +print(f"value = {value} (type: {type(value).__name__})") + +# ----------------------------------------------------------------------------- +# 2. Variable Naming Rules +# ----------------------------------------------------------------------------- +# Rules for Python variable names + +print("\n--- Variable Naming Rules ---") + +# ✅ Valid variable names +my_variable = 1 # Lowercase with underscores (snake_case) - RECOMMENDED +myVariable = 2 # camelCase - valid but not Pythonic +MyVariable = 3 # PascalCase - usually for classes +_private_var = 4 # Starts with underscore - convention for private +__very_private = 5 # Double underscore - name mangling +var123 = 6 # Can contain numbers (but not start with them) +CONSTANT = 7 # ALL CAPS - convention for constants + +print(f"my_variable = {my_variable}") +print(f"_private_var = {_private_var}") +print(f"CONSTANT = {CONSTANT}") + +# ❌ Invalid variable names (would cause errors): +# 123var = 1 # Cannot start with a number +# my-variable = 1 # Hyphens not allowed +# my variable = 1 # Spaces not allowed +# class = 1 # Cannot use reserved keywords + +# Reserved keywords in Python (cannot use as variable names): +import keyword +print(f"\nPython reserved keywords: {keyword.kwlist[:10]}...") + +# ----------------------------------------------------------------------------- +# 3. Naming Conventions (PEP 8) +# ----------------------------------------------------------------------------- + +print("\n--- Naming Conventions (PEP 8) ---") + +# Variables and functions: snake_case +user_name = "john_doe" +max_value = 100 +is_active = True + +# Constants: UPPER_SNAKE_CASE +PI = 3.14159 +MAX_CONNECTIONS = 100 +DEFAULT_TIMEOUT = 30 + +# Classes: PascalCase (CapWords) +# class MyClass: +# pass + +# Private variables: leading underscore +_internal_state = "private" + +# "Very private" / Name-mangled: double leading underscore +__name_mangled = "mangled" + +print(f"user_name: {user_name}") +print(f"PI (constant): {PI}") +print(f"MAX_CONNECTIONS (constant): {MAX_CONNECTIONS}") + +# Descriptive names are better than short names +# ✅ Good +total_price = 99.99 +customer_name = "Alice" +is_logged_in = True + +# ❌ Bad (avoid!) +# tp = 99.99 +# cn = "Alice" +# il = True + +# ----------------------------------------------------------------------------- +# 4. Multiple Variable Assignment +# ----------------------------------------------------------------------------- + +print("\n--- Multiple Variable Assignment ---") + +# Assign the same value to multiple variables +a = b = c = 100 +print(f"a = {a}, b = {b}, c = {c}") + +# Assign different values in one line +x, y, z = 1, 2, 3 +print(f"x = {x}, y = {y}, z = {z}") + +# Swap variables easily +print(f"\nBefore swap: x = {x}, y = {y}") +x, y = y, x +print(f"After swap: x = {x}, y = {y}") + +# Unpack a list/tuple into variables +coordinates = (10, 20, 30) +x, y, z = coordinates +print(f"\nUnpacked coordinates: x={x}, y={y}, z={z}") + +# Extended unpacking with * +first, *rest = [1, 2, 3, 4, 5] +print(f"first = {first}, rest = {rest}") + +*beginning, last = [1, 2, 3, 4, 5] +print(f"beginning = {beginning}, last = {last}") + +first, *middle, last = [1, 2, 3, 4, 5] +print(f"first = {first}, middle = {middle}, last = {last}") + +# ----------------------------------------------------------------------------- +# 5. Variable Types and Type Checking +# ----------------------------------------------------------------------------- + +print("\n--- Variable Types and Type Checking ---") + +# Python is dynamically typed - types are determined at runtime +integer_var = 42 +float_var = 3.14 +string_var = "Hello" +bool_var = True +list_var = [1, 2, 3] +dict_var = {"key": "value"} +none_var = None + +# Check types using type() +print(f"integer_var: {integer_var} -> {type(integer_var)}") +print(f"float_var: {float_var} -> {type(float_var)}") +print(f"string_var: {string_var} -> {type(string_var)}") +print(f"bool_var: {bool_var} -> {type(bool_var)}") +print(f"list_var: {list_var} -> {type(list_var)}") +print(f"dict_var: {dict_var} -> {type(dict_var)}") +print(f"none_var: {none_var} -> {type(none_var)}") + +# Check if variable is of a specific type using isinstance() +print(f"\nIs integer_var an int? {isinstance(integer_var, int)}") +print(f"Is string_var a str? {isinstance(string_var, str)}") +print(f"Is list_var a list? {isinstance(list_var, list)}") + +# Check multiple types +print(f"Is integer_var int or float? {isinstance(integer_var, (int, float))}") + +# ----------------------------------------------------------------------------- +# 6. Type Casting (Converting Types) +# ----------------------------------------------------------------------------- + +print("\n--- Type Casting ---") + +# String to integer +str_num = "123" +int_num = int(str_num) +print(f"'{str_num}' (str) -> {int_num} (int)") + +# String to float +str_float = "3.14" +float_num = float(str_float) +print(f"'{str_float}' (str) -> {float_num} (float)") + +# Number to string +number = 42 +str_number = str(number) +print(f"{number} (int) -> '{str_number}' (str)") + +# Float to integer (truncates, doesn't round) +pi = 3.99 +int_pi = int(pi) +print(f"{pi} (float) -> {int_pi} (int) [truncated]") + +# Boolean conversions +print(f"\nbool(1) = {bool(1)}") # True +print(f"bool(0) = {bool(0)}") # False +print(f"bool('hello') = {bool('hello')}") # True +print(f"bool('') = {bool('')}") # False +print(f"bool([1, 2]) = {bool([1, 2])}") # True +print(f"bool([]) = {bool([])}") # False + +# Integer to boolean +print(f"\nint(True) = {int(True)}") # 1 +print(f"int(False) = {int(False)}") # 0 + +# ----------------------------------------------------------------------------- +# 7. Constants +# ----------------------------------------------------------------------------- + +print("\n--- Constants ---") + +# Python doesn't have true constants, but by convention: +# - Use UPPER_SNAKE_CASE for constants +# - Don't modify them after initial assignment + +# Mathematical constants +PI = 3.14159265359 +E = 2.71828182845 +GOLDEN_RATIO = 1.61803398875 + +# Application constants +MAX_USERS = 1000 +API_TIMEOUT = 30 +BASE_URL = "https://api.example.com" +DEBUG_MODE = False + +print(f"PI = {PI}") +print(f"MAX_USERS = {MAX_USERS}") +print(f"BASE_URL = {BASE_URL}") + +# You CAN modify them (Python won't stop you), but you SHOULDN'T +# PI = 3 # Don't do this! + +# For true constants, you can use: +# 1. Separate constants module (constants.py) +# 2. typing.Final (Python 3.8+) +from typing import Final + +MAX_SIZE: Final = 100 +# MAX_SIZE = 200 # Type checker will warn about this + +print(f"MAX_SIZE (Final) = {MAX_SIZE}") + +# ----------------------------------------------------------------------------- +# 8. Variable Scope (Basic) +# ----------------------------------------------------------------------------- + +print("\n--- Variable Scope ---") + +# Global variable +global_var = "I'm global" + + +def my_function(): + # Local variable + local_var = "I'm local" + print(f"Inside function - global_var: {global_var}") + print(f"Inside function - local_var: {local_var}") + + +my_function() +print(f"Outside function - global_var: {global_var}") +# print(local_var) # Error! local_var doesn't exist here + + +# Modifying global variables inside functions +counter = 0 + + +def increment(): + global counter # Declare we want to use global counter + counter += 1 + + +print(f"\nBefore increment: counter = {counter}") +increment() +increment() +print(f"After 2 increments: counter = {counter}") + +# ----------------------------------------------------------------------------- +# 9. Variable Identity and Memory +# ----------------------------------------------------------------------------- + +print("\n--- Variable Identity and Memory ---") + +# id() returns the memory address of a variable +a = 10 +b = 10 +c = a + +print(f"a = {a}, id(a) = {id(a)}") +print(f"b = {b}, id(b) = {id(b)}") +print(f"c = {c}, id(c) = {id(c)}") + +# Small integers (-5 to 256) are cached in Python +print(f"\na is b: {a is b}") # True (same memory location) +print(f"a is c: {a is c}") # True + +# Larger numbers may have different ids +x = 1000 +y = 1000 +print(f"\nx = {x}, id(x) = {id(x)}") +print(f"y = {y}, id(y) = {id(y)}") + +# 'is' vs '==' +list1 = [1, 2, 3] +list2 = [1, 2, 3] +list3 = list1 + +print(f"\nlist1 == list2: {list1 == list2}") # True (same values) +print(f"list1 is list2: {list1 is list2}") # False (different objects) +print(f"list1 is list3: {list1 is list3}") # True (same object) + +# ----------------------------------------------------------------------------- +# 10. Deleting Variables +# ----------------------------------------------------------------------------- + +print("\n--- Deleting Variables ---") + +temp_var = "I will be deleted" +print(f"temp_var exists: {temp_var}") + +del temp_var +# print(temp_var) # Error! NameError: name 'temp_var' is not defined + +print("temp_var has been deleted") + +# Check if variable exists +if 'temp_var' not in dir(): + print("temp_var no longer exists in current scope") + +# ----------------------------------------------------------------------------- +# 11. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Example 1: User profile +first_name = "Baraa" +last_name = "Shaer" +full_name = f"{first_name} {last_name}" +email = f"{first_name.lower()}.{last_name.lower()}@example.com" + +print(f"Full Name: {full_name}") +print(f"Email: {email}") + +# Example 2: Shopping cart +item_price = 29.99 +quantity = 3 +discount_percent = 10 + +subtotal = item_price * quantity +discount_amount = subtotal * (discount_percent / 100) +total = subtotal - discount_amount + +print(f"\nSubtotal: ${subtotal:.2f}") +print(f"Discount ({discount_percent}%): -${discount_amount:.2f}") +print(f"Total: ${total:.2f}") + +# Example 3: Temperature conversion +celsius = 25 +fahrenheit = (celsius * 9/5) + 32 +kelvin = celsius + 273.15 + +print(f"\n{celsius}°C = {fahrenheit}°F = {kelvin}K") + +# ----------------------------------------------------------------------------- +# Summary +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("VARIABLES SUMMARY") +print("=" * 60) +print(""" +1. Variables are created by assignment (=) +2. Python is dynamically typed +3. Use snake_case for variables (PEP 8) +4. Use UPPER_CASE for constants +5. Multiple assignments: x, y, z = 1, 2, 3 +6. Check types with type() or isinstance() +7. Convert types with int(), str(), float(), bool() +8. Variables have scope (local vs global) +9. Use 'is' for identity, '==' for equality +10. Delete variables with 'del' +""") diff --git a/01_basics/04_data_types.py b/01_basics/04_data_types.py new file mode 100644 index 0000000..6ca5cb7 --- /dev/null +++ b/01_basics/04_data_types.py @@ -0,0 +1,536 @@ +""" +================================================================================ +File: 04_data_types.py +Topic: Data Types in Python +================================================================================ + +This file demonstrates Python's built-in data types. Understanding data types +is fundamental to programming as they determine what operations can be +performed on data and how it's stored in memory. + +Key Concepts: +- Numeric types (int, float, complex) +- Text type (str) +- Boolean type (bool) +- Sequence types (list, tuple, range) +- Mapping type (dict) +- Set types (set, frozenset) +- None type +- Type checking and conversion + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Numeric Types Overview +# ----------------------------------------------------------------------------- + +print("=" * 60) +print("NUMERIC TYPES") +print("=" * 60) + +# ----------------------------------------------------------------------------- +# 1.1 Integers (int) +# ----------------------------------------------------------------------------- + +print("\n--- Integers (int) ---") + +# Basic integers +positive_int = 42 +negative_int = -17 +zero = 0 + +print(f"Positive: {positive_int}") +print(f"Negative: {negative_int}") +print(f"Zero: {zero}") + +# Large integers (Python handles arbitrary precision) +big_number = 123456789012345678901234567890 +print(f"Big number: {big_number}") +print(f"Type: {type(big_number)}") + +# Different number bases +binary = 0b1010 # Binary (base 2) +octal = 0o17 # Octal (base 8) +hexadecimal = 0xFF # Hexadecimal (base 16) + +print(f"\nBinary 0b1010 = {binary}") +print(f"Octal 0o17 = {octal}") +print(f"Hex 0xFF = {hexadecimal}") + +# Underscores for readability (Python 3.6+) +million = 1_000_000 +credit_card = 1234_5678_9012_3456 +print(f"\nMillion: {million}") +print(f"Credit card: {credit_card}") + +# Integer operations +a, b = 17, 5 +print(f"\na = {a}, b = {b}") +print(f"a + b = {a + b}") # Addition +print(f"a - b = {a - b}") # Subtraction +print(f"a * b = {a * b}") # Multiplication +print(f"a / b = {a / b}") # Division (returns float) +print(f"a // b = {a // b}") # Floor division +print(f"a % b = {a % b}") # Modulo (remainder) +print(f"a ** b = {a ** b}") # Exponentiation + +# ----------------------------------------------------------------------------- +# 1.2 Floating-Point Numbers (float) +# ----------------------------------------------------------------------------- + +print("\n--- Floating-Point Numbers (float) ---") + +# Basic floats +pi = 3.14159 +negative_float = -2.5 +scientific = 2.5e10 # Scientific notation (2.5 × 10^10) + +print(f"Pi: {pi}") +print(f"Negative: {negative_float}") +print(f"Scientific 2.5e10: {scientific}") +print(f"Type: {type(pi)}") + +# Float precision limitations +print(f"\n0.1 + 0.2 = {0.1 + 0.2}") # Not exactly 0.3! +print(f"0.1 + 0.2 == 0.3: {0.1 + 0.2 == 0.3}") # False! + +# For precise decimal calculations, use the decimal module +from decimal import Decimal +d1 = Decimal('0.1') +d2 = Decimal('0.2') +print(f"Decimal: {d1} + {d2} = {d1 + d2}") + +# Special float values +infinity = float('inf') +neg_infinity = float('-inf') +not_a_number = float('nan') + +print(f"\nInfinity: {infinity}") +print(f"Negative infinity: {neg_infinity}") +print(f"NaN: {not_a_number}") +print(f"1000 < infinity: {1000 < infinity}") + +# Float methods +f = 3.7 +print(f"\n{f}.is_integer(): {f.is_integer()}") +print(f"4.0.is_integer(): {(4.0).is_integer()}") + +# ----------------------------------------------------------------------------- +# 1.3 Complex Numbers (complex) +# ----------------------------------------------------------------------------- + +print("\n--- Complex Numbers (complex) ---") + +# Creating complex numbers +c1 = 3 + 4j +c2 = complex(2, -1) + +print(f"c1 = {c1}") +print(f"c2 = {c2}") +print(f"Type: {type(c1)}") + +# Accessing parts +print(f"\nc1.real = {c1.real}") +print(f"c1.imag = {c1.imag}") +print(f"Conjugate of c1: {c1.conjugate()}") + +# Complex arithmetic +print(f"\nc1 + c2 = {c1 + c2}") +print(f"c1 * c2 = {c1 * c2}") +print(f"abs(c1) = {abs(c1)}") # Magnitude + +# ----------------------------------------------------------------------------- +# 2. Text Type (str) +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("TEXT TYPE - STRINGS") +print("=" * 60) + +# Creating strings +single_quotes = 'Hello, World!' +double_quotes = "Python is awesome" +multi_line = """This is a +multi-line string""" + +print(f"Single quotes: {single_quotes}") +print(f"Double quotes: {double_quotes}") +print(f"Multi-line:\n{multi_line}") + +# String operations +s = "Hello, Python!" +print(f"\nString: '{s}'") +print(f"Length: {len(s)}") +print(f"Uppercase: {s.upper()}") +print(f"Lowercase: {s.lower()}") +print(f"Title case: {s.title()}") +print(f"Replace: {s.replace('Python', 'World')}") +print(f"Split: {s.split(', ')}") +print(f"Strip: {' hello '.strip()}") + +# String indexing and slicing +print(f"\nIndexing: s[0] = '{s[0]}', s[-1] = '{s[-1]}'") +print(f"Slicing: s[0:5] = '{s[0:5]}'") +print(f"Step: s[::2] = '{s[::2]}'") +print(f"Reverse: s[::-1] = '{s[::-1]}'") + +# String methods for checking +text = "Python3" +print(f"\n'{text}'.isalnum(): {text.isalnum()}") +print(f"'python'.isalpha(): {'python'.isalpha()}") +print(f"'12345'.isdigit(): {'12345'.isdigit()}") +print(f"'hello'.islower(): {'hello'.islower()}") +print(f"'HELLO'.isupper(): {'HELLO'.isupper()}") + +# String formatting +name = "Baraa" +age = 25 +print(f"\nf-string: {name} is {age} years old") +print("format(): {} is {} years old".format(name, age)) +print("%-formatting: %s is %d years old" % (name, age)) + +# Escape characters +print(f"\nNewline: Hello\\nWorld → Hello\nWorld") +print(f"Tab: Hello\\tWorld → Hello\tWorld") +print("Quote: She said \"Hi!\"") + +# Raw strings +print(f"\nRaw string: r'C:\\Users\\Name' → {r'C:\Users\Name'}") + +# ----------------------------------------------------------------------------- +# 3. Boolean Type (bool) +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("BOOLEAN TYPE") +print("=" * 60) + +# Boolean values +is_active = True +is_deleted = False + +print(f"is_active: {is_active}, type: {type(is_active)}") +print(f"is_deleted: {is_deleted}") + +# Boolean as integers +print(f"\nTrue as int: {int(True)}") # 1 +print(f"False as int: {int(False)}") # 0 +print(f"True + True = {True + True}") # 2 + +# Comparison operators return booleans +x, y = 10, 5 +print(f"\nx = {x}, y = {y}") +print(f"x > y: {x > y}") +print(f"x < y: {x < y}") +print(f"x == y: {x == y}") +print(f"x != y: {x != y}") +print(f"x >= y: {x >= y}") + +# Logical operators +print(f"\nTrue and False: {True and False}") +print(f"True or False: {True or False}") +print(f"not True: {not True}") + +# Truthy and Falsy values +print("\nFalsy values (evaluate to False):") +falsy_values = [False, 0, 0.0, "", [], {}, set(), None] +for val in falsy_values: + print(f" bool({repr(val)}) = {bool(val)}") + +print("\nTruthy values (evaluate to True):") +truthy_values = [True, 1, -1, 3.14, "hello", [1, 2], {"a": 1}] +for val in truthy_values: + print(f" bool({repr(val)}) = {bool(val)}") + +# ----------------------------------------------------------------------------- +# 4. Sequence Types +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("SEQUENCE TYPES") +print("=" * 60) + +# ----------------------------------------------------------------------------- +# 4.1 Lists +# ----------------------------------------------------------------------------- + +print("\n--- Lists (Mutable Sequences) ---") + +# Creating lists +empty_list = [] +numbers = [1, 2, 3, 4, 5] +mixed = [1, "two", 3.0, True, None] +nested = [[1, 2], [3, 4], [5, 6]] + +print(f"Numbers: {numbers}") +print(f"Mixed types: {mixed}") +print(f"Nested: {nested}") + +# List operations +fruits = ["apple", "banana", "cherry"] +print(f"\nFruits: {fruits}") +print(f"Length: {len(fruits)}") +print(f"First: {fruits[0]}, Last: {fruits[-1]}") +print(f"Slice: {fruits[1:3]}") + +# Modifying lists +fruits.append("date") +print(f"After append: {fruits}") + +fruits.insert(1, "blueberry") +print(f"After insert at 1: {fruits}") + +fruits.remove("banana") +print(f"After remove 'banana': {fruits}") + +popped = fruits.pop() +print(f"Popped: {popped}, List: {fruits}") + +# List comprehension +squares = [x**2 for x in range(1, 6)] +print(f"\nSquares (comprehension): {squares}") + +# ----------------------------------------------------------------------------- +# 4.2 Tuples +# ----------------------------------------------------------------------------- + +print("\n--- Tuples (Immutable Sequences) ---") + +# Creating tuples +empty_tuple = () +single = (1,) # Note the comma +coordinates = (10, 20, 30) +mixed_tuple = (1, "two", 3.0) + +print(f"Coordinates: {coordinates}") +print(f"Type: {type(coordinates)}") + +# Tuple unpacking +x, y, z = coordinates +print(f"Unpacked: x={x}, y={y}, z={z}") + +# Tuples are immutable +# coordinates[0] = 100 # This would raise an error + +# Named tuples +from collections import namedtuple +Point = namedtuple('Point', ['x', 'y']) +p = Point(10, 20) +print(f"\nNamed tuple: {p}") +print(f"p.x = {p.x}, p.y = {p.y}") + +# ----------------------------------------------------------------------------- +# 4.3 Range +# ----------------------------------------------------------------------------- + +print("\n--- Range ---") + +# Creating ranges +r1 = range(5) # 0-4 +r2 = range(1, 6) # 1-5 +r3 = range(0, 10, 2) # 0, 2, 4, 6, 8 + +print(f"range(5): {list(r1)}") +print(f"range(1, 6): {list(r2)}") +print(f"range(0, 10, 2): {list(r3)}") +print(f"Type: {type(r1)}") + +# Range is memory efficient +big_range = range(1000000) +print(f"\nrange(1000000) - Length: {len(big_range)}") +print(f"500000 in big_range: {500000 in big_range}") + +# ----------------------------------------------------------------------------- +# 5. Mapping Type (dict) +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("MAPPING TYPE - DICTIONARY") +print("=" * 60) + +# Creating dictionaries +empty_dict = {} +person = {"name": "Baraa", "age": 25, "city": "Gaza"} +using_dict = dict(a=1, b=2, c=3) + +print(f"Person: {person}") +print(f"Type: {type(person)}") + +# Accessing values +print(f"\nName: {person['name']}") +print(f"Age (get): {person.get('age')}") +print(f"Country (get with default): {person.get('country', 'Unknown')}") + +# Modifying dictionaries +person["email"] = "baraa@example.com" # Add +person["age"] = 26 # Update +print(f"\nAfter modifications: {person}") + +# Dictionary methods +print(f"\nKeys: {list(person.keys())}") +print(f"Values: {list(person.values())}") +print(f"Items: {list(person.items())}") + +# Dictionary comprehension +squares_dict = {x: x**2 for x in range(1, 6)} +print(f"\nSquares dict: {squares_dict}") + +# ----------------------------------------------------------------------------- +# 6. Set Types +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("SET TYPES") +print("=" * 60) + +# Creating sets (mutable, unordered, unique elements) +empty_set = set() # Note: {} creates an empty dict, not set +numbers_set = {1, 2, 3, 4, 5} +from_list = set([1, 2, 2, 3, 3, 3]) # Duplicates removed + +print(f"Numbers set: {numbers_set}") +print(f"From list with duplicates: {from_list}") +print(f"Type: {type(numbers_set)}") + +# Set operations +a = {1, 2, 3, 4} +b = {3, 4, 5, 6} + +print(f"\nSet A: {a}") +print(f"Set B: {b}") +print(f"Union (A | B): {a | b}") +print(f"Intersection (A & B): {a & b}") +print(f"Difference (A - B): {a - b}") +print(f"Symmetric difference (A ^ B): {a ^ b}") + +# Set methods +s = {1, 2, 3} +s.add(4) +print(f"\nAfter add(4): {s}") +s.discard(2) +print(f"After discard(2): {s}") + +# Frozenset (immutable set) +frozen = frozenset([1, 2, 3]) +print(f"\nFrozenset: {frozen}") +# frozen.add(4) # This would raise an error + +# Sets are useful for membership testing +valid_statuses = {"active", "pending", "completed"} +user_status = "active" +print(f"\n'{user_status}' is valid: {user_status in valid_statuses}") + +# ----------------------------------------------------------------------------- +# 7. None Type +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("NONE TYPE") +print("=" * 60) + +# None represents absence of a value +result = None + +print(f"result = {result}") +print(f"Type: {type(result)}") +print(f"result is None: {result is None}") # Use 'is' not '==' + +# Common uses of None +def greet(name=None): + """Function with optional parameter""" + if name is None: + return "Hello, Guest!" + return f"Hello, {name}!" + +print(f"\ngreet(): {greet()}") +print(f"greet('Baraa'): {greet('Baraa')}") + +# None as placeholder +data = None +# ... later in code ... +data = fetch_data() if False else [] # Simulated +print(f"Data initialized: {data}") + +# ----------------------------------------------------------------------------- +# 8. Type Checking and Conversion +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("TYPE CHECKING AND CONVERSION") +print("=" * 60) + +# Using type() +values = [42, 3.14, "hello", True, [1, 2], {"a": 1}, None] +print("Type checking with type():") +for val in values: + print(f" {repr(val):15} -> {type(val).__name__}") + +# Using isinstance() (preferred for type checking) +print("\nType checking with isinstance():") +x = 42 +print(f"isinstance(42, int): {isinstance(x, int)}") +print(f"isinstance(42, (int, float)): {isinstance(x, (int, float))}") + +# Type conversion summary +print("\nType Conversion Examples:") +print(f"int('42') = {int('42')}") +print(f"float('3.14') = {float('3.14')}") +print(f"str(42) = {str(42)}") +print(f"bool(1) = {bool(1)}") +print(f"list('abc') = {list('abc')}") +print(f"tuple([1,2,3]) = {tuple([1, 2, 3])}") +print(f"set([1,2,2,3]) = {set([1, 2, 2, 3])}") +print(f"dict([('a',1),('b',2)]) = {dict([('a', 1), ('b', 2)])}") + +# ----------------------------------------------------------------------------- +# 9. Data Type Comparison Table +# ----------------------------------------------------------------------------- + +print("\n" + "=" * 60) +print("DATA TYPE COMPARISON TABLE") +print("=" * 60) +print(""" +| Type | Mutable | Ordered | Duplicates | Example | +|-----------|---------|---------|------------|----------------------| +| int | No | N/A | N/A | 42 | +| float | No | N/A | N/A | 3.14 | +| complex | No | N/A | N/A | 3+4j | +| str | No | Yes | Yes | "hello" | +| bool | No | N/A | N/A | True | +| list | Yes | Yes | Yes | [1, 2, 3] | +| tuple | No | Yes | Yes | (1, 2, 3) | +| range | No | Yes | No | range(5) | +| dict | Yes | Yes* | Keys: No | {"a": 1} | +| set | Yes | No | No | {1, 2, 3} | +| frozenset | No | No | No | frozenset({1, 2, 3}) | +| NoneType | No | N/A | N/A | None | + +* Dicts preserve insertion order in Python 3.7+ +""") + +# ----------------------------------------------------------------------------- +# Summary +# ----------------------------------------------------------------------------- + +print("=" * 60) +print("DATA TYPES SUMMARY") +print("=" * 60) +print(""" +Numeric: int, float, complex +Text: str (immutable) +Boolean: bool (True/False) +Sequences: list (mutable), tuple (immutable), range +Mapping: dict (key-value pairs) +Sets: set (mutable), frozenset (immutable) +Special: None (absence of value) + +Key Points: +1. Python is dynamically typed +2. Use type() or isinstance() to check types +3. Use appropriate type for your data +4. Mutable types can be changed, immutable cannot +5. Choose between list/tuple based on mutability needs +6. Use dict for key-value mappings +7. Use set for unique elements and fast membership tests +""") diff --git a/01_basics/__pycache__/04_data_types.cpython-313.pyc b/01_basics/__pycache__/04_data_types.cpython-313.pyc new file mode 100644 index 0000000..fda8b61 Binary files /dev/null and b/01_basics/__pycache__/04_data_types.cpython-313.pyc differ diff --git a/02_control_flow/01_if_else.py b/02_control_flow/01_if_else.py new file mode 100644 index 0000000..4cadac3 --- /dev/null +++ b/02_control_flow/01_if_else.py @@ -0,0 +1,145 @@ +""" +================================================================================ +File: 01_if_else.py +Topic: Conditional Statements - if/else +================================================================================ + +This file demonstrates the fundamentals of conditional statements in Python. +Conditional statements allow your program to make decisions and execute +different code blocks based on whether conditions are True or False. + +Key Concepts: +- if statement: Executes code block if condition is True +- else statement: Executes code block if condition is False +- Comparison operators: ==, !=, <, >, <=, >= +- Logical operators: and, or, not + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Simple if Statement +# ----------------------------------------------------------------------------- +# The most basic form - executes code only if condition is True + +age = 18 + +if age >= 18: + print("You are an adult.") + +# ----------------------------------------------------------------------------- +# 2. if-else Statement +# ----------------------------------------------------------------------------- +# Provides an alternative when the condition is False + +temperature = 15 + +if temperature > 25: + print("It's a hot day!") +else: + print("It's not that hot today.") + +# ----------------------------------------------------------------------------- +# 3. Comparison Operators +# ----------------------------------------------------------------------------- +# Used to compare values and return True or False + +x = 10 +y = 20 + +print("\n--- Comparison Operators ---") +print(f"x == y: {x == y}") # Equal to +print(f"x != y: {x != y}") # Not equal to +print(f"x < y: {x < y}") # Less than +print(f"x > y: {x > y}") # Greater than +print(f"x <= y: {x <= y}") # Less than or equal to +print(f"x >= y: {x >= y}") # Greater than or equal to + +# ----------------------------------------------------------------------------- +# 4. Logical Operators (and, or, not) +# ----------------------------------------------------------------------------- +# Combine multiple conditions + +age = 25 +has_license = True + +print("\n--- Logical Operators ---") + +# 'and' - Both conditions must be True +if age >= 18 and has_license: + print("You can drive a car.") + +# 'or' - At least one condition must be True +is_weekend = True +is_holiday = False + +if is_weekend or is_holiday: + print("You can relax today!") + +# 'not' - Reverses the boolean value +is_raining = False + +if not is_raining: + print("You don't need an umbrella.") + +# ----------------------------------------------------------------------------- +# 5. Nested if Statements +# ----------------------------------------------------------------------------- +# You can place if statements inside other if statements + +print("\n--- Nested if Statements ---") + +score = 85 +attendance = 90 + +if score >= 60: + print("You passed the exam!") + if attendance >= 80: + print("And you have excellent attendance!") + else: + print("But try to improve your attendance.") +else: + print("You need to study harder.") + +# ----------------------------------------------------------------------------- +# 6. Truthy and Falsy Values +# ----------------------------------------------------------------------------- +# In Python, some values are considered False: 0, "", [], {}, None, False +# Everything else is considered True + +print("\n--- Truthy and Falsy Values ---") + +empty_list = [] +full_list = [1, 2, 3] + +if full_list: + print("The list has items.") + +if not empty_list: + print("The list is empty.") + +# ----------------------------------------------------------------------------- +# 7. Ternary Operator (One-line if-else) +# ----------------------------------------------------------------------------- +# A compact way to write simple if-else statements + +age = 20 +status = "adult" if age >= 18 else "minor" +print(f"\nTernary result: You are an {status}.") + +# ----------------------------------------------------------------------------- +# 8. Practical Example: Simple Login Check +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: Login Check ---") + +username = "admin" +password = "secret123" + +input_user = "admin" +input_pass = "secret123" + +if input_user == username and input_pass == password: + print("Login successful! Welcome back.") +else: + print("Invalid username or password.") diff --git a/02_control_flow/02_elif.py b/02_control_flow/02_elif.py new file mode 100644 index 0000000..b430db7 --- /dev/null +++ b/02_control_flow/02_elif.py @@ -0,0 +1,213 @@ +""" +================================================================================ +File: 02_elif.py +Topic: Multiple Conditions with elif +================================================================================ + +This file demonstrates how to handle multiple conditions using 'elif' (else if). +When you have more than two possible outcomes, elif allows you to check +multiple conditions in sequence. + +Key Concepts: +- elif chains for multiple conditions +- Order matters - first True condition wins +- Default case with else + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic elif Structure +# ----------------------------------------------------------------------------- +# Check multiple conditions in sequence + +score = 85 + +print("--- Grade Calculator ---") + +if score >= 90: + grade = "A" + print("Excellent work!") +elif score >= 80: + grade = "B" + print("Good job!") +elif score >= 70: + grade = "C" + print("Satisfactory.") +elif score >= 60: + grade = "D" + print("You passed, but need improvement.") +else: + grade = "F" + print("You need to study harder.") + +print(f"Your grade: {grade}") + +# ----------------------------------------------------------------------------- +# 2. Order Matters in elif +# ----------------------------------------------------------------------------- +# The first condition that evaluates to True will be executed + +print("\n--- Order Matters Example ---") + +number = 15 + +# Correct order (most specific first) +if number > 20: + print("Greater than 20") +elif number > 10: + print("Greater than 10") # This will execute +elif number > 5: + print("Greater than 5") +else: + print("5 or less") + +# ----------------------------------------------------------------------------- +# 3. Day of the Week Example +# ----------------------------------------------------------------------------- +# Classic example showing multiple discrete options + +print("\n--- Day of the Week ---") + +day = 3 # 1=Monday, 2=Tuesday, etc. + +if day == 1: + day_name = "Monday" +elif day == 2: + day_name = "Tuesday" +elif day == 3: + day_name = "Wednesday" +elif day == 4: + day_name = "Thursday" +elif day == 5: + day_name = "Friday" +elif day == 6: + day_name = "Saturday" +elif day == 7: + day_name = "Sunday" +else: + day_name = "Invalid day" + +print(f"Day {day} is {day_name}") + +# ----------------------------------------------------------------------------- +# 4. Temperature Categories +# ----------------------------------------------------------------------------- +# Real-world example with ranges + +print("\n--- Temperature Categories ---") + +celsius = 22 + +if celsius < 0: + category = "Freezing" + advice = "Stay indoors if possible!" +elif celsius < 10: + category = "Cold" + advice = "Wear a warm jacket." +elif celsius < 20: + category = "Cool" + advice = "A light jacket is recommended." +elif celsius < 30: + category = "Warm" + advice = "Perfect weather for outdoor activities." +else: + category = "Hot" + advice = "Stay hydrated!" + +print(f"Temperature: {celsius}°C") +print(f"Category: {category}") +print(f"Advice: {advice}") + +# ----------------------------------------------------------------------------- +# 5. BMI Calculator Example +# ----------------------------------------------------------------------------- +# Practical health-related example + +print("\n--- BMI Calculator ---") + +weight = 70 # kg +height = 1.75 # meters + +bmi = weight / (height ** 2) + +if bmi < 18.5: + category = "Underweight" +elif bmi < 25: + category = "Normal weight" +elif bmi < 30: + category = "Overweight" +else: + category = "Obese" + +print(f"BMI: {bmi:.1f}") +print(f"Category: {category}") + +# ----------------------------------------------------------------------------- +# 6. Age Group Classification +# ----------------------------------------------------------------------------- +# Using elif for demographic categories + +print("\n--- Age Group Classification ---") + +age = 25 + +if age < 0: + group = "Invalid age" +elif age < 13: + group = "Child" +elif age < 20: + group = "Teenager" +elif age < 30: + group = "Young Adult" +elif age < 60: + group = "Adult" +else: + group = "Senior" + +print(f"Age {age} belongs to: {group}") + +# ----------------------------------------------------------------------------- +# 7. Combined Conditions with elif +# ----------------------------------------------------------------------------- +# Using logical operators within elif + +print("\n--- Combined Conditions ---") + +hour = 14 # 24-hour format +is_weekend = False + +if hour < 6: + greeting = "It's very early!" +elif hour < 12 and not is_weekend: + greeting = "Good morning, time to work!" +elif hour < 12 and is_weekend: + greeting = "Good morning, enjoy your day off!" +elif hour < 18: + greeting = "Good afternoon!" +elif hour < 22: + greeting = "Good evening!" +else: + greeting = "Good night!" + +print(f"Hour: {hour}:00 - {greeting}") + +# ----------------------------------------------------------------------------- +# 8. Handling User Input Example +# ----------------------------------------------------------------------------- + +print("\n--- Menu Selection Example ---") + +# Simulating user choice (in real code, you'd use input()) +choice = "B" + +if choice == "A" or choice == "a": + print("You selected: Start Game") +elif choice == "B" or choice == "b": + print("You selected: Load Game") +elif choice == "C" or choice == "c": + print("You selected: Settings") +elif choice == "Q" or choice == "q": + print("Goodbye!") +else: + print("Invalid choice. Please try again.") diff --git a/02_control_flow/03_match_case.py b/02_control_flow/03_match_case.py new file mode 100644 index 0000000..0b599be --- /dev/null +++ b/02_control_flow/03_match_case.py @@ -0,0 +1,243 @@ +""" +================================================================================ +File: 03_match_case.py +Topic: Pattern Matching with match-case (Python 3.10+) +================================================================================ + +This file demonstrates Python's structural pattern matching introduced in +Python 3.10. It's similar to switch/case in other languages but much more +powerful with pattern matching capabilities. + +Key Concepts: +- Basic match-case syntax +- The underscore (_) as wildcard/default case +- Pattern matching with guards +- Matching sequences and mappings +- Capturing values in patterns + +Note: This requires Python 3.10 or later! + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic match-case (Similar to switch/case) +# ----------------------------------------------------------------------------- +# The simplest form of pattern matching + +def get_day_name(day_number: int) -> str: + """Convert day number to day name.""" + match day_number: + case 1: + return "Monday" + case 2: + return "Tuesday" + case 3: + return "Wednesday" + case 4: + return "Thursday" + case 5: + return "Friday" + case 6: + return "Saturday" + case 7: + return "Sunday" + case _: # Default case (underscore is wildcard) + return "Invalid day" + +print("--- Basic match-case ---") +print(f"Day 1: {get_day_name(1)}") +print(f"Day 5: {get_day_name(5)}") +print(f"Day 9: {get_day_name(9)}") + +# ----------------------------------------------------------------------------- +# 2. Multiple Patterns in One Case +# ----------------------------------------------------------------------------- +# Use the | operator to match multiple values + +def categorize_day(day: str) -> str: + """Categorize day as weekday or weekend.""" + match day.lower(): + case "saturday" | "sunday": + return "Weekend - Time to relax!" + case "monday" | "tuesday" | "wednesday" | "thursday" | "friday": + return "Weekday - Time to work!" + case _: + return "Unknown day" + +print("\n--- Multiple Patterns ---") +print(f"Saturday: {categorize_day('Saturday')}") +print(f"Monday: {categorize_day('Monday')}") + +# ----------------------------------------------------------------------------- +# 3. Pattern Matching with Guards +# ----------------------------------------------------------------------------- +# Add conditions using 'if' after the pattern + +def evaluate_score(score: int) -> str: + """Evaluate score with guards.""" + match score: + case n if n < 0: + return "Invalid score (negative)" + case n if n > 100: + return "Invalid score (over 100)" + case n if n >= 90: + return f"Grade A - Excellent! ({n}%)" + case n if n >= 80: + return f"Grade B - Good! ({n}%)" + case n if n >= 70: + return f"Grade C - Satisfactory ({n}%)" + case n if n >= 60: + return f"Grade D - Pass ({n}%)" + case _: + return f"Grade F - Fail ({score}%)" + +print("\n--- Pattern with Guards ---") +print(evaluate_score(95)) +print(evaluate_score(75)) +print(evaluate_score(45)) +print(evaluate_score(-10)) + +# ----------------------------------------------------------------------------- +# 4. Matching Sequences (Tuples/Lists) +# ----------------------------------------------------------------------------- +# Match against list or tuple patterns + +def describe_point(point): + """Describe a point based on its coordinates.""" + match point: + case (0, 0): + return "Origin" + case (0, y): + return f"On Y-axis at y={y}" + case (x, 0): + return f"On X-axis at x={x}" + case (x, y) if x == y: + return f"On diagonal at ({x}, {y})" + case (x, y): + return f"Point at ({x}, {y})" + case _: + return "Not a valid point" + +print("\n--- Matching Sequences ---") +print(describe_point((0, 0))) +print(describe_point((0, 5))) +print(describe_point((3, 0))) +print(describe_point((4, 4))) +print(describe_point((3, 7))) + +# ----------------------------------------------------------------------------- +# 5. Matching with Variable Length Sequences +# ----------------------------------------------------------------------------- +# Use * to capture remaining elements + +def analyze_list(data): + """Analyze list structure.""" + match data: + case []: + return "Empty list" + case [single]: + return f"Single element: {single}" + case [first, second]: + return f"Two elements: {first} and {second}" + case [first, *middle, last]: + return f"First: {first}, Last: {last}, Middle count: {len(middle)}" + case _: + return "Not a list" + +print("\n--- Variable Length Matching ---") +print(analyze_list([])) +print(analyze_list([42])) +print(analyze_list([1, 2])) +print(analyze_list([1, 2, 3, 4, 5])) + +# ----------------------------------------------------------------------------- +# 6. Matching Dictionaries +# ----------------------------------------------------------------------------- +# Match against dictionary patterns + +def process_request(request): + """Process different types of requests.""" + match request: + case {"type": "login", "user": username}: + return f"User '{username}' is logging in" + case {"type": "logout", "user": username}: + return f"User '{username}' is logging out" + case {"type": "message", "user": username, "content": content}: + return f"Message from '{username}': {content}" + case {"type": action}: + return f"Unknown action: {action}" + case _: + return "Invalid request format" + +print("\n--- Matching Dictionaries ---") +print(process_request({"type": "login", "user": "Baraa"})) +print(process_request({"type": "message", "user": "Ali", "content": "Hello!"})) +print(process_request({"type": "logout", "user": "Sara"})) +print(process_request({"type": "unknown_action"})) + +# ----------------------------------------------------------------------------- +# 7. Matching Class Instances +# ----------------------------------------------------------------------------- +# Match against object attributes + +class Point: + """Simple point class for demonstration.""" + def __init__(self, x, y): + self.x = x + self.y = y + +def classify_point(point): + """Classify point using class matching.""" + match point: + case Point(x=0, y=0): + return "At origin" + case Point(x=0, y=y): + return f"On Y-axis at {y}" + case Point(x=x, y=0): + return f"On X-axis at {x}" + case Point(x=x, y=y): + return f"At ({x}, {y})" + case _: + return "Not a Point" + +print("\n--- Matching Class Instances ---") +print(classify_point(Point(0, 0))) +print(classify_point(Point(0, 7))) +print(classify_point(Point(5, 0))) +print(classify_point(Point(3, 4))) + +# ----------------------------------------------------------------------------- +# 8. Practical Example: Command Parser +# ----------------------------------------------------------------------------- +# Real-world use case for a simple command interpreter + +def execute_command(command): + """Parse and execute simple commands.""" + parts = command.split() + + match parts: + case ["quit"] | ["exit"] | ["q"]: + return "Goodbye!" + case ["hello"]: + return "Hello there!" + case ["hello", name]: + return f"Hello, {name}!" + case ["add", x, y]: + return f"Result: {int(x) + int(y)}" + case ["repeat", count, *words]: + return " ".join(words) * int(count) + case ["help"]: + return "Available: quit, hello [name], add x y, repeat n words..." + case [unknown, *_]: + return f"Unknown command: {unknown}. Type 'help' for assistance." + case _: + return "Please enter a command" + +print("\n--- Command Parser Example ---") +print(f"'hello': {execute_command('hello')}") +print(f"'hello Baraa': {execute_command('hello Baraa')}") +print(f"'add 5 3': {execute_command('add 5 3')}") +print(f"'repeat 3 Hi': {execute_command('repeat 3 Hi ')}") +print(f"'help': {execute_command('help')}") +print(f"'xyz': {execute_command('xyz')}") diff --git a/03_loops/01_for_loop.py b/03_loops/01_for_loop.py new file mode 100644 index 0000000..ff6793a --- /dev/null +++ b/03_loops/01_for_loop.py @@ -0,0 +1,228 @@ +""" +================================================================================ +File: 01_for_loop.py +Topic: For Loops in Python +================================================================================ + +This file demonstrates the 'for' loop in Python, which is used to iterate +over sequences (lists, tuples, strings, ranges, etc.) or any iterable object. + +Key Concepts: +- Basic for loop syntax +- range() function for numeric iterations +- Iterating over different data types +- enumerate() for index and value +- zip() for parallel iteration +- Nested for loops + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic For Loop +# ----------------------------------------------------------------------------- +# Iterate over items in a sequence + +print("--- Basic For Loop ---") + +fruits = ["apple", "banana", "cherry", "date"] + +for fruit in fruits: + print(f"I like {fruit}") + +# ----------------------------------------------------------------------------- +# 2. Using range() Function +# ----------------------------------------------------------------------------- +# range(start, stop, step) - generates numbers from start to stop-1 + +print("\n--- Using range() ---") + +# range(5) generates 0, 1, 2, 3, 4 +print("range(5):") +for i in range(5): + print(i, end=" ") +print() + +# range(2, 8) generates 2, 3, 4, 5, 6, 7 +print("\nrange(2, 8):") +for i in range(2, 8): + print(i, end=" ") +print() + +# range(0, 10, 2) generates 0, 2, 4, 6, 8 (step of 2) +print("\nrange(0, 10, 2):") +for i in range(0, 10, 2): + print(i, end=" ") +print() + +# Counting backwards: range(10, 0, -1) +print("\nrange(10, 0, -1) - Countdown:") +for i in range(10, 0, -1): + print(i, end=" ") +print("Blastoff!") + +# ----------------------------------------------------------------------------- +# 3. Iterating Over Strings +# ----------------------------------------------------------------------------- +# Each character in a string is an item + +print("\n--- Iterating Over Strings ---") + +word = "Python" +print(f"Characters in '{word}':") + +for char in word: + print(f" {char}") + +# ----------------------------------------------------------------------------- +# 4. Using enumerate() - Get Index and Value +# ----------------------------------------------------------------------------- +# enumerate() provides both the index and the item + +print("\n--- Using enumerate() ---") + +languages = ["Python", "JavaScript", "C++", "Rust"] + +for index, language in enumerate(languages): + print(f"{index + 1}. {language}") + +# Start from a custom number +print("\nWith custom start:") +for rank, language in enumerate(languages, start=1): + print(f"Rank {rank}: {language}") + +# ----------------------------------------------------------------------------- +# 5. Using zip() - Parallel Iteration +# ----------------------------------------------------------------------------- +# zip() combines multiple iterables and iterates over them together + +print("\n--- Using zip() ---") + +names = ["Alice", "Bob", "Charlie"] +ages = [25, 30, 35] +cities = ["New York", "London", "Tokyo"] + +# Iterate over multiple lists simultaneously +for name, age, city in zip(names, ages, cities): + print(f"{name} is {age} years old and lives in {city}") + +# ----------------------------------------------------------------------------- +# 6. Iterating Over Dictionaries +# ----------------------------------------------------------------------------- +# Different ways to loop through dictionary data + +print("\n--- Iterating Over Dictionaries ---") + +person = { + "name": "Baraa", + "age": 25, + "city": "Gaza", + "profession": "Developer" +} + +# Keys only (default) +print("Keys:") +for key in person: + print(f" {key}") + +# Values only +print("\nValues:") +for value in person.values(): + print(f" {value}") + +# Both keys and values +print("\nKey-Value pairs:") +for key, value in person.items(): + print(f" {key}: {value}") + +# ----------------------------------------------------------------------------- +# 7. Nested For Loops +# ----------------------------------------------------------------------------- +# A loop inside another loop + +print("\n--- Nested For Loops ---") + +# Multiplication table +print("Multiplication Table (1-5):") +for i in range(1, 6): + for j in range(1, 6): + print(f"{i * j:3}", end=" ") + print() # New line after each row + +# Working with 2D lists (matrices) +print("\n2D Matrix:") +matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] +] + +for row in matrix: + for cell in row: + print(f"{cell}", end=" ") + print() + +# ----------------------------------------------------------------------------- +# 8. Else Clause in For Loop +# ----------------------------------------------------------------------------- +# The else block executes when the loop completes without break + +print("\n--- For-Else Clause ---") + +# Search for a number +numbers = [1, 3, 5, 7, 9] +search_for = 5 + +for num in numbers: + if num == search_for: + print(f"Found {search_for}!") + break +else: + print(f"{search_for} was not found.") + +# ----------------------------------------------------------------------------- +# 9. List Comprehension (Related to For Loops) +# ----------------------------------------------------------------------------- +# A concise way to create lists using for loops + +print("\n--- List Comprehension Preview ---") + +# Traditional way +squares_traditional = [] +for x in range(1, 6): + squares_traditional.append(x ** 2) +print(f"Traditional: {squares_traditional}") + +# List comprehension way +squares_comprehension = [x ** 2 for x in range(1, 6)] +print(f"Comprehension: {squares_comprehension}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Sum of numbers +numbers = [10, 20, 30, 40, 50] +total = 0 +for num in numbers: + total += num +print(f"Sum of {numbers} = {total}") + +# Finding maximum +values = [23, 45, 12, 78, 34] +maximum = values[0] +for value in values: + if value > maximum: + maximum = value +print(f"Maximum in {values} = {maximum}") + +# Counting vowels in a string +text = "Hello, World!" +vowels = "aeiouAEIOU" +vowel_count = 0 +for char in text: + if char in vowels: + vowel_count += 1 +print(f"Vowels in '{text}': {vowel_count}") diff --git a/03_loops/02_while_loop.py b/03_loops/02_while_loop.py new file mode 100644 index 0000000..a389d82 --- /dev/null +++ b/03_loops/02_while_loop.py @@ -0,0 +1,263 @@ +""" +================================================================================ +File: 02_while_loop.py +Topic: While Loops in Python +================================================================================ + +This file demonstrates the 'while' loop in Python, which repeatedly executes +a block of code as long as a condition remains True. Unlike for loops, +while loops are typically used when the number of iterations is not known +in advance. + +Key Concepts: +- Basic while loop syntax +- Counter-controlled loops +- Sentinel-controlled loops (input validation) +- Infinite loops and how to handle them +- While-else construct + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic While Loop +# ----------------------------------------------------------------------------- +# Executes as long as condition is True + +print("--- Basic While Loop ---") + +count = 1 + +while count <= 5: + print(f"Count: {count}") + count += 1 # Don't forget to update the condition variable! + +print("Loop finished!") + +# ----------------------------------------------------------------------------- +# 2. Counter-Controlled While Loop +# ----------------------------------------------------------------------------- +# When you know how many iterations you need + +print("\n--- Counter-Controlled Loop ---") + +# Countdown +countdown = 5 + +print("Rocket Launch Countdown:") +while countdown > 0: + print(f"T-minus {countdown}...") + countdown -= 1 +print("Liftoff! 🚀") + +# ----------------------------------------------------------------------------- +# 3. Summing Numbers with While +# ----------------------------------------------------------------------------- +# Accumulator pattern + +print("\n--- Sum of Numbers ---") + +# Sum numbers from 1 to 10 +n = 1 +total = 0 + +while n <= 10: + total += n + n += 1 + +print(f"Sum of 1 to 10 = {total}") + +# ----------------------------------------------------------------------------- +# 4. Sentinel-Controlled Loop (Input Validation) +# ----------------------------------------------------------------------------- +# Loop until a specific condition is met + +print("\n--- Sentinel-Controlled Loop ---") + +# Simulating password validation (in practice, use input()) +attempts = 0 +max_attempts = 3 +correct_password = "secret123" + +# Simulated user inputs +user_inputs = ["wrong1", "wrong2", "secret123"] + +while attempts < max_attempts: + # In real code: password = input("Enter password: ") + password = user_inputs[attempts] + print(f"Attempt {attempts + 1}: Entered '{password}'") + + if password == correct_password: + print("Access granted! ✓") + break + else: + print("Incorrect password.") + attempts += 1 +else: + # This runs if the loop completes without break + print("Too many failed attempts. Account locked.") + +# ----------------------------------------------------------------------------- +# 5. While Loop with User Simulation +# ----------------------------------------------------------------------------- +# Menu-driven program example + +print("\n--- Menu-Driven Example ---") + +# Simulated menu choices +menu_selections = [1, 2, 4, 3] +selection_index = 0 + +running = True +while running: + # Simulating menu selection + choice = menu_selections[selection_index] + selection_index += 1 + + print(f"\nMenu: 1=Add, 2=View, 3=Exit | Choice: {choice}") + + if choice == 1: + print(" ➤ Adding item...") + elif choice == 2: + print(" ➤ Viewing items...") + elif choice == 3: + print(" ➤ Exiting program...") + running = False + else: + print(" ➤ Invalid choice, try again.") + +# ----------------------------------------------------------------------------- +# 6. Infinite Loops (and how to avoid them) +# ----------------------------------------------------------------------------- +# A loop that never ends - usually a bug + +print("\n--- Avoiding Infinite Loops ---") + +# BAD: This would run forever (commented out) +# i = 0 +# while i < 5: +# print(i) +# # Missing: i += 1 + +# GOOD: Always update the condition variable +i = 0 +while i < 5: + print(f"i = {i}") + i += 1 # This is crucial! + +# ----------------------------------------------------------------------------- +# 7. While-Else Construct +# ----------------------------------------------------------------------------- +# The else block executes if the loop completes normally (no break) + +print("\n--- While-Else Construct ---") + +# Example 1: Loop completes normally +n = 5 +while n > 0: + print(n, end=" ") + n -= 1 +else: + print("\n ↳ Countdown complete! (else block executed)") + +# Example 2: Loop exits via break +print("\nSearching in list:") +items = ["a", "b", "c", "d"] +target = "b" +index = 0 + +while index < len(items): + if items[index] == target: + print(f" Found '{target}' at index {index}") + break + index += 1 +else: + print(f" '{target}' not found") + +# ----------------------------------------------------------------------------- +# 8. Nested While Loops +# ----------------------------------------------------------------------------- +# A while inside another while + +print("\n--- Nested While Loops ---") + +# Create a simple pattern +row = 1 +while row <= 4: + col = 1 + while col <= row: + print("*", end=" ") + col += 1 + print() # New line + row += 1 + +# ----------------------------------------------------------------------------- +# 9. While with Break and Continue +# ----------------------------------------------------------------------------- +# Control flow within loops (preview - detailed in next file) + +print("\n--- Break and Continue in While ---") + +# Break example +print("Break example (stop at 5):") +i = 0 +while True: # Intentional infinite loop + i += 1 + if i > 5: + break + print(i, end=" ") +print() + +# Continue example +print("\nContinue example (skip even numbers):") +i = 0 +while i < 10: + i += 1 + if i % 2 == 0: + continue # Skip even numbers + print(i, end=" ") +print() + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Fibonacci sequence +print("Fibonacci sequence (first 10 numbers):") +a, b = 0, 1 +count = 0 +while count < 10: + print(a, end=" ") + a, b = b, a + b + count += 1 +print() + +# Finding digits in a number +print("\nDigits of 12345:") +number = 12345 +while number > 0: + digit = number % 10 + print(f" Digit: {digit}") + number //= 10 + +# Guessing game logic +print("\n Guess the Number Game Logic:") +secret = 7 +guesses = [3, 8, 5, 7] +guess_index = 0 + +while guess_index < len(guesses): + guess = guesses[guess_index] + print(f" Guess: {guess}", end=" ") + + if guess < secret: + print("- Too low!") + elif guess > secret: + print("- Too high!") + else: + print("- Correct! 🎉") + break + + guess_index += 1 diff --git a/03_loops/03_break_continue.py b/03_loops/03_break_continue.py new file mode 100644 index 0000000..cc202ee --- /dev/null +++ b/03_loops/03_break_continue.py @@ -0,0 +1,286 @@ +""" +================================================================================ +File: 03_break_continue.py +Topic: Loop Control Statements - break, continue, pass +================================================================================ + +This file demonstrates loop control statements that alter the normal flow +of loop execution. These are essential tools for writing efficient and +readable loops. + +Key Concepts: +- break: Exit the loop immediately +- continue: Skip to the next iteration +- pass: Do nothing (placeholder) +- Practical use cases for each + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. The break Statement +# ----------------------------------------------------------------------------- +# Immediately exits the loop, skipping any remaining iterations + +print("--- The break Statement ---") + +# Example 1: Exit when target is found +print("Finding first even number:") +numbers = [1, 3, 5, 8, 9, 10] + +for num in numbers: + print(f" Checking {num}...", end=" ") + if num % 2 == 0: + print(f"Found! {num} is even.") + break + print("Odd, continuing...") + +# Example 2: Exit on specific condition +print("\nSearching for name 'Charlie':") +names = ["Alice", "Bob", "Charlie", "David", "Eve"] + +for name in names: + if name == "Charlie": + print(f" ✓ Found '{name}'!") + break + print(f" Checking '{name}'...") + +# ----------------------------------------------------------------------------- +# 2. The continue Statement +# ----------------------------------------------------------------------------- +# Skips the rest of the current iteration and moves to the next + +print("\n--- The continue Statement ---") + +# Example 1: Skip negative numbers +print("Processing only positive numbers:") +values = [5, -2, 8, -1, 10, -3, 7] + +for value in values: + if value < 0: + print(f" Skipping {value} (negative)") + continue + print(f" Processing {value}") + +# Example 2: Skip specific items +print("\nPrinting all fruits except banana:") +fruits = ["apple", "banana", "cherry", "banana", "date"] + +for fruit in fruits: + if fruit == "banana": + continue + print(f" {fruit}") + +# ----------------------------------------------------------------------------- +# 3. The pass Statement +# ----------------------------------------------------------------------------- +# Does nothing - used as a placeholder + +print("\n--- The pass Statement ---") + +# Example 1: Placeholder in empty function/class +class FutureFeature: + pass # Will implement later + +def not_implemented_yet(): + pass # Placeholder for future code + +# Example 2: Explicit "do nothing" in conditionals +print("Processing numbers (ignoring zeros for now):") +numbers = [1, 0, 2, 0, 3] + +for num in numbers: + if num == 0: + pass # Explicitly do nothing (could be a TODO) + else: + print(f" Number: {num}") + +# ----------------------------------------------------------------------------- +# 4. break vs continue - Side by Side Comparison +# ----------------------------------------------------------------------------- + +print("\n--- break vs continue Comparison ---") + +data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +# With break - stops at 5 +print("With break (stop at 5):") +for num in data: + if num == 5: + break + print(num, end=" ") +print() + +# With continue - skips 5 +print("With continue (skip 5):") +for num in data: + if num == 5: + continue + print(num, end=" ") +print() + +# ----------------------------------------------------------------------------- +# 5. break with while Loops +# ----------------------------------------------------------------------------- + +print("\n--- break with while Loops ---") + +# Breaking out of an infinite loop +print("Processing until 'quit' command:") +commands = ["start", "process", "load", "quit", "save"] +index = 0 + +while True: + command = commands[index] + print(f" Command: {command}") + + if command == "quit": + print(" ↳ Exiting loop...") + break + + index += 1 + +# ----------------------------------------------------------------------------- +# 6. continue with while Loops +# ----------------------------------------------------------------------------- + +print("\n--- continue with while Loops ---") + +# Skip multiples of 3 +print("Numbers 1-10 (skipping multiples of 3):") +i = 0 +while i < 10: + i += 1 + if i % 3 == 0: + continue + print(i, end=" ") +print() + +# ----------------------------------------------------------------------------- +# 7. Nested Loops with break +# ----------------------------------------------------------------------------- +# break only exits the innermost loop + +print("\n--- Nested Loops with break ---") + +print("Breaking inner loop only:") +for i in range(1, 4): + print(f" Outer loop: i = {i}") + for j in range(1, 4): + if j == 2: + print(" ↳ Breaking inner loop at j = 2") + break + print(f" Inner loop: j = {j}") + +# Using a flag to break outer loop +print("\nBreaking outer loop with flag:") +stop_outer = False + +for i in range(1, 4): + if stop_outer: + break + for j in range(1, 4): + print(f" i={i}, j={j}") + if i == 2 and j == 2: + print(" ↳ Breaking both loops!") + stop_outer = True + break + +# ----------------------------------------------------------------------------- +# 8. for-else with break +# ----------------------------------------------------------------------------- +# else block runs only if loop completes without break + +print("\n--- for-else with break ---") + +# Example: Searching for a prime number +def is_prime(n): + """Check if n is prime.""" + if n < 2: + return False + for i in range(2, int(n ** 0.5) + 1): + if n % i == 0: + return False # Found a divisor, not prime + return True # No divisors found, is prime + +# Search for first prime in list +numbers = [4, 6, 8, 9, 11, 12] +print(f"Searching for prime in {numbers}:") + +for num in numbers: + if is_prime(num): + print(f" → First prime found: {num}") + break +else: + print(" → No primes found in the list") + +# ----------------------------------------------------------------------------- +# 9. Practical Example: Input Validation +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: Input Validation ---") + +# Simulating user input validation +test_inputs = ["abc", "-5", "150", "42"] +print("Validating inputs (must be 1-100):") + +for input_str in test_inputs: + print(f"\n Input: '{input_str}'") + + # Check if it's a number + if not input_str.lstrip('-').isdigit(): + print(" ✗ Error: Not a valid number") + continue + + value = int(input_str) + + # Check range + if value < 1: + print(" ✗ Error: Too low (must be >= 1)") + continue + + if value > 100: + print(" ✗ Error: Too high (must be <= 100)") + continue + + print(f" ✓ Valid input: {value}") + break +else: + print("\n No valid input found!") + +# ----------------------------------------------------------------------------- +# 10. Practical Example: Skip Processing on Error +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: Error Handling ---") + +# Processing a list of files (simulated) +files = [ + {"name": "data1.csv", "readable": True}, + {"name": "data2.csv", "readable": False}, # Error + {"name": "data3.csv", "readable": True}, + {"name": "corrupt.csv", "readable": True, "corrupt": True}, # Error + {"name": "data4.csv", "readable": True}, +] + +print("Processing files:") +processed_count = 0 + +for file in files: + name = file["name"] + + # Skip unreadable files + if not file.get("readable", False): + print(f" ✗ {name}: Cannot read file, skipping...") + continue + + # Skip corrupt files + if file.get("corrupt", False): + print(f" ✗ {name}: File is corrupt, skipping...") + continue + + # Process the file + print(f" ✓ {name}: Processing complete") + processed_count += 1 + +print(f"\nTotal files processed: {processed_count}/{len(files)}") diff --git a/04_data_structures/01_lists.py b/04_data_structures/01_lists.py new file mode 100644 index 0000000..6acebc6 --- /dev/null +++ b/04_data_structures/01_lists.py @@ -0,0 +1,287 @@ +""" +================================================================================ +File: 01_lists.py +Topic: Python Lists - Ordered, Mutable Collections +================================================================================ + +This file demonstrates Python lists, which are ordered, mutable collections +that can store elements of any type. Lists are one of the most versatile +and commonly used data structures in Python. + +Key Concepts: +- Creating and accessing lists +- List methods (append, insert, remove, pop, etc.) +- Slicing and indexing +- List operations (concatenation, repetition) +- Nested lists +- List comprehensions + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Creating Lists +# ----------------------------------------------------------------------------- +# Lists are created with square brackets [] + +print("--- Creating Lists ---") + +# Empty list +empty_list = [] +print(f"Empty list: {empty_list}") + +# List with elements +numbers = [1, 2, 3, 4, 5] +fruits = ["apple", "banana", "cherry"] +mixed = [1, "hello", 3.14, True, None] + +print(f"Numbers: {numbers}") +print(f"Fruits: {fruits}") +print(f"Mixed types: {mixed}") + +# Using list() constructor +chars = list("Python") +print(f"From string: {chars}") + +# List from range +range_list = list(range(1, 6)) +print(f"From range: {range_list}") + +# ----------------------------------------------------------------------------- +# 2. Accessing Elements (Indexing) +# ----------------------------------------------------------------------------- +# Lists are zero-indexed; negative indices count from the end + +print("\n--- Indexing ---") + +colors = ["red", "green", "blue", "yellow", "purple"] + +print(f"List: {colors}") +print(f"First element (index 0): {colors[0]}") +print(f"Third element (index 2): {colors[2]}") +print(f"Last element (index -1): {colors[-1]}") +print(f"Second to last (index -2): {colors[-2]}") + +# ----------------------------------------------------------------------------- +# 3. Slicing Lists +# ----------------------------------------------------------------------------- +# Syntax: list[start:stop:step] + +print("\n--- Slicing ---") + +nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +print(f"Original: {nums}") + +print(f"nums[2:5]: {nums[2:5]}") # Elements 2, 3, 4 +print(f"nums[:4]: {nums[:4]}") # First 4 elements +print(f"nums[6:]: {nums[6:]}") # From index 6 to end +print(f"nums[::2]: {nums[::2]}") # Every 2nd element +print(f"nums[::-1]: {nums[::-1]}") # Reversed list +print(f"nums[1:8:2]: {nums[1:8:2]}") # Odd indices from 1 to 7 + +# ----------------------------------------------------------------------------- +# 4. Modifying Lists +# ----------------------------------------------------------------------------- +# Lists are mutable - you can change their contents + +print("\n--- Modifying Lists ---") + +languages = ["Python", "Java", "C++"] +print(f"Original: {languages}") + +# Change single element +languages[1] = "JavaScript" +print(f"After changing index 1: {languages}") + +# Change multiple elements with slicing +languages[0:2] = ["Rust", "Go"] +print(f"After slice replacement: {languages}") + +# ----------------------------------------------------------------------------- +# 5. List Methods +# ----------------------------------------------------------------------------- +# Built-in methods to manipulate lists + +print("\n--- List Methods ---") + +# append() - Add element to end +items = [1, 2, 3] +items.append(4) +print(f"After append(4): {items}") + +# extend() - Add multiple elements +items.extend([5, 6]) +print(f"After extend([5, 6]): {items}") + +# insert() - Add element at specific position +items.insert(0, 0) # Insert 0 at index 0 +print(f"After insert(0, 0): {items}") + +# remove() - Remove first occurrence of value +items.remove(3) +print(f"After remove(3): {items}") + +# pop() - Remove and return element at index (default: last) +popped = items.pop() +print(f"Popped: {popped}, List now: {items}") + +popped = items.pop(0) +print(f"Popped at 0: {popped}, List now: {items}") + +# index() - Find index of first occurrence +fruits = ["apple", "banana", "cherry", "banana"] +print(f"\nfruits = {fruits}") +print(f"Index of 'banana': {fruits.index('banana')}") + +# count() - Count occurrences +print(f"Count of 'banana': {fruits.count('banana')}") + +# sort() - Sort in place +numbers = [3, 1, 4, 1, 5, 9, 2, 6] +numbers.sort() +print(f"\nAfter sort(): {numbers}") + +numbers.sort(reverse=True) +print(f"After sort(reverse=True): {numbers}") + +# reverse() - Reverse in place +numbers.reverse() +print(f"After reverse(): {numbers}") + +# copy() - Create a shallow copy +original = [1, 2, 3] +copied = original.copy() +print(f"\nOriginal: {original}, Copy: {copied}") + +# clear() - Remove all elements +copied.clear() +print(f"After clear(): {copied}") + +# ----------------------------------------------------------------------------- +# 6. List Operations +# ----------------------------------------------------------------------------- + +print("\n--- List Operations ---") + +# Concatenation (+) +list1 = [1, 2, 3] +list2 = [4, 5, 6] +combined = list1 + list2 +print(f"{list1} + {list2} = {combined}") + +# Repetition (*) +repeated = [0] * 5 +print(f"[0] * 5 = {repeated}") + +# Membership (in) +fruits = ["apple", "banana", "cherry"] +print(f"'banana' in fruits: {'banana' in fruits}") +print(f"'grape' in fruits: {'grape' in fruits}") + +# Length +print(f"len(fruits): {len(fruits)}") + +# Min and Max +numbers = [5, 2, 8, 1, 9] +print(f"min({numbers}): {min(numbers)}") +print(f"max({numbers}): {max(numbers)}") +print(f"sum({numbers}): {sum(numbers)}") + +# ----------------------------------------------------------------------------- +# 7. Nested Lists (2D Lists) +# ----------------------------------------------------------------------------- + +print("\n--- Nested Lists ---") + +# Creating a 2D list (matrix) +matrix = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] +] + +print("Matrix:") +for row in matrix: + print(f" {row}") + +# Accessing elements +print(f"\nElement at [1][2]: {matrix[1][2]}") # Row 1, Column 2 = 6 +print(f"Second row: {matrix[1]}") + +# Modifying nested element +matrix[0][0] = 100 +print(f"After matrix[0][0] = 100: {matrix[0]}") + +# ----------------------------------------------------------------------------- +# 8. List Comprehensions +# ----------------------------------------------------------------------------- +# Concise way to create lists + +print("\n--- List Comprehensions ---") + +# Basic list comprehension +squares = [x**2 for x in range(1, 6)] +print(f"Squares 1-5: {squares}") + +# With condition +even_squares = [x**2 for x in range(1, 11) if x % 2 == 0] +print(f"Even squares: {even_squares}") + +# With expression +words = ["hello", "world", "python"] +upper_words = [word.upper() for word in words] +print(f"Uppercase: {upper_words}") + +# Nested comprehension (flattening) +matrix = [[1, 2], [3, 4], [5, 6]] +flattened = [num for row in matrix for num in row] +print(f"Flattened: {flattened}") + +# ----------------------------------------------------------------------------- +# 9. Copying Lists - Shallow vs Deep +# ----------------------------------------------------------------------------- + +print("\n--- Copying Lists ---") + +import copy + +# Shallow copy - nested objects share reference +original = [[1, 2], [3, 4]] +shallow = original.copy() +shallow[0][0] = 999 # Affects both! +print(f"Shallow copy issue:") +print(f" Original: {original}") +print(f" Shallow: {shallow}") + +# Deep copy - completely independent +original = [[1, 2], [3, 4]] +deep = copy.deepcopy(original) +deep[0][0] = 999 # Only affects copy +print(f"\nDeep copy:") +print(f" Original: {original}") +print(f" Deep: {deep}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Finding unique elements while preserving order +items = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] +unique = [] +for item in items: + if item not in unique: + unique.append(item) +print(f"Unique elements: {unique}") + +# Filtering with list comprehension +scores = [85, 42, 91, 78, 55, 99, 66] +passing = [score for score in scores if score >= 60] +print(f"Passing scores: {passing}") + +# Transforming data +temperatures_c = [0, 10, 20, 30, 40] +temperatures_f = [(c * 9/5) + 32 for c in temperatures_c] +print(f"Celsius: {temperatures_c}") +print(f"Fahrenheit: {temperatures_f}") diff --git a/04_data_structures/02_tuples.py b/04_data_structures/02_tuples.py new file mode 100644 index 0000000..7870ade --- /dev/null +++ b/04_data_structures/02_tuples.py @@ -0,0 +1,286 @@ +""" +================================================================================ +File: 02_tuples.py +Topic: Python Tuples - Ordered, Immutable Collections +================================================================================ + +This file demonstrates Python tuples, which are ordered, immutable collections. +Unlike lists, tuples cannot be modified after creation, making them useful +for data that should not change. + +Key Concepts: +- Creating tuples +- Tuple immutability +- Accessing elements +- Tuple unpacking +- When to use tuples vs lists +- Named tuples for clarity + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Creating Tuples +# ----------------------------------------------------------------------------- +# Tuples are created with parentheses () or just commas + +print("--- Creating Tuples ---") + +# Empty tuple +empty_tuple = () +print(f"Empty tuple: {empty_tuple}") + +# Tuple with elements +coordinates = (10, 20) +colors = ("red", "green", "blue") +mixed = (1, "hello", 3.14, True) + +print(f"Coordinates: {coordinates}") +print(f"Colors: {colors}") +print(f"Mixed types: {mixed}") + +# Single element tuple - needs trailing comma! +single = (42,) # This is a tuple +not_tuple = (42) # This is just an integer! +print(f"\nSingle element tuple: {single}, type: {type(single)}") +print(f"Without comma: {not_tuple}, type: {type(not_tuple)}") + +# Tuple without parentheses (packing) +packed = 1, 2, 3 +print(f"Packed tuple: {packed}, type: {type(packed)}") + +# Using tuple() constructor +from_list = tuple([1, 2, 3]) +from_string = tuple("Python") +print(f"From list: {from_list}") +print(f"From string: {from_string}") + +# ----------------------------------------------------------------------------- +# 2. Accessing Elements +# ----------------------------------------------------------------------------- +# Same indexing and slicing as lists + +print("\n--- Accessing Elements ---") + +fruits = ("apple", "banana", "cherry", "date", "elderberry") +print(f"Tuple: {fruits}") + +print(f"First element: {fruits[0]}") +print(f"Last element: {fruits[-1]}") +print(f"Slice [1:4]: {fruits[1:4]}") +print(f"Every other: {fruits[::2]}") + +# ----------------------------------------------------------------------------- +# 3. Tuple Immutability +# ----------------------------------------------------------------------------- +# Tuples cannot be modified after creation + +print("\n--- Immutability ---") + +point = (10, 20, 30) +print(f"Original point: {point}") + +# This would raise an error: +# point[0] = 100 # TypeError: 'tuple' object does not support item assignment + +# However, you can create a new tuple +point = (100,) + point[1:] # Create new tuple +print(f"New point: {point}") + +# Note: Mutable objects inside tuples can still be modified +mutable_inside = ([1, 2], [3, 4]) +mutable_inside[0].append(3) # This works! +print(f"Mutable inside tuple: {mutable_inside}") + +# ----------------------------------------------------------------------------- +# 4. Tuple Unpacking +# ----------------------------------------------------------------------------- +# Assign tuple elements to multiple variables + +print("\n--- Tuple Unpacking ---") + +# Basic unpacking +coordinates = (100, 200, 300) +x, y, z = coordinates +print(f"Unpacked: x={x}, y={y}, z={z}") + +# Swap values using tuple unpacking +a, b = 1, 2 +print(f"Before swap: a={a}, b={b}") +a, b = b, a # Elegant swap! +print(f"After swap: a={a}, b={b}") + +# Unpacking with * (extended unpacking) +numbers = (1, 2, 3, 4, 5) +first, *middle, last = numbers +print(f"\nExtended unpacking:") +print(f" first: {first}") +print(f" middle: {middle}") # This becomes a list! +print(f" last: {last}") + +# Ignoring values with underscore +data = ("John", 25, "Developer", "NYC") +name, _, job, _ = data # Ignore age and city +print(f"Name: {name}, Job: {job}") + +# ----------------------------------------------------------------------------- +# 5. Tuple Methods +# ----------------------------------------------------------------------------- +# Tuples have only two methods (because they're immutable) + +print("\n--- Tuple Methods ---") + +numbers = (1, 2, 3, 2, 4, 2, 5) +print(f"Tuple: {numbers}") + +# count() - Count occurrences +print(f"Count of 2: {numbers.count(2)}") + +# index() - Find index of first occurrence +print(f"Index of 4: {numbers.index(4)}") + +# ----------------------------------------------------------------------------- +# 6. Tuple Operations +# ----------------------------------------------------------------------------- + +print("\n--- Tuple Operations ---") + +# Concatenation +t1 = (1, 2, 3) +t2 = (4, 5, 6) +combined = t1 + t2 +print(f"{t1} + {t2} = {combined}") + +# Repetition +repeated = (0,) * 5 +print(f"(0,) * 5 = {repeated}") + +# Membership +colors = ("red", "green", "blue") +print(f"'green' in colors: {'green' in colors}") + +# Length +print(f"len(colors): {len(colors)}") + +# Min, Max, Sum +nums = (5, 2, 8, 1, 9) +print(f"min: {min(nums)}, max: {max(nums)}, sum: {sum(nums)}") + +# ----------------------------------------------------------------------------- +# 7. Tuples as Dictionary Keys +# ----------------------------------------------------------------------------- +# Tuples can be used as dictionary keys (lists cannot) + +print("\n--- Tuples as Dictionary Keys ---") + +# Mapping (x, y) coordinates to location names +locations = { + (40.7128, -74.0060): "New York City", + (34.0522, -118.2437): "Los Angeles", + (51.5074, -0.1278): "London" +} + +print("Locations dictionary:") +for coords, city in locations.items(): + print(f" {coords}: {city}") + +# Access by tuple key +coord = (40.7128, -74.0060) +print(f"\nLocation at {coord}: {locations[coord]}") + +# ----------------------------------------------------------------------------- +# 8. Returning Multiple Values from Functions +# ----------------------------------------------------------------------------- +# Functions can return tuples + +print("\n--- Functions Returning Tuples ---") + +def get_stats(numbers): + """Return multiple statistics as a tuple.""" + return min(numbers), max(numbers), sum(numbers) / len(numbers) + +data = [10, 20, 30, 40, 50] +minimum, maximum, average = get_stats(data) +print(f"Stats for {data}:") +print(f" Min: {minimum}, Max: {maximum}, Avg: {average}") + +def divide_with_remainder(a, b): + """Return quotient and remainder.""" + return a // b, a % b + +quotient, remainder = divide_with_remainder(17, 5) +print(f"\n17 ÷ 5 = {quotient} remainder {remainder}") + +# ----------------------------------------------------------------------------- +# 9. Named Tuples +# ----------------------------------------------------------------------------- +# Tuples with named fields for clarity + +print("\n--- Named Tuples ---") + +from collections import namedtuple + +# Define a named tuple type +Person = namedtuple("Person", ["name", "age", "city"]) + +# Create instances +person1 = Person("Baraa", 25, "Gaza") +person2 = Person(name="Sara", age=30, city="Cairo") + +print(f"Person 1: {person1}") +print(f"Person 2: {person2}") + +# Access by name or index +print(f"\nAccess by name: {person1.name}") +print(f"Access by index: {person1[0]}") + +# Named tuple is still immutable +# person1.age = 26 # This would raise an error + +# Convert to dictionary +print(f"As dict: {person1._asdict()}") + +# Create from dictionary +data = {"name": "Ali", "age": 28, "city": "Dubai"} +person3 = Person(**data) +print(f"From dict: {person3}") + +# ----------------------------------------------------------------------------- +# 10. When to Use Tuples vs Lists +# ----------------------------------------------------------------------------- + +print("\n--- Tuples vs Lists ---") + +# Use TUPLES when: +# 1. Data should not change +rgb_red = (255, 0, 0) # Color values shouldn't change + +# 2. Using as dictionary keys +grid_positions = {(0, 0): "start", (9, 9): "end"} + +# 3. Returning multiple values from functions +def get_point(): return (10, 20) + +# 4. Heterogeneous data (different types, specific meaning) +person = ("John", 30, "Engineer") # name, age, job + +# Use LISTS when: +# 1. Data will be modified +shopping_cart = ["apple", "bread"] # Items will be added/removed + +# 2. Homogeneous collection +scores = [85, 90, 78, 92] # All same type, will be processed together + +# 3. Order matters and you need to sort/shuffle +rankings = [3, 1, 4, 1, 5] +rankings.sort() # Lists are sortable in place + +print("Summary:") +print(" Tuples: Immutable, hashable, for fixed data") +print(" Lists: Mutable, flexible, for changing data") + +# Performance note: Tuples are slightly faster and use less memory +import sys +list_size = sys.getsizeof([1, 2, 3, 4, 5]) +tuple_size = sys.getsizeof((1, 2, 3, 4, 5)) +print(f"\nMemory: List={list_size} bytes, Tuple={tuple_size} bytes") diff --git a/04_data_structures/03_sets.py b/04_data_structures/03_sets.py new file mode 100644 index 0000000..4990eb3 --- /dev/null +++ b/04_data_structures/03_sets.py @@ -0,0 +1,299 @@ +""" +================================================================================ +File: 03_sets.py +Topic: Python Sets - Unordered Collections of Unique Elements +================================================================================ + +This file demonstrates Python sets, which are unordered collections that +store only unique elements. Sets are highly optimized for membership testing +and mathematical set operations. + +Key Concepts: +- Creating sets +- Adding and removing elements +- Set operations (union, intersection, difference) +- Frozen sets (immutable sets) +- Use cases for sets + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Creating Sets +# ----------------------------------------------------------------------------- +# Sets are created with curly braces {} or set() + +print("--- Creating Sets ---") + +# Using curly braces +fruits = {"apple", "banana", "cherry"} +print(f"Fruits set: {fruits}") + +# Duplicates are automatically removed +numbers = {1, 2, 2, 3, 3, 3, 4} +print(f"Numbers (duplicates removed): {numbers}") + +# Empty set - must use set(), not {} +empty_set = set() # {} creates an empty dictionary! +print(f"Empty set: {empty_set}, type: {type(empty_set)}") +print(f"Empty dict: {{}}, type: {type({})}") + +# From other iterables +from_list = set([1, 2, 3, 4, 5]) +from_string = set("hello") # Unique characters +from_tuple = set((10, 20, 30)) +print(f"\nFrom list: {from_list}") +print(f"From string 'hello': {from_string}") +print(f"From tuple: {from_tuple}") + +# ----------------------------------------------------------------------------- +# 2. Set Characteristics +# ----------------------------------------------------------------------------- +# Unordered, no duplicates, no indexing + +print("\n--- Set Characteristics ---") + +colors = {"red", "green", "blue"} + +# Sets are unordered - no indexing +# colors[0] # This would raise an error! + +# Check membership (very fast - O(1)) +print(f"'red' in colors: {'red' in colors}") +print(f"'yellow' in colors: {'yellow' in colors}") + +# Length +print(f"Number of colors: {len(colors)}") + +# Sets can only contain immutable (hashable) elements +valid_set = {1, "hello", (1, 2), 3.14} # OK +# invalid_set = {1, [2, 3]} # Error! Lists are not hashable + +# ----------------------------------------------------------------------------- +# 3. Adding and Removing Elements +# ----------------------------------------------------------------------------- + +print("\n--- Adding and Removing Elements ---") + +languages = {"Python", "Java"} +print(f"Initial: {languages}") + +# add() - Add single element +languages.add("JavaScript") +print(f"After add('JavaScript'): {languages}") + +# Adding duplicate has no effect +languages.add("Python") +print(f"After add('Python'): {languages}") + +# update() - Add multiple elements +languages.update(["C++", "Rust"]) +print(f"After update(['C++', 'Rust']): {languages}") + +# remove() - Remove element (raises error if not found) +languages.remove("Java") +print(f"After remove('Java'): {languages}") + +# discard() - Remove element (no error if not found) +languages.discard("Go") # No error even though "Go" isn't in set +print(f"After discard('Go'): {languages}") + +# pop() - Remove and return an arbitrary element +popped = languages.pop() +print(f"Popped: {popped}, Remaining: {languages}") + +# clear() - Remove all elements +temp_set = {1, 2, 3} +temp_set.clear() +print(f"After clear(): {temp_set}") + +# ----------------------------------------------------------------------------- +# 4. Set Operations - Mathematical Operations +# ----------------------------------------------------------------------------- + +print("\n--- Set Operations ---") + +a = {1, 2, 3, 4, 5} +b = {4, 5, 6, 7, 8} + +print(f"Set A: {a}") +print(f"Set B: {b}") + +# Union - Elements in A or B or both +union = a | b # or a.union(b) +print(f"\nUnion (A | B): {union}") + +# Intersection - Elements in both A and B +intersection = a & b # or a.intersection(b) +print(f"Intersection (A & B): {intersection}") + +# Difference - Elements in A but not in B +difference = a - b # or a.difference(b) +print(f"Difference (A - B): {difference}") + +difference_ba = b - a +print(f"Difference (B - A): {difference_ba}") + +# Symmetric Difference - Elements in A or B but not both +sym_diff = a ^ b # or a.symmetric_difference(b) +print(f"Symmetric Difference (A ^ B): {sym_diff}") + +# ----------------------------------------------------------------------------- +# 5. Set Comparison Operations +# ----------------------------------------------------------------------------- + +print("\n--- Set Comparisons ---") + +numbers = {1, 2, 3, 4, 5} +subset = {2, 3} +same = {1, 2, 3, 4, 5} +different = {6, 7, 8} + +print(f"numbers: {numbers}") +print(f"subset: {subset}") + +# issubset() - Is subset contained in numbers? +print(f"\nsubset.issubset(numbers): {subset.issubset(numbers)}") +print(f"subset <= numbers: {subset <= numbers}") + +# issuperset() - Does numbers contain subset? +print(f"numbers.issuperset(subset): {numbers.issuperset(subset)}") +print(f"numbers >= subset: {numbers >= subset}") + +# isdisjoint() - Do they share any elements? +print(f"\nnumbers.isdisjoint(different): {numbers.isdisjoint(different)}") +print(f"numbers.isdisjoint(subset): {numbers.isdisjoint(subset)}") + +# Equality +print(f"\nnumbers == same: {numbers == same}") +print(f"numbers == subset: {numbers == subset}") + +# ----------------------------------------------------------------------------- +# 6. Update Operations (Modify in Place) +# ----------------------------------------------------------------------------- + +print("\n--- Update Operations ---") + +# These modify the set in place instead of creating new ones + +x = {1, 2, 3} +y = {3, 4, 5} + +# update() - Union in place (|=) +x_copy = x.copy() +x_copy.update(y) # or x_copy |= y +print(f"After update (union): {x_copy}") + +# intersection_update() - Intersection in place (&=) +x_copy = x.copy() +x_copy.intersection_update(y) # or x_copy &= y +print(f"After intersection_update: {x_copy}") + +# difference_update() - Difference in place (-=) +x_copy = x.copy() +x_copy.difference_update(y) # or x_copy -= y +print(f"After difference_update: {x_copy}") + +# symmetric_difference_update() - Symmetric difference in place (^=) +x_copy = x.copy() +x_copy.symmetric_difference_update(y) # or x_copy ^= y +print(f"After symmetric_difference_update: {x_copy}") + +# ----------------------------------------------------------------------------- +# 7. Frozen Sets (Immutable Sets) +# ----------------------------------------------------------------------------- + +print("\n--- Frozen Sets ---") + +# Frozen sets are immutable versions of sets +frozen = frozenset([1, 2, 3, 4, 5]) +print(f"Frozen set: {frozen}") + +# Can perform operations but not modify +print(f"3 in frozen: {3 in frozen}") +print(f"len(frozen): {len(frozen)}") + +# These would raise errors: +# frozen.add(6) +# frozen.remove(1) + +# Frozen sets can be used as dictionary keys or set elements +nested = {frozenset([1, 2]), frozenset([3, 4])} +print(f"Set of frozensets: {nested}") + +# ----------------------------------------------------------------------------- +# 8. Set Comprehensions +# ----------------------------------------------------------------------------- + +print("\n--- Set Comprehensions ---") + +# Basic set comprehension +squares = {x**2 for x in range(1, 6)} +print(f"Squares: {squares}") + +# With condition +even_squares = {x**2 for x in range(1, 11) if x % 2 == 0} +print(f"Even squares: {even_squares}") + +# From string - unique vowels +text = "Hello, World!" +vowels = {char.lower() for char in text if char.lower() in "aeiou"} +print(f"Unique vowels in '{text}': {vowels}") + +# ----------------------------------------------------------------------------- +# 9. Practical Use Cases +# ----------------------------------------------------------------------------- + +print("\n--- Practical Use Cases ---") + +# 1. Remove duplicates from a list +my_list = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] +unique_list = list(set(my_list)) +print(f"Remove duplicates: {my_list} → {unique_list}") + +# 2. Find common elements +list1 = [1, 2, 3, 4, 5] +list2 = [4, 5, 6, 7, 8] +common = set(list1) & set(list2) +print(f"Common elements: {common}") + +# 3. Find unique elements across lists +all_elements = set(list1) | set(list2) +print(f"All unique elements: {all_elements}") + +# 4. Check if all required items exist +required_skills = {"Python", "SQL", "Git"} +candidate_skills = {"Python", "SQL", "Git", "Docker", "AWS"} +has_all_required = required_skills.issubset(candidate_skills) +print(f"\nCandidate has all required skills: {has_all_required}") + +# 5. Find missing items +available_items = {"apple", "banana", "orange"} +shopping_list = {"apple", "milk", "bread", "banana"} +need_to_buy = shopping_list - available_items +print(f"Need to buy: {need_to_buy}") + +# 6. Count unique words +text = "the quick brown fox jumps over the lazy dog" +unique_words = set(text.split()) +print(f"\nUnique words in text: {len(unique_words)}") +print(f"Words: {unique_words}") + +# ----------------------------------------------------------------------------- +# 10. Performance: Sets vs Lists for Membership Testing +# ----------------------------------------------------------------------------- + +print("\n--- Performance Note ---") + +# Sets use hash tables - O(1) lookup +# Lists use linear search - O(n) lookup + +big_list = list(range(10000)) +big_set = set(range(10000)) + +# For membership testing: +# 9999 in big_list # Slow - checks up to 10000 elements +# 9999 in big_set # Fast - direct hash lookup + +print("For membership testing, sets are MUCH faster than lists!") +print("Use sets when you frequently check if items exist in a collection.") diff --git a/04_data_structures/04_dictionaries.py b/04_data_structures/04_dictionaries.py new file mode 100644 index 0000000..db7063d --- /dev/null +++ b/04_data_structures/04_dictionaries.py @@ -0,0 +1,351 @@ +""" +================================================================================ +File: 04_dictionaries.py +Topic: Python Dictionaries - Key-Value Pair Collections +================================================================================ + +This file demonstrates Python dictionaries, which store data as key-value pairs. +Dictionaries are extremely versatile and provide fast access to values using +their associated keys. + +Key Concepts: +- Creating and accessing dictionaries +- Adding, modifying, and removing key-value pairs +- Dictionary methods +- Iterating over dictionaries +- Nested dictionaries +- Dictionary comprehensions + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Creating Dictionaries +# ----------------------------------------------------------------------------- +# Dictionaries use curly braces with key: value syntax + +print("--- Creating Dictionaries ---") + +# Empty dictionary +empty_dict = {} +also_empty = dict() +print(f"Empty dict: {empty_dict}") + +# Dictionary with elements +person = { + "name": "Baraa", + "age": 25, + "city": "Gaza", + "is_developer": True +} +print(f"Person: {person}") + +# Using dict() constructor +from_pairs = dict([("a", 1), ("b", 2), ("c", 3)]) +from_kwargs = dict(x=10, y=20, z=30) +print(f"From pairs: {from_pairs}") +print(f"From kwargs: {from_kwargs}") + +# Keys can be any immutable type +mixed_keys = { + "string_key": "value1", + 42: "value2", + (1, 2): "value3", # Tuple as key + True: "value4" +} +print(f"Mixed keys: {mixed_keys}") + +# ----------------------------------------------------------------------------- +# 2. Accessing Values +# ----------------------------------------------------------------------------- + +print("\n--- Accessing Values ---") + +student = { + "name": "Ali", + "age": 20, + "grades": [85, 90, 78], + "active": True +} + +# Using square brackets +print(f"Name: {student['name']}") +print(f"Grades: {student['grades']}") + +# Using get() - safer, returns None if key doesn't exist +print(f"Age: {student.get('age')}") +print(f"Email: {student.get('email')}") # Returns None +print(f"Email with default: {student.get('email', 'Not provided')}") + +# Accessing nested values +print(f"First grade: {student['grades'][0]}") + +# ----------------------------------------------------------------------------- +# 3. Modifying Dictionaries +# ----------------------------------------------------------------------------- + +print("\n--- Modifying Dictionaries ---") + +config = {"theme": "dark", "font_size": 14} +print(f"Original: {config}") + +# Update existing key +config["font_size"] = 16 +print(f"After update: {config}") + +# Add new key +config["language"] = "English" +print(f"After adding key: {config}") + +# Update multiple keys at once +config.update({"theme": "light", "auto_save": True}) +print(f"After update(): {config}") + +# Using setdefault() - only adds if key doesn't exist +config.setdefault("font_size", 20) # Won't change (key exists) +config.setdefault("new_key", "default_value") # Will add +print(f"After setdefault(): {config}") + +# ----------------------------------------------------------------------------- +# 4. Removing Items +# ----------------------------------------------------------------------------- + +print("\n--- Removing Items ---") + +data = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5} +print(f"Original: {data}") + +# pop() - Remove and return value +removed = data.pop("c") +print(f"Popped 'c': {removed}, Dict: {data}") + +# pop() with default - no error if key missing +removed = data.pop("z", "Not found") +print(f"Popped 'z': {removed}") + +# popitem() - Remove and return last inserted pair +last_item = data.popitem() +print(f"Popitem: {last_item}, Dict: {data}") + +# del - Delete specific key +del data["b"] +print(f"After del 'b': {data}") + +# clear() - Remove all items +temp = {"x": 1, "y": 2} +temp.clear() +print(f"After clear(): {temp}") + +# ----------------------------------------------------------------------------- +# 5. Dictionary Methods +# ----------------------------------------------------------------------------- + +print("\n--- Dictionary Methods ---") + +info = {"name": "Sara", "age": 28, "job": "Engineer"} + +# keys() - Get all keys +print(f"Keys: {list(info.keys())}") + +# values() - Get all values +print(f"Values: {list(info.values())}") + +# items() - Get all key-value pairs +print(f"Items: {list(info.items())}") + +# Check if key exists +print(f"\n'name' in info: {'name' in info}") +print(f"'email' in info: {'email' in info}") + +# Copy +original = {"a": 1, "b": 2} +copied = original.copy() +copied["c"] = 3 +print(f"\nOriginal: {original}") +print(f"Copy: {copied}") + +# fromkeys() - Create dict with same value for all keys +keys = ["x", "y", "z"] +default_dict = dict.fromkeys(keys, 0) +print(f"From keys: {default_dict}") + +# ----------------------------------------------------------------------------- +# 6. Iterating Over Dictionaries +# ----------------------------------------------------------------------------- + +print("\n--- Iterating Over Dictionaries ---") + +scores = {"Alice": 95, "Bob": 87, "Charlie": 92} + +# Iterate over keys (default) +print("Keys:") +for key in scores: + print(f" {key}") + +# Iterate over values +print("\nValues:") +for value in scores.values(): + print(f" {value}") + +# Iterate over key-value pairs +print("\nKey-Value pairs:") +for name, score in scores.items(): + print(f" {name}: {score}") + +# With enumerate (if you need index) +print("\nWith index:") +for idx, (name, score) in enumerate(scores.items(), 1): + print(f" {idx}. {name} scored {score}") + +# ----------------------------------------------------------------------------- +# 7. Nested Dictionaries +# ----------------------------------------------------------------------------- + +print("\n--- Nested Dictionaries ---") + +# Dictionary containing dictionaries +company = { + "engineering": { + "lead": "Alice", + "members": ["Bob", "Charlie"], + "budget": 50000 + }, + "marketing": { + "lead": "David", + "members": ["Eve", "Frank"], + "budget": 30000 + }, + "hr": { + "lead": "Grace", + "members": ["Henry"], + "budget": 20000 + } +} + +print("Company structure:") +for dept, details in company.items(): + print(f"\n {dept.title()} Department:") + print(f" Lead: {details['lead']}") + print(f" Members: {details['members']}") + print(f" Budget: ${details['budget']:,}") + +# Access nested values +print(f"\nEngineering lead: {company['engineering']['lead']}") +print(f"Marketing members: {company['marketing']['members']}") + +# Modify nested value +company['hr']['budget'] = 25000 +print(f"Updated HR budget: {company['hr']['budget']}") + +# ----------------------------------------------------------------------------- +# 8. Dictionary Comprehensions +# ----------------------------------------------------------------------------- + +print("\n--- Dictionary Comprehensions ---") + +# Basic dictionary comprehension +squares = {x: x**2 for x in range(1, 6)} +print(f"Squares: {squares}") + +# With condition +even_squares = {x: x**2 for x in range(1, 11) if x % 2 == 0} +print(f"Even squares: {even_squares}") + +# Transform existing dictionary +original = {"a": 1, "b": 2, "c": 3} +doubled = {k: v * 2 for k, v in original.items()} +print(f"Doubled values: {doubled}") + +# Swap keys and values +flipped = {v: k for k, v in original.items()} +print(f"Flipped: {flipped}") + +# Filter dictionary +scores = {"Alice": 95, "Bob": 67, "Charlie": 82, "David": 55} +passing = {name: score for name, score in scores.items() if score >= 70} +print(f"Passing students: {passing}") + +# From two lists +keys = ["name", "age", "city"] +values = ["John", 30, "NYC"] +combined = {k: v for k, v in zip(keys, values)} +print(f"Combined: {combined}") + +# ----------------------------------------------------------------------------- +# 9. Merging Dictionaries +# ----------------------------------------------------------------------------- + +print("\n--- Merging Dictionaries ---") + +dict1 = {"a": 1, "b": 2} +dict2 = {"c": 3, "d": 4} +dict3 = {"b": 99, "e": 5} # Note: 'b' also exists in dict1 + +# Method 1: update() - modifies in place +merged = dict1.copy() +merged.update(dict2) +print(f"Using update(): {merged}") + +# Method 2: ** unpacking (Python 3.5+) +merged = {**dict1, **dict2} +print(f"Using ** unpacking: {merged}") + +# Method 3: | operator (Python 3.9+) +merged = dict1 | dict2 +print(f"Using | operator: {merged}") + +# Later values overwrite earlier ones +merged = {**dict1, **dict3} # 'b' will be 99 +print(f"With overlap: {merged}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# 1. Word frequency counter +text = "the quick brown fox jumps over the lazy dog the fox" +words = text.split() +frequency = {} +for word in words: + frequency[word] = frequency.get(word, 0) + 1 +print(f"Word frequency: {frequency}") + +# 2. Grouping items +students = [ + {"name": "Alice", "grade": "A"}, + {"name": "Bob", "grade": "B"}, + {"name": "Charlie", "grade": "A"}, + {"name": "David", "grade": "B"}, + {"name": "Eve", "grade": "A"} +] + +by_grade = {} +for student in students: + grade = student["grade"] + if grade not in by_grade: + by_grade[grade] = [] + by_grade[grade].append(student["name"]) +print(f"\nStudents by grade: {by_grade}") + +# 3. Using dict as simple cache/memo +cache = {} + +def fibonacci(n): + if n in cache: + return cache[n] + if n <= 1: + return n + result = fibonacci(n-1) + fibonacci(n-2) + cache[n] = result + return result + +print(f"\nFibonacci(10): {fibonacci(10)}") +print(f"Cache: {cache}") + +# 4. Configuration settings +default_config = {"theme": "dark", "font": "Arial", "size": 12} +user_config = {"theme": "light", "size": 14} +final_config = {**default_config, **user_config} +print(f"\nFinal config: {final_config}") diff --git a/05_functions/01_function_basics.py b/05_functions/01_function_basics.py new file mode 100644 index 0000000..3650cd4 --- /dev/null +++ b/05_functions/01_function_basics.py @@ -0,0 +1,337 @@ +""" +================================================================================ +File: 01_function_basics.py +Topic: Python Functions - Basic Concepts +================================================================================ + +This file demonstrates the fundamentals of functions in Python. Functions +are reusable blocks of code that perform specific tasks, making your code +more organized, readable, and maintainable. + +Key Concepts: +- Defining functions with def +- Calling functions +- Function documentation (docstrings) +- Variable scope (local vs global) +- Basic parameters + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Defining and Calling Functions +# ----------------------------------------------------------------------------- +# Use 'def' keyword to define a function + +print("--- Defining and Calling Functions ---") + +# Simple function with no parameters +def greet(): + """Print a greeting message.""" + print("Hello, World!") + +# Call the function +greet() + +# Function with a parameter +def greet_person(name): + """Greet a specific person.""" + print(f"Hello, {name}!") + +greet_person("Baraa") +greet_person("Sara") + +# ----------------------------------------------------------------------------- +# 2. Function with Multiple Parameters +# ----------------------------------------------------------------------------- + +print("\n--- Multiple Parameters ---") + +def introduce(name, age, city): + """Introduce a person with their details.""" + print(f"My name is {name}, I am {age} years old, and I live in {city}.") + +introduce("Ali", 25, "Cairo") + +# ----------------------------------------------------------------------------- +# 3. Return Values +# ----------------------------------------------------------------------------- +# Functions can return values using 'return' + +print("\n--- Return Values ---") + +def add(a, b): + """Add two numbers and return the result.""" + return a + b + +result = add(5, 3) +print(f"5 + 3 = {result}") + +# Using returned value in expressions +total = add(10, 20) + add(5, 5) +print(f"(10+20) + (5+5) = {total}") + +# Function without explicit return returns None +def print_message(msg): + print(msg) + +result = print_message("Hello") +print(f"Function without return: {result}") # None + +# Early return +def get_grade(score): + """Return letter grade based on score.""" + if score >= 90: + return "A" + if score >= 80: + return "B" + if score >= 70: + return "C" + if score >= 60: + return "D" + return "F" + +print(f"\nScore 85 → Grade {get_grade(85)}") + +# ----------------------------------------------------------------------------- +# 4. Docstrings - Function Documentation +# ----------------------------------------------------------------------------- +# Use triple quotes to document what a function does + +print("\n--- Docstrings ---") + +def calculate_area(length, width): + """ + Calculate the area of a rectangle. + + Args: + length: The length of the rectangle (positive number) + width: The width of the rectangle (positive number) + + Returns: + The area of the rectangle (length * width) + + Example: + >>> calculate_area(5, 3) + 15 + """ + return length * width + +# Access docstring +print(f"Function docstring:\n{calculate_area.__doc__}") + +# Using help() +# help(calculate_area) # Uncomment to see full help + +# ----------------------------------------------------------------------------- +# 5. Variable Scope - Local vs Global +# ----------------------------------------------------------------------------- +# Variables inside functions are local by default + +print("\n--- Variable Scope ---") + +global_var = "I am global" + +def demonstrate_scope(): + """Demonstrate variable scope.""" + local_var = "I am local" + print(f" Inside function - global_var: {global_var}") + print(f" Inside function - local_var: {local_var}") + +demonstrate_scope() +print(f"Outside function - global_var: {global_var}") +# print(local_var) # This would cause an error! + +# Modifying global variables inside functions +counter = 0 + +def increment_counter(): + """Increment the global counter.""" + global counter # Declare we want to modify global variable + counter += 1 + print(f" Counter inside function: {counter}") + +print(f"\nCounter before: {counter}") +increment_counter() +increment_counter() +print(f"Counter after: {counter}") + +# Local variable shadows global +value = 100 + +def shadow_example(): + """Local variable shadows global.""" + value = 200 # This is a new local variable, not the global one + print(f" Inside function: {value}") + +print(f"\nGlobal value: {value}") +shadow_example() +print(f"Global value unchanged: {value}") + +# ----------------------------------------------------------------------------- +# 6. Multiple Return Values +# ----------------------------------------------------------------------------- +# Functions can return multiple values as a tuple + +print("\n--- Multiple Return Values ---") + +def get_min_max(numbers): + """Return both minimum and maximum of a list.""" + return min(numbers), max(numbers) + +data = [5, 2, 8, 1, 9, 3] +minimum, maximum = get_min_max(data) +print(f"List: {data}") +print(f"Min: {minimum}, Max: {maximum}") + +# Return multiple named values using dictionary +def analyze_text(text): + """Analyze text and return statistics.""" + return { + "length": len(text), + "words": len(text.split()), + "uppercase": sum(1 for c in text if c.isupper()) + } + +stats = analyze_text("Hello World! How Are You?") +print(f"\nText stats: {stats}") + +# ----------------------------------------------------------------------------- +# 7. Pass Statement - Placeholder Functions +# ----------------------------------------------------------------------------- +# Use pass to create empty function bodies + +print("\n--- Placeholder Functions ---") + +def future_feature(): + """This will be implemented later.""" + pass # Placeholder - does nothing + +def another_placeholder(): + """Placeholder with ellipsis (also valid).""" + ... # Alternative to pass + +future_feature() # Can be called, just does nothing +print("Placeholder functions work!") + +# ----------------------------------------------------------------------------- +# 8. Nested Functions +# ----------------------------------------------------------------------------- +# Functions can be defined inside other functions + +print("\n--- Nested Functions ---") + +def outer_function(message): + """Outer function that contains an inner function.""" + + def inner_function(): + """Inner function that uses outer's variable.""" + print(f" Inner says: {message}") + + print("Outer function called") + inner_function() + +outer_function("Hello from outer!") + +# Inner function not accessible outside +# inner_function() # This would cause an error! + +# Practical example: Helper function +def calculate_statistics(numbers): + """Calculate various statistics using helper functions.""" + + def mean(nums): + return sum(nums) / len(nums) + + def variance(nums): + avg = mean(nums) + return sum((x - avg) ** 2 for x in nums) / len(nums) + + return { + "mean": mean(numbers), + "variance": variance(numbers), + "std_dev": variance(numbers) ** 0.5 + } + +data = [2, 4, 4, 4, 5, 5, 7, 9] +stats = calculate_statistics(data) +print(f"\nStatistics for {data}:") +for key, value in stats.items(): + print(f" {key}: {value:.2f}") + +# ----------------------------------------------------------------------------- +# 9. Functions as Objects +# ----------------------------------------------------------------------------- +# In Python, functions are first-class objects + +print("\n--- Functions as Objects ---") + +def say_hello(name): + """Just say hello.""" + return f"Hello, {name}!" + +# Assign function to variable +greeting_func = say_hello +print(greeting_func("World")) + +# Store functions in a list +def add_one(x): return x + 1 +def double(x): return x * 2 +def square(x): return x * x + +operations = [add_one, double, square] +value = 5 + +print(f"\nApplying operations to {value}:") +for func in operations: + print(f" {func.__name__}({value}) = {func(value)}") + +# Pass function as argument +def apply_operation(func, value): + """Apply a function to a value.""" + return func(value) + +print(f"\napply_operation(double, 10) = {apply_operation(double, 10)}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Temperature converter +def celsius_to_fahrenheit(celsius): + """Convert Celsius to Fahrenheit.""" + return (celsius * 9/5) + 32 + +def fahrenheit_to_celsius(fahrenheit): + """Convert Fahrenheit to Celsius.""" + return (fahrenheit - 32) * 5/9 + +print(f"20°C = {celsius_to_fahrenheit(20):.1f}°F") +print(f"68°F = {fahrenheit_to_celsius(68):.1f}°C") + +# Password validator +def is_valid_password(password): + """ + Check if password meets requirements: + - At least 8 characters + - Contains uppercase letter + - Contains lowercase letter + - Contains digit + """ + if len(password) < 8: + return False, "Too short" + if not any(c.isupper() for c in password): + return False, "No uppercase letter" + if not any(c.islower() for c in password): + return False, "No lowercase letter" + if not any(c.isdigit() for c in password): + return False, "No digit" + return True, "Valid password" + +test_passwords = ["short", "alllowercase", "ALLUPPERCASE", "ValidPass123"] +print("\nPassword validation:") +for pwd in test_passwords: + is_valid, message = is_valid_password(pwd) + print(f" '{pwd}': {message}") diff --git a/05_functions/02_arguments.py b/05_functions/02_arguments.py new file mode 100644 index 0000000..75ece0b --- /dev/null +++ b/05_functions/02_arguments.py @@ -0,0 +1,308 @@ +""" +================================================================================ +File: 02_arguments.py +Topic: Function Arguments in Python +================================================================================ + +This file demonstrates the different ways to pass arguments to functions +in Python. Understanding these patterns is essential for writing flexible +and reusable functions. + +Key Concepts: +- Positional arguments +- Keyword arguments +- Default parameter values +- *args (variable positional arguments) +- **kwargs (variable keyword arguments) +- Argument unpacking +- Keyword-only and positional-only arguments + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Positional Arguments +# ----------------------------------------------------------------------------- +# Arguments passed in order; position matters + +print("--- Positional Arguments ---") + +def greet(first_name, last_name): + """Greet using positional arguments.""" + print(f"Hello, {first_name} {last_name}!") + +greet("John", "Doe") # Correct order +greet("Jane", "Smith") # Correct order + +# Order matters! +greet("Doe", "John") # Wrong order gives wrong result + +# ----------------------------------------------------------------------------- +# 2. Keyword Arguments +# ----------------------------------------------------------------------------- +# Arguments passed by name; order doesn't matter + +print("\n--- Keyword Arguments ---") + +def create_profile(name, age, city): + """Create a profile using keyword arguments.""" + print(f"Name: {name}, Age: {age}, City: {city}") + +# Using keyword arguments (order doesn't matter) +create_profile(name="Alice", age=25, city="NYC") +create_profile(city="Tokyo", name="Bob", age=30) + +# Mix positional and keyword (positional must come first!) +create_profile("Charlie", city="London", age=28) + +# ----------------------------------------------------------------------------- +# 3. Default Parameter Values +# ----------------------------------------------------------------------------- +# Parameters can have default values + +print("\n--- Default Parameter Values ---") + +def greet_with_title(name, title="Mr."): + """Greet with an optional title.""" + print(f"Hello, {title} {name}!") + +greet_with_title("Smith") # Uses default title +greet_with_title("Johnson", "Dr.") # Overrides default + +# Multiple defaults +def create_user(username, email, role="user", active=True): + """Create a user with defaults.""" + print(f"User: {username}, Email: {email}, Role: {role}, Active: {active}") + +create_user("john", "john@example.com") +create_user("admin", "admin@example.com", role="admin") +create_user("test", "test@example.com", active=False) + +# CAUTION: Mutable default arguments +# DON'T DO THIS: +def bad_function(items=[]): # Bad! List is shared across calls + items.append(1) + return items + +# DO THIS INSTEAD: +def good_function(items=None): + """Safe way to handle mutable defaults.""" + if items is None: + items = [] + items.append(1) + return items + +print(f"\nBad function calls: {bad_function()}, {bad_function()}, {bad_function()}") +print(f"Good function calls: {good_function()}, {good_function()}, {good_function()}") + +# ----------------------------------------------------------------------------- +# 4. *args - Variable Positional Arguments +# ----------------------------------------------------------------------------- +# Accept any number of positional arguments + +print("\n--- *args (Variable Positional Arguments) ---") + +def sum_all(*args): + """Sum all provided numbers.""" + print(f" Received args: {args} (type: {type(args).__name__})") + return sum(args) + +print(f"sum_all(1, 2): {sum_all(1, 2)}") +print(f"sum_all(1, 2, 3, 4, 5): {sum_all(1, 2, 3, 4, 5)}") +print(f"sum_all(): {sum_all()}") + +# Combining regular parameters with *args +def greet_people(greeting, *names): + """Greet multiple people with the same greeting.""" + for name in names: + print(f" {greeting}, {name}!") + +print("\nGreeting people:") +greet_people("Hello", "Alice", "Bob", "Charlie") + +# ----------------------------------------------------------------------------- +# 5. **kwargs - Variable Keyword Arguments +# ----------------------------------------------------------------------------- +# Accept any number of keyword arguments + +print("\n--- **kwargs (Variable Keyword Arguments) ---") + +def print_info(**kwargs): + """Print all keyword arguments.""" + print(f" Received kwargs: {kwargs} (type: {type(kwargs).__name__})") + for key, value in kwargs.items(): + print(f" {key}: {value}") + +print_info(name="Baraa", age=25, city="Gaza") +print_info(language="Python", level="Expert") + +# Combining all parameter types +def complete_example(required, *args, default="value", **kwargs): + """Demonstrate all parameter types.""" + print(f" Required: {required}") + print(f" *args: {args}") + print(f" Default: {default}") + print(f" **kwargs: {kwargs}") + +print("\nComplete example:") +complete_example("must_have", 1, 2, 3, default="custom", extra="data", more="stuff") + +# ----------------------------------------------------------------------------- +# 6. Argument Unpacking +# ----------------------------------------------------------------------------- +# Use * and ** to unpack sequences and dictionaries into arguments + +print("\n--- Argument Unpacking ---") + +def introduce(name, age, city): + """Introduce a person.""" + print(f"I'm {name}, {age} years old, from {city}") + +# Unpack list/tuple with * +person_list = ["Alice", 30, "Paris"] +introduce(*person_list) # Same as: introduce("Alice", 30, "Paris") + +# Unpack dictionary with ** +person_dict = {"name": "Bob", "age": 25, "city": "Berlin"} +introduce(**person_dict) # Same as: introduce(name="Bob", age=25, city="Berlin") + +# Combine unpacking +def multiply(a, b, c): + return a * b * c + +nums = [2, 3] +print(f"\nMultiply with unpacking: {multiply(*nums, 4)}") + +# ----------------------------------------------------------------------------- +# 7. Keyword-Only Arguments (Python 3+) +# ----------------------------------------------------------------------------- +# Arguments after * must be passed as keywords + +print("\n--- Keyword-Only Arguments ---") + +def format_name(first, last, *, upper=False, reverse=False): + """ + Format a name. 'upper' and 'reverse' are keyword-only. + """ + name = f"{first} {last}" + if reverse: + name = f"{last}, {first}" + if upper: + name = name.upper() + return name + +# These work: +print(format_name("John", "Doe")) +print(format_name("John", "Doe", upper=True)) +print(format_name("John", "Doe", reverse=True, upper=True)) + +# This would fail: +# format_name("John", "Doe", True) # Error! upper must be keyword + +# Using bare * for keyword-only +def connect(*, host, port, timeout=30): + """All parameters are keyword-only.""" + print(f" Connecting to {host}:{port} (timeout: {timeout}s)") + +connect(host="localhost", port=8080) +connect(host="db.example.com", port=5432, timeout=60) + +# ----------------------------------------------------------------------------- +# 8. Positional-Only Arguments (Python 3.8+) +# ----------------------------------------------------------------------------- +# Arguments before / must be passed positionally + +print("\n--- Positional-Only Arguments ---") + +def divide(x, y, /): + """x and y must be passed positionally.""" + return x / y + +print(f"divide(10, 2): {divide(10, 2)}") +# divide(x=10, y=2) # Error! x and y are positional-only + +# Combining positional-only, regular, and keyword-only +def complex_function(pos_only1, pos_only2, /, regular1, regular2, *, kw_only1, kw_only2="default"): + """ + pos_only1, pos_only2: positional-only (before /) + regular1, regular2: can be either + kw_only1, kw_only2: keyword-only (after *) + """ + print(f" pos_only: {pos_only1}, {pos_only2}") + print(f" regular: {regular1}, {regular2}") + print(f" kw_only: {kw_only1}, {kw_only2}") + +print("\nComplex function:") +complex_function(1, 2, 3, regular2=4, kw_only1="required") + +# ----------------------------------------------------------------------------- +# 9. Type Hints for Arguments (Preview) +# ----------------------------------------------------------------------------- +# Adding type information (doesn't enforce, just hints) + +print("\n--- Type Hints ---") + +def calculate_total(price: float, quantity: int, tax_rate: float = 0.1) -> float: + """ + Calculate total price with tax. + + Args: + price: Unit price (float) + quantity: Number of items (int) + tax_rate: Tax rate as decimal (default 0.1 = 10%) + + Returns: + Total price including tax + """ + subtotal = price * quantity + return subtotal * (1 + tax_rate) + +total = calculate_total(19.99, 3) +print(f"Total: ${total:.2f}") + +# Type hints with complex types +from typing import List, Optional, Dict + +def process_scores(scores: List[int], name: Optional[str] = None) -> Dict[str, float]: + """Process a list of scores and return statistics.""" + return { + "name": name or "Unknown", + "average": sum(scores) / len(scores), + "highest": max(scores), + "lowest": min(scores) + } + +result = process_scores([85, 90, 78, 92], "Alice") +print(f"Score stats: {result}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Flexible logging function +def log(message, *tags, level="INFO", **metadata): + """Log a message with optional tags and metadata.""" + tag_str = " ".join(f"[{tag}]" for tag in tags) + meta_str = " | ".join(f"{k}={v}" for k, v in metadata.items()) + + output = f"[{level}] {tag_str} {message}" + if meta_str: + output += f" ({meta_str})" + print(output) + +log("Server started", "server", "startup") +log("User logged in", "auth", level="DEBUG", user_id=123, ip="192.168.1.1") + +# Builder pattern with kwargs +def create_html_element(tag, content="", **attributes): + """Create an HTML element string.""" + attrs = " ".join(f'{k}="{v}"' for k, v in attributes.items()) + if attrs: + attrs = " " + attrs + return f"<{tag}{attrs}>{content}" + +print("\n" + create_html_element("p", "Hello World")) +print(create_html_element("a", "Click me", href="https://example.com", target="_blank")) +print(create_html_element("input", type="text", placeholder="Enter name", id="name_input")) diff --git a/05_functions/03_return_values.py b/05_functions/03_return_values.py new file mode 100644 index 0000000..433a166 --- /dev/null +++ b/05_functions/03_return_values.py @@ -0,0 +1,399 @@ +""" +================================================================================ +File: 03_return_values.py +Topic: Function Return Values in Python +================================================================================ + +This file demonstrates various ways functions can return values in Python, +including returning multiple values, early returns, and more advanced patterns. + +Key Concepts: +- Returning single and multiple values +- Returning None (explicit and implicit) +- Early returns for cleaner code +- Returning functions (closures) +- Return type annotations + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic Return Values +# ----------------------------------------------------------------------------- +# Use 'return' to send a value back to the caller + +print("--- Basic Return Values ---") + +def square(n): + """Return the square of a number.""" + return n * n + +def add(a, b): + """Return the sum of two numbers.""" + return a + b + +result = square(5) +print(f"square(5) = {result}") + +# Using return value directly +print(f"add(3, 4) = {add(3, 4)}") + +# Chaining function calls +print(f"square(add(2, 3)) = {square(add(2, 3))}") + +# ----------------------------------------------------------------------------- +# 2. Returning None +# ----------------------------------------------------------------------------- +# Functions without explicit return (or with 'return' alone) return None + +print("\n--- Returning None ---") + +# Implicit None return +def greet(name): + """Print a greeting - no explicit return.""" + print(f" Hello, {name}!") + +result = greet("Alice") +print(f"Return value: {result}") + +# Explicit None return +def try_parse_int(value): + """Try to parse string as int, return None on failure.""" + try: + return int(value) + except ValueError: + return None # Explicit None + +print(f"\ntry_parse_int('42'): {try_parse_int('42')}") +print(f"try_parse_int('abc'): {try_parse_int('abc')}") + +# Using None as sentinel value +def find_item(items, target): + """Find item in list, return index or None.""" + for i, item in enumerate(items): + if item == target: + return i + return None # Not found + +fruits = ["apple", "banana", "cherry"] +index = find_item(fruits, "banana") +if index is not None: + print(f"\nFound 'banana' at index {index}") + +# ----------------------------------------------------------------------------- +# 3. Returning Multiple Values +# ----------------------------------------------------------------------------- +# Python functions can return multiple values as tuples + +print("\n--- Returning Multiple Values ---") + +# Return as tuple (implicit) +def get_name_parts(full_name): + """Split full name into first and last.""" + parts = full_name.split() + return parts[0], parts[-1] # Returns tuple + +first, last = get_name_parts("John William Doe") +print(f"First: {first}, Last: {last}") + +# Return as tuple (explicit) +def min_max(numbers): + """Return minimum and maximum as explicit tuple.""" + return (min(numbers), max(numbers)) + +result = min_max([3, 1, 4, 1, 5, 9]) +print(f"min_max result: {result}") +print(f"Type: {type(result)}") + +# Return as dictionary (named values) +def analyze_string(text): + """Analyze string and return statistics as dict.""" + return { + "length": len(text), + "words": len(text.split()), + "chars_no_space": len(text.replace(" ", "")), + "upper_count": sum(1 for c in text if c.isupper()), + "lower_count": sum(1 for c in text if c.islower()) + } + +stats = analyze_string("Hello World") +print(f"\nString analysis: {stats}") + +# Return as named tuple (best of both worlds) +from collections import namedtuple + +Point = namedtuple("Point", ["x", "y", "z"]) + +def create_point(x, y, z): + """Create a 3D point.""" + return Point(x, y, z) + +p = create_point(10, 20, 30) +print(f"\nNamed tuple: {p}") +print(f"Access by name: x={p.x}, y={p.y}, z={p.z}") + +# ----------------------------------------------------------------------------- +# 4. Early Returns (Guard Clauses) +# ----------------------------------------------------------------------------- +# Return early to handle edge cases and improve readability + +print("\n--- Early Returns ---") + +# Without early returns (nested, harder to read) +def get_grade_nested(score): + if score >= 0 and score <= 100: + if score >= 90: + return "A" + else: + if score >= 80: + return "B" + else: + if score >= 70: + return "C" + else: + if score >= 60: + return "D" + else: + return "F" + else: + return "Invalid" + +# With early returns (flat, easier to read) +def get_grade_early(score): + """Get grade using early returns (guard clauses).""" + if score < 0 or score > 100: + return "Invalid" + if score >= 90: + return "A" + if score >= 80: + return "B" + if score >= 70: + return "C" + if score >= 60: + return "D" + return "F" + +print(f"get_grade_early(85) = {get_grade_early(85)}") +print(f"get_grade_early(150) = {get_grade_early(150)}") + +# Practical example: Input validation with early returns +def process_user_data(data): + """Process user data with validation.""" + # Guard clauses + if data is None: + return {"error": "No data provided"} + + if not isinstance(data, dict): + return {"error": "Data must be a dictionary"} + + if "name" not in data: + return {"error": "Name is required"} + + if "age" not in data: + return {"error": "Age is required"} + + if data["age"] < 0: + return {"error": "Age must be positive"} + + # Main logic (only runs if all validations pass) + return { + "success": True, + "message": f"Processed user {data['name']}, age {data['age']}" + } + +print("\nData validation examples:") +print(f" None: {process_user_data(None)}") +print(f" Empty dict: {process_user_data({})}") +print(f" Valid: {process_user_data({'name': 'Alice', 'age': 25})}") + +# ----------------------------------------------------------------------------- +# 5. Returning Functions (Closures) +# ----------------------------------------------------------------------------- +# Functions can return other functions + +print("\n--- Returning Functions ---") + +def make_multiplier(factor): + """Return a function that multiplies by factor.""" + def multiplier(x): + return x * factor + return multiplier + +double = make_multiplier(2) +triple = make_multiplier(3) + +print(f"double(5) = {double(5)}") +print(f"triple(5) = {triple(5)}") + +# Practical: Create custom validators +def make_range_validator(min_val, max_val): + """Create a validator for a specific range.""" + def validate(value): + return min_val <= value <= max_val + return validate + +age_validator = make_range_validator(0, 120) +percentage_validator = make_range_validator(0, 100) + +print(f"\nage_validator(25) = {age_validator(25)}") +print(f"age_validator(150) = {age_validator(150)}") +print(f"percentage_validator(85.5) = {percentage_validator(85.5)}") + +# ----------------------------------------------------------------------------- +# 6. Return Type Annotations +# ----------------------------------------------------------------------------- +# Add type hints for return values + +print("\n--- Return Type Annotations ---") + +from typing import List, Optional, Tuple, Dict, Union + +def get_greeting(name: str) -> str: + """Return a greeting string.""" + return f"Hello, {name}!" + +def divide(a: float, b: float) -> Optional[float]: + """Divide a by b, return None if b is zero.""" + if b == 0: + return None + return a / b + +def get_user_info(user_id: int) -> Dict[str, Union[str, int]]: + """Return user info as dictionary.""" + return {"id": user_id, "name": "Test User", "age": 25} + +def process_numbers(nums: List[int]) -> Tuple[int, int, float]: + """Return min, max, and average.""" + return min(nums), max(nums), sum(nums) / len(nums) + +print(f"get_greeting('World'): {get_greeting('World')}") +print(f"divide(10, 3): {divide(10, 3)}") +print(f"divide(10, 0): {divide(10, 0)}") +print(f"get_user_info(1): {get_user_info(1)}") +print(f"process_numbers([1,2,3,4,5]): {process_numbers([1,2,3,4,5])}") + +# ----------------------------------------------------------------------------- +# 7. Generator Functions (yield vs return) +# ----------------------------------------------------------------------------- +# Functions that yield values instead of returning + +print("\n--- Generators (yield) ---") + +# Regular function - returns all at once +def get_squares_list(n): + """Return list of squares.""" + return [i ** 2 for i in range(n)] + +# Generator function - yields one at a time +def get_squares_generator(n): + """Generate squares one at a time.""" + for i in range(n): + yield i ** 2 + +# Generator is memory-efficient for large sequences +squares_list = get_squares_list(5) +squares_gen = get_squares_generator(5) + +print(f"List: {squares_list}") +print(f"Generator: {squares_gen}") +print(f"From generator: {list(squares_gen)}") + +# Practical: Generator for large files (conceptual) +def read_large_file_lines(filename): + """Yield lines one at a time (memory efficient).""" + # In real code: open(filename) and yield line by line + # This is just a demonstration + for i in range(5): + yield f"Line {i + 1} from {filename}" + +print("\nGenerator example:") +for line in read_large_file_lines("data.txt"): + print(f" {line}") + +# ----------------------------------------------------------------------------- +# 8. Conditional Returns +# ----------------------------------------------------------------------------- +# Return different types or values based on conditions + +print("\n--- Conditional Returns ---") + +# Ternary return +def absolute_value(n): + """Return absolute value using ternary.""" + return n if n >= 0 else -n + +print(f"absolute_value(-5) = {absolute_value(-5)}") +print(f"absolute_value(5) = {absolute_value(5)}") + +# Short-circuit return with 'or' +def get_username(user): + """Get username or default.""" + return user.get("username") or "anonymous" + +print(f"get_username({{}}): {get_username({})}") +print(f"get_username({{'username': 'john'}}): {get_username({'username': 'john'})}") + +# Return type based on input +def smart_divide(a, b, as_float=True): + """Return float or int based on parameter.""" + if b == 0: + return None + return a / b if as_float else a // b + +print(f"\nsmart_divide(10, 3, True): {smart_divide(10, 3, True)}") +print(f"smart_divide(10, 3, False): {smart_divide(10, 3, False)}") + +# ----------------------------------------------------------------------------- +# 9. Return vs Print +# ----------------------------------------------------------------------------- +# Important distinction for beginners + +print("\n--- Return vs Print ---") + +def print_double(n): + """Print double (returns None).""" + print(n * 2) + +def return_double(n): + """Return double.""" + return n * 2 + +# print_double can't be used in expressions +result1 = print_double(5) # Prints 10 +print(f"print_double(5) returns: {result1}") + +# return_double can be used in expressions +result2 = return_double(5) # Returns 10 +print(f"return_double(5) returns: {result2}") +print(f"return_double(5) + 1 = {return_double(5) + 1}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# API-style response +def api_response(success, data=None, error=None): + """Create standardized API response.""" + return { + "success": success, + "data": data, + "error": error + } + +print("API responses:") +print(f" Success: {api_response(True, {'user': 'john', 'id': 1})}") +print(f" Error: {api_response(False, error='User not found')}") + +# Chain-able operations +def chain_operation(value, operations): + """Apply a chain of operations to a value.""" + result = value + for op in operations: + result = op(result) + return result + +ops = [lambda x: x + 10, lambda x: x * 2, lambda x: x - 5] +print(f"\nChained operations on 5: {chain_operation(5, ops)}") +# (5 + 10) * 2 - 5 = 25 diff --git a/05_functions/04_lambda_functions.py b/05_functions/04_lambda_functions.py new file mode 100644 index 0000000..27185f7 --- /dev/null +++ b/05_functions/04_lambda_functions.py @@ -0,0 +1,340 @@ +""" +================================================================================ +File: 04_lambda_functions.py +Topic: Lambda Functions in Python +================================================================================ + +This file demonstrates lambda functions (anonymous functions) in Python. +Lambda functions are small, one-line functions that can be defined inline +without using the 'def' keyword. + +Key Concepts: +- Lambda syntax +- When to use lambda functions +- Lambda with built-in functions (map, filter, sorted) +- Lambda vs regular functions +- Common use cases and patterns + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic Lambda Syntax +# ----------------------------------------------------------------------------- +# lambda arguments: expression + +print("--- Basic Lambda Syntax ---") + +# Regular function +def add_regular(a, b): + return a + b + +# Equivalent lambda function +add_lambda = lambda a, b: a + b + +print(f"Regular function: {add_regular(3, 5)}") +print(f"Lambda function: {add_lambda(3, 5)}") + +# More examples +square = lambda x: x ** 2 +is_even = lambda x: x % 2 == 0 +greet = lambda name: f"Hello, {name}!" + +print(f"\nsquare(4) = {square(4)}") +print(f"is_even(7) = {is_even(7)}") +print(f"greet('Alice') = {greet('Alice')}") + +# Lambda with no arguments +get_pi = lambda: 3.14159 +print(f"get_pi() = {get_pi()}") + +# ----------------------------------------------------------------------------- +# 2. Lambda with Multiple Arguments +# ----------------------------------------------------------------------------- + +print("\n--- Multiple Arguments ---") + +# Two arguments +multiply = lambda x, y: x * y +print(f"multiply(4, 5) = {multiply(4, 5)}") + +# Three arguments +volume = lambda l, w, h: l * w * h +print(f"volume(2, 3, 4) = {volume(2, 3, 4)}") + +# With default arguments +power = lambda base, exp=2: base ** exp +print(f"power(3) = {power(3)}") # 3^2 = 9 +print(f"power(2, 3) = {power(2, 3)}") # 2^3 = 8 + +# ----------------------------------------------------------------------------- +# 3. Lambda with Conditional Expression +# ----------------------------------------------------------------------------- +# Using ternary operator in lambda + +print("\n--- Conditional Lambda ---") + +# Simple conditional +get_sign = lambda x: "positive" if x > 0 else ("negative" if x < 0 else "zero") + +print(f"get_sign(5) = {get_sign(5)}") +print(f"get_sign(-3) = {get_sign(-3)}") +print(f"get_sign(0) = {get_sign(0)}") + +# Max of two numbers +max_of_two = lambda a, b: a if a > b else b +print(f"\nmax_of_two(10, 20) = {max_of_two(10, 20)}") + +# Absolute value +absolute = lambda x: x if x >= 0 else -x +print(f"absolute(-7) = {absolute(-7)}") + +# ----------------------------------------------------------------------------- +# 4. Lambda with map() +# ----------------------------------------------------------------------------- +# Apply function to each element of iterable + +print("\n--- Lambda with map() ---") + +numbers = [1, 2, 3, 4, 5] + +# Square each number +squares = list(map(lambda x: x ** 2, numbers)) +print(f"Original: {numbers}") +print(f"Squared: {squares}") + +# Convert to strings +strings = list(map(lambda x: str(x), numbers)) +print(f"As strings: {strings}") + +# Multiple iterables with map +list1 = [1, 2, 3] +list2 = [10, 20, 30] +sums = list(map(lambda x, y: x + y, list1, list2)) +print(f"\n{list1} + {list2} = {sums}") + +# Processing strings +names = ["alice", "bob", "charlie"] +capitalized = list(map(lambda name: name.capitalize(), names)) +print(f"Capitalized: {capitalized}") + +# ----------------------------------------------------------------------------- +# 5. Lambda with filter() +# ----------------------------------------------------------------------------- +# Filter elements based on condition + +print("\n--- Lambda with filter() ---") + +numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +# Filter even numbers +evens = list(filter(lambda x: x % 2 == 0, numbers)) +print(f"Original: {numbers}") +print(f"Even only: {evens}") + +# Filter numbers greater than 5 +greater_than_5 = list(filter(lambda x: x > 5, numbers)) +print(f"Greater than 5: {greater_than_5}") + +# Filter non-empty strings +strings = ["hello", "", "world", "", "python", ""] +non_empty = list(filter(lambda s: s, strings)) +print(f"\nNon-empty strings: {non_empty}") + +# Complex filtering +people = [ + {"name": "Alice", "age": 25}, + {"name": "Bob", "age": 17}, + {"name": "Charlie", "age": 30}, + {"name": "David", "age": 15} +] + +adults = list(filter(lambda p: p["age"] >= 18, people)) +print(f"Adults: {[p['name'] for p in adults]}") + +# ----------------------------------------------------------------------------- +# 6. Lambda with sorted() +# ----------------------------------------------------------------------------- +# Custom sorting with key function + +print("\n--- Lambda with sorted() ---") + +# Sort by absolute value +numbers = [-5, 2, -1, 7, -3, 4] +by_absolute = sorted(numbers, key=lambda x: abs(x)) +print(f"Original: {numbers}") +print(f"Sorted by absolute value: {by_absolute}") + +# Sort strings by length +words = ["python", "is", "a", "programming", "language"] +by_length = sorted(words, key=lambda w: len(w)) +print(f"\nSorted by length: {by_length}") + +# Sort objects by attribute +students = [ + {"name": "Alice", "grade": 85}, + {"name": "Bob", "grade": 92}, + {"name": "Charlie", "grade": 78} +] + +by_grade = sorted(students, key=lambda s: s["grade"], reverse=True) +print(f"\nBy grade (highest first):") +for s in by_grade: + print(f" {s['name']}: {s['grade']}") + +# Sort by multiple criteria +items = [("apple", 3), ("banana", 1), ("cherry", 2), ("apple", 1)] +# Sort by name, then by number +sorted_items = sorted(items, key=lambda x: (x[0], x[1])) +print(f"\nSorted by name, then number: {sorted_items}") + +# ----------------------------------------------------------------------------- +# 7. Lambda with reduce() +# ----------------------------------------------------------------------------- +# Reduce iterable to single value + +print("\n--- Lambda with reduce() ---") + +from functools import reduce + +numbers = [1, 2, 3, 4, 5] + +# Sum all numbers +total = reduce(lambda acc, x: acc + x, numbers) +print(f"Sum of {numbers} = {total}") + +# Product of all numbers +product = reduce(lambda acc, x: acc * x, numbers) +print(f"Product of {numbers} = {product}") + +# Find maximum +maximum = reduce(lambda a, b: a if a > b else b, numbers) +print(f"Maximum of {numbers} = {maximum}") + +# Concatenate strings +words = ["Hello", " ", "World", "!"] +sentence = reduce(lambda a, b: a + b, words) +print(f"Concatenated: '{sentence}'") + +# With initial value +numbers = [1, 2, 3] +sum_with_initial = reduce(lambda acc, x: acc + x, numbers, 100) +print(f"\nSum with initial 100: {sum_with_initial}") + +# ----------------------------------------------------------------------------- +# 8. Lambda in Data Processing +# ----------------------------------------------------------------------------- + +print("\n--- Data Processing Example ---") + +# Sample data +transactions = [ + {"id": 1, "type": "credit", "amount": 100}, + {"id": 2, "type": "debit", "amount": 50}, + {"id": 3, "type": "credit", "amount": 200}, + {"id": 4, "type": "debit", "amount": 75}, + {"id": 5, "type": "credit", "amount": 150} +] + +# Filter credits only +credits = list(filter(lambda t: t["type"] == "credit", transactions)) +print(f"Credit transactions: {len(credits)}") + +# Extract amounts from credits +credit_amounts = list(map(lambda t: t["amount"], credits)) +print(f"Credit amounts: {credit_amounts}") + +# Total credits +total_credits = reduce(lambda acc, t: acc + t["amount"], credits, 0) +print(f"Total credits: ${total_credits}") + +# Combined: total debits in one expression +total_debits = reduce( + lambda acc, x: acc + x, + map( + lambda t: t["amount"], + filter(lambda t: t["type"] == "debit", transactions) + ), + 0 +) +print(f"Total debits: ${total_debits}") + +# ----------------------------------------------------------------------------- +# 9. Lambda vs Regular Functions +# ----------------------------------------------------------------------------- + +print("\n--- Lambda vs Regular Functions ---") + +# Lambda: one expression, implicit return +# Regular: multiple statements, explicit return + +# When to use LAMBDA: +# - Simple, one-line operations +# - As arguments to higher-order functions +# - When function won't be reused + +# When to use REGULAR FUNCTIONS: +# - Multiple expressions/statements needed +# - Need docstrings +# - Function will be reused or tested +# - Complex logic + +# Example: Complex logic needs regular function +def process_value(x): + """Process value with multiple steps.""" + # Step 1: Validate + if x < 0: + return None + + # Step 2: Transform + result = x ** 2 + + # Step 3: Apply ceiling + if result > 100: + result = 100 + + return result + +# This CAN'T be easily done with lambda +# lambda x: (None if x < 0 else min(x ** 2, 100)) # Gets messy + +print(f"process_value(-5) = {process_value(-5)}") +print(f"process_value(5) = {process_value(5)}") +print(f"process_value(15) = {process_value(15)}") + +# ----------------------------------------------------------------------------- +# 10. Common Lambda Patterns +# ----------------------------------------------------------------------------- + +print("\n--- Common Lambda Patterns ---") + +# 1. Default value getter +get_value = lambda d, key, default=None: d.get(key, default) +data = {"name": "John", "age": 30} +print(f"get_value: {get_value(data, 'name')}, {get_value(data, 'email', 'N/A')}") + +# 2. Compose functions +compose = lambda f, g: lambda x: f(g(x)) +add_one = lambda x: x + 1 +double = lambda x: x * 2 +add_then_double = compose(double, add_one) +print(f"add_then_double(5) = {add_then_double(5)}") # (5+1)*2 = 12 + +# 3. Partial application simulation +multiply_by = lambda n: lambda x: x * n +times_three = multiply_by(3) +print(f"times_three(7) = {times_three(7)}") + +# 4. Key extractors for sorting +by_key = lambda key: lambda x: x[key] +products = [ + {"name": "Apple", "price": 1.50}, + {"name": "Banana", "price": 0.75}, + {"name": "Cherry", "price": 2.00} +] +sorted_by_price = sorted(products, key=by_key("price")) +print(f"\nSorted by price: {[p['name'] for p in sorted_by_price]}") + +# 5. Immediate invocation (IIFE-style) +result = (lambda x, y: x ** y)(2, 10) +print(f"\nImmediately invoked: 2^10 = {result}") diff --git a/06_modules_packages/01_imports.py b/06_modules_packages/01_imports.py new file mode 100644 index 0000000..544e542 --- /dev/null +++ b/06_modules_packages/01_imports.py @@ -0,0 +1,273 @@ +""" +================================================================================ +File: 01_imports.py +Topic: Importing Modules and Packages in Python +================================================================================ + +This file demonstrates how to import and use modules in Python. Modules are +files containing Python code that can be reused across different programs. +Python's extensive standard library and third-party packages make imports +essential for productive development. + +Key Concepts: +- import statement +- from ... import ... +- Aliasing with 'as' +- Built-in and standard library modules +- Relative imports + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic Import Statement +# ----------------------------------------------------------------------------- +# Import entire module + +print("--- Basic Import ---") + +import math + +# Access functions using module.function +print(f"math.pi = {math.pi}") +print(f"math.sqrt(16) = {math.sqrt(16)}") +print(f"math.ceil(3.2) = {math.ceil(3.2)}") +print(f"math.floor(3.8) = {math.floor(3.8)}") + +# ----------------------------------------------------------------------------- +# 2. Import Specific Items +# ----------------------------------------------------------------------------- +# Import only what you need + +print("\n--- From Import ---") + +from math import pi, sqrt, pow + +# Use directly without module prefix +print(f"pi = {pi}") +print(f"sqrt(25) = {sqrt(25)}") +print(f"pow(2, 8) = {pow(2, 8)}") + +# Import multiple items +from datetime import date, time, datetime + +today = date.today() +print(f"\nToday's date: {today}") + +now = datetime.now() +print(f"Current datetime: {now}") + +# ----------------------------------------------------------------------------- +# 3. Import with Alias +# ----------------------------------------------------------------------------- +# Rename modules or items for convenience + +print("\n--- Import with Alias ---") + +# Module alias +import random as rnd + +print(f"Random number: {rnd.randint(1, 100)}") +print(f"Random choice: {rnd.choice(['apple', 'banana', 'cherry'])}") + +# Common conventions +import collections as col +import json as json # Usually kept as is +import os as os # Usually kept as is + +# Item alias +from math import factorial as fact +print(f"\nfact(5) = {fact(5)}") + +# ----------------------------------------------------------------------------- +# 4. Import All (Use Sparingly) +# ----------------------------------------------------------------------------- +# Import everything from a module + +print("\n--- Import All (Not Recommended) ---") + +# from math import * +# This imports everything, but: +# - Can cause naming conflicts +# - Makes it unclear where functions come from +# - Only use in interactive sessions if at all + +# Better to be explicit about what you import + +# ----------------------------------------------------------------------------- +# 5. Useful Standard Library Modules +# ----------------------------------------------------------------------------- + +print("\n--- Standard Library Examples ---") + +# os - Operating system interface +import os +print(f"Current directory: {os.getcwd()}") +print(f"Directory separator: {os.sep}") + +# sys - System-specific parameters +import sys +print(f"\nPython version: {sys.version_info.major}.{sys.version_info.minor}") + +# collections - Specialized containers +from collections import Counter, defaultdict + +word_counts = Counter("mississippi") +print(f"\nCharacter counts: {dict(word_counts)}") + +# Default dictionary +dd = defaultdict(list) +dd["fruits"].append("apple") +dd["fruits"].append("banana") +dd["vegetables"].append("carrot") +print(f"defaultdict: {dict(dd)}") + +# json - JSON encoding/decoding +import json + +data = {"name": "Alice", "age": 30} +json_string = json.dumps(data, indent=2) +print(f"\nJSON string:\n{json_string}") + +# re - Regular expressions +import re + +text = "Contact: john@email.com or jane@company.org" +emails = re.findall(r'\b[\w.-]+@[\w.-]+\.\w+\b', text) +print(f"Found emails: {emails}") + +# itertools - Iteration utilities +from itertools import combinations, permutations + +items = ['A', 'B', 'C'] +print(f"\nCombinations of 2: {list(combinations(items, 2))}") +print(f"Permutations of 2: {list(permutations(items, 2))}") + +# functools - Higher-order functions +from functools import lru_cache + +@lru_cache(maxsize=100) +def fibonacci(n): + """Cached fibonacci for performance.""" + if n < 2: + return n + return fibonacci(n-1) + fibonacci(n-2) + +print(f"\nFibonacci(30): {fibonacci(30)}") +print(f"Cache info: {fibonacci.cache_info()}") + +# ----------------------------------------------------------------------------- +# 6. Checking Module Attributes +# ----------------------------------------------------------------------------- + +print("\n--- Module Attributes ---") + +import math + +# List all attributes/functions in a module +print("Math module functions (first 10):") +for name in dir(math)[:10]: + print(f" {name}") + +# Module docstring +print(f"\nMath module doc: {math.__doc__[:50]}...") + +# Module file location +print(f"Math module file: {math.__file__}") + +# ----------------------------------------------------------------------------- +# 7. Conditional Imports +# ----------------------------------------------------------------------------- + +print("\n--- Conditional Imports ---") + +# Try to import, use fallback if not available +try: + import numpy as np + HAS_NUMPY = True + print("NumPy is available") +except ImportError: + HAS_NUMPY = False + print("NumPy is NOT available") + +# Another pattern +import sys +if sys.version_info >= (3, 9): + from typing import Annotated # Python 3.9+ +else: + print("Annotated not available (Python < 3.9)") + +# ----------------------------------------------------------------------------- +# 8. Module Search Path +# ----------------------------------------------------------------------------- + +print("\n--- Module Search Path ---") + +import sys + +print("Python searches for modules in:") +for i, path in enumerate(sys.path[:5]): # First 5 paths + print(f" {i+1}. {path}") +print(f" ... and {len(sys.path) - 5} more locations") + +# Adding custom path (temporarily) +# sys.path.append('/my/custom/modules') + +# ----------------------------------------------------------------------------- +# 9. Import Patterns in Projects +# ----------------------------------------------------------------------------- + +print("\n--- Import Best Practices ---") + +# Standard order for imports: +# 1. Standard library imports +# 2. Third-party imports +# 3. Local/project imports + +# Example of well-organized imports: +""" +# Standard library +import os +import sys +from datetime import datetime +from typing import List, Dict + +# Third-party (installed via pip) +import requests +import numpy as np +import pandas as pd + +# Local/project modules +from myproject.utils import helper +from myproject.models import User +""" + +print("Import order: Standard -> Third-party -> Local") +print("Group imports with blank lines between groups") + +# ----------------------------------------------------------------------------- +# 10. Practical Example: Building a Utility +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example ---") + +# Using multiple modules together +import os +import json +from datetime import datetime +from pathlib import Path + +def get_system_info(): + """Gather system information using various modules.""" + return { + "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + "platform": sys.platform, + "cwd": os.getcwd(), + "home_dir": str(Path.home()), + "timestamp": datetime.now().isoformat(), + "path_separator": os.sep + } + +info = get_system_info() +print("System Info:") +print(json.dumps(info, indent=2)) diff --git a/06_modules_packages/02_custom_modules.py b/06_modules_packages/02_custom_modules.py new file mode 100644 index 0000000..886618d --- /dev/null +++ b/06_modules_packages/02_custom_modules.py @@ -0,0 +1,443 @@ +""" +================================================================================ +File: 02_custom_modules.py +Topic: Creating and Using Custom Modules +================================================================================ + +This file demonstrates how to create your own modules and packages in Python. +Custom modules help organize code, promote reusability, and maintain clean +project structures. + +Key Concepts: +- Creating module files +- The __name__ variable +- __init__.py for packages +- Relative imports +- Module documentation +- Publishing modules + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. What is a Module? +# ----------------------------------------------------------------------------- +# A module is simply a .py file containing Python code + +print("--- What is a Module? ---") + +# Any Python file is a module! +# If you have: my_utils.py +# You can: import my_utils + +# Example module content (imagine this is saved as 'my_math.py'): +""" +# my_math.py + +def add(a, b): + return a + b + +def subtract(a, b): + return a - b + +PI = 3.14159 + +class Calculator: + def multiply(self, a, b): + return a * b +""" + +print("A module is just a Python file that can be imported.") +print("It can contain functions, classes, and variables.") + +# ----------------------------------------------------------------------------- +# 2. The __name__ Variable +# ----------------------------------------------------------------------------- +# __name__ tells you how the module is being used + +print("\n--- The __name__ Variable ---") + +# When a file is run directly: __name__ == "__main__" +# When a file is imported: __name__ == module_name + +print(f"Current __name__: {__name__}") + +# Common pattern for executable modules: +""" +# utils.py + +def useful_function(): + return "I'm useful!" + +def main(): + print("Running as main program") + print(useful_function()) + +# This block only runs when file is executed directly +if __name__ == "__main__": + main() +""" + +# Demo of the pattern +def demo_function(): + """A function that would be in a module.""" + return "Hello from demo!" + +def main(): + """Main function that runs when executed directly.""" + print("This module is being run directly!") + print(demo_function()) + +# This is the __name__ check pattern +if __name__ == "__main__": + # This runs only when this file is executed directly + # Not when it's imported + pass # In real code, you'd call main() + +print("\nThe 'if __name__ == \"__main__\"' pattern prevents code from") +print("running when the module is imported.") + +# ----------------------------------------------------------------------------- +# 3. Module Structure Example +# ----------------------------------------------------------------------------- + +print("\n--- Module Structure ---") + +# A well-structured module typically has: +module_template = ''' +""" +Module: my_module +Description: What this module does + +This module provides functionality for... +""" + +# Imports at the top +import os +from typing import List + +# Module-level constants +VERSION = "1.0.0" +DEFAULT_TIMEOUT = 30 + +# Private helpers (convention: prefix with _) +def _internal_helper(): + """Not meant to be used outside this module.""" + pass + +# Public functions +def public_function(arg1: str) -> str: + """ + This is a public function. + + Args: + arg1: Description of argument + + Returns: + Description of return value + """ + return f"Result: {arg1}" + +# Classes +class MyClass: + """A class in the module.""" + pass + +# Main entry point (if applicable) +def main(): + """Entry point when run as script.""" + print(public_function("test")) + +if __name__ == "__main__": + main() +''' + +print("A module should have:") +print(" 1. Module docstring at top") +print(" 2. Imports") +print(" 3. Constants") +print(" 4. Private helpers (prefixed with _)") +print(" 5. Public functions/classes") +print(" 6. Main block if it's executable") + +# ----------------------------------------------------------------------------- +# 4. Creating a Package +# ----------------------------------------------------------------------------- + +print("\n--- Creating a Package ---") + +# A package is a directory containing: +# - __init__.py (can be empty) +# - One or more module files + +package_structure = """ +my_package/ + __init__.py + module1.py + module2.py + utils/ + __init__.py + helpers.py + validators.py +""" + +print("Package structure:") +print(package_structure) + +# The __init__.py file makes a directory a package +# It can be empty or contain initialization code + +init_example = ''' +# my_package/__init__.py + +# Package version +__version__ = "1.0.0" + +# Control what gets imported with "from my_package import *" +__all__ = ['module1', 'module2', 'important_function'] + +# Import commonly used items for convenient access +from .module1 import important_function +from .module2 import AnotherClass +''' + +print("__init__.py can:") +print(" - Define package version") +print(" - Control __all__ exports") +print(" - Import frequently used items for convenience") + +# ----------------------------------------------------------------------------- +# 5. Import Examples for Packages +# ----------------------------------------------------------------------------- + +print("\n--- Package Import Examples ---") + +import_examples = """ +# Given this package structure: +# myapp/ +# __init__.py +# core.py +# utils/ +# __init__.py +# helpers.py + +# Import the package +import myapp + +# Import a module from the package +from myapp import core + +# Import a function from a module +from myapp.core import process_data + +# Import from nested package +from myapp.utils import helpers +from myapp.utils.helpers import format_output + +# Relative imports (inside the package) +# In myapp/core.py: +from . import utils # Same-level import +from .utils import helpers # Nested module +from .utils.helpers import format_output +from .. import other_package # Parent-level (if applicable) +""" + +print("Package import patterns:") +print(" import package") +print(" from package import module") +print(" from package.module import function") +print(" from package.sub import submodule") + +# ----------------------------------------------------------------------------- +# 6. Simulating a Module +# ----------------------------------------------------------------------------- + +print("\n--- Simulated Module Example ---") + +# Let's create module-like code inline to demonstrate + +# === This would be in: calculator.py === +class Calculator: + """A simple calculator class.""" + + def __init__(self): + self.history = [] + + def add(self, a, b): + """Add two numbers.""" + result = a + b + self.history.append(f"{a} + {b} = {result}") + return result + + def subtract(self, a, b): + """Subtract b from a.""" + result = a - b + self.history.append(f"{a} - {b} = {result}") + return result + + def get_history(self): + """Get calculation history.""" + return self.history + +# === Using the "module" === +calc = Calculator() +print(f"5 + 3 = {calc.add(5, 3)}") +print(f"10 - 4 = {calc.subtract(10, 4)}") +print(f"History: {calc.get_history()}") + +# ----------------------------------------------------------------------------- +# 7. Module Documentation +# ----------------------------------------------------------------------------- + +print("\n--- Module Documentation ---") + +# Good module documentation includes: +# - Module-level docstring +# - Function/class docstrings +# - Type hints +# - Examples + +documented_module = ''' +""" +mymodule - A demonstration of proper documentation + +This module provides utilities for string manipulation +and validation. It follows Google-style docstrings. + +Example: + >>> from mymodule import validate_email + >>> validate_email("test@example.com") + True + +Attributes: + EMAIL_REGEX (str): Regular expression for email validation +""" + +import re +from typing import Optional + +EMAIL_REGEX = r'^[\w.-]+@[\w.-]+\.\w+$' + +def validate_email(email: str) -> bool: + """ + Validate an email address format. + + Args: + email: The email address to validate + + Returns: + True if valid format, False otherwise + + Raises: + TypeError: If email is not a string + + Example: + >>> validate_email("user@domain.com") + True + >>> validate_email("invalid-email") + False + """ + if not isinstance(email, str): + raise TypeError("email must be a string") + return bool(re.match(EMAIL_REGEX, email)) +''' + +print("Include in your modules:") +print(" - Module docstring explaining purpose") +print(" - Type hints for parameters and returns") +print(" - Examples in docstrings") +print(" - Proper exception documentation") + +# ----------------------------------------------------------------------------- +# 8. The __all__ Variable +# ----------------------------------------------------------------------------- + +print("\n--- The __all__ Variable ---") + +# __all__ controls what's exported with "from module import *" + +all_example = ''' +# mymodule.py + +# Only these will be exported with "from mymodule import *" +__all__ = ['public_func', 'PublicClass', 'CONSTANT'] + +CONSTANT = 42 + +def public_func(): + """This is meant to be used externally.""" + pass + +def _private_func(): + """This is internal (won't be exported).""" + pass + +class PublicClass: + """This class is for external use.""" + pass +''' + +print("__all__ defines what gets exported with 'import *'") +print("Items NOT in __all__ won't be exported") +print("Underscore-prefixed items are convention for private") + +# ----------------------------------------------------------------------------- +# 9. Reloading Modules +# ----------------------------------------------------------------------------- + +print("\n--- Reloading Modules ---") + +# During development, you might need to reload a modified module +from importlib import reload + +# If you modify 'mymodule', you can reload it: +# import mymodule +# # ... modify the file ... +# reload(mymodule) # Get the updated version + +print("Use importlib.reload() to reload a modified module") +print("Useful during development and debugging") + +# ----------------------------------------------------------------------------- +# 10. Project Structure Best Practices +# ----------------------------------------------------------------------------- + +print("\n--- Project Structure Best Practices ---") + +project_structure = """ +myproject/ + README.md + setup.py or pyproject.toml + requirements.txt + .gitignore + + src/ + mypackage/ + __init__.py + core.py + utils.py + models/ + __init__.py + user.py + product.py + + tests/ + __init__.py + test_core.py + test_utils.py + + docs/ + index.md + + scripts/ + run_server.py +""" + +print("Recommended project structure:") +print(project_structure) + +print("Key points:") +print(" - Separate source code (src/) from tests") +print(" - Keep configuration at project root") +print(" - Use __init__.py for all packages") +print(" - Tests mirror source structure") diff --git a/07_error_handling/01_try_except.py b/07_error_handling/01_try_except.py new file mode 100644 index 0000000..e6c0ace --- /dev/null +++ b/07_error_handling/01_try_except.py @@ -0,0 +1,335 @@ +""" +================================================================================ +File: 01_try_except.py +Topic: Exception Handling with try-except +================================================================================ + +This file demonstrates error handling in Python using try-except blocks. +Proper error handling makes your code more robust and user-friendly by +gracefully managing unexpected situations. + +Key Concepts: +- try, except, else, finally blocks +- Catching specific exceptions +- Exception hierarchy +- Raising exceptions +- Getting exception information + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic try-except +# ----------------------------------------------------------------------------- +# Catch and handle errors that would otherwise crash your program + +print("--- Basic try-except ---") + +# Without error handling - this would crash: +# result = 10 / 0 # ZeroDivisionError! + +# With error handling: +try: + result = 10 / 0 +except: + print("An error occurred!") + +# Better - catch specific exception: +try: + result = 10 / 0 +except ZeroDivisionError: + print("Cannot divide by zero!") + result = 0 + +print(f"Result: {result}") + +# ----------------------------------------------------------------------------- +# 2. Catching Specific Exceptions +# ----------------------------------------------------------------------------- +# Different errors need different handling + +print("\n--- Catching Specific Exceptions ---") + +def safe_divide(a, b): + """Divide with specific error handling.""" + try: + result = a / b + except ZeroDivisionError: + print(" Error: Cannot divide by zero") + return None + except TypeError: + print(" Error: Invalid types for division") + return None + return result + +print(f"10 / 2 = {safe_divide(10, 2)}") +print(f"10 / 0 = {safe_divide(10, 0)}") +print(f"10 / 'a' = {safe_divide(10, 'a')}") + +# ----------------------------------------------------------------------------- +# 3. Catching Multiple Exceptions +# ----------------------------------------------------------------------------- + +print("\n--- Multiple Exceptions ---") + +# Catch multiple in one line +def process_number(value): + """Process a number with multiple exception handlers.""" + try: + # This might raise ValueError or TypeError + number = int(value) + result = 100 / number + return result + except (ValueError, TypeError) as e: + print(f" Conversion error: {e}") + return None + except ZeroDivisionError: + print(" Cannot divide by zero") + return None + +print(f"process_number('5'): {process_number('5')}") +print(f"process_number('abc'): {process_number('abc')}") +print(f"process_number('0'): {process_number('0')}") + +# ----------------------------------------------------------------------------- +# 4. The else Clause +# ----------------------------------------------------------------------------- +# Runs only if no exception occurred + +print("\n--- The else Clause ---") + +def divide_with_else(a, b): + """Division with else clause for success logging.""" + try: + result = a / b + except ZeroDivisionError: + print(" Division failed: Cannot divide by zero") + return None + else: + # Only runs if try block succeeded + print(f" Division successful: {a} / {b} = {result}") + return result + +divide_with_else(10, 2) +divide_with_else(10, 0) + +# ----------------------------------------------------------------------------- +# 5. The finally Clause +# ----------------------------------------------------------------------------- +# Always runs, regardless of whether an exception occurred + +print("\n--- The finally Clause ---") + +def read_file_example(filename): + """Demonstrate finally for cleanup.""" + file = None + try: + print(f" Attempting to open '{filename}'...") + # Simulating file operation + if filename == "missing.txt": + raise FileNotFoundError("File not found") + print(" File opened successfully!") + return "File content" + except FileNotFoundError as e: + print(f" Error: {e}") + return None + finally: + # This ALWAYS runs + print(" Cleanup: Closing file (if open)") + +read_file_example("data.txt") +print() +read_file_example("missing.txt") + +# Real-world pattern with file +print("\n--- Real file handling ---") + +# Best practice: use 'with' statement (handles cleanup automatically) +# But for learning, here's the try-finally pattern: +""" +file = None +try: + file = open("data.txt", "r") + content = file.read() +finally: + if file: + file.close() # Always closes, even if error occurred +""" + +# ----------------------------------------------------------------------------- +# 6. Complete try-except-else-finally +# ----------------------------------------------------------------------------- + +print("\n--- Complete Pattern ---") + +def complete_example(value): + """Show all four clauses in action.""" + print(f"\n Processing: {value}") + try: + number = int(value) + result = 100 / number + except ValueError: + print(" EXCEPT: Not a valid integer") + result = None + except ZeroDivisionError: + print(" EXCEPT: Division by zero") + result = None + else: + print(f" ELSE: Success! Result = {result}") + finally: + print(" FINALLY: This always runs") + + return result + +complete_example("10") +complete_example("abc") +complete_example("0") + +# ----------------------------------------------------------------------------- +# 7. Getting Exception Information +# ----------------------------------------------------------------------------- + +print("\n--- Exception Information ---") + +try: + numbers = [1, 2, 3] + print(numbers[10]) +except IndexError as e: + print(f"Exception type: {type(e).__name__}") + print(f"Exception message: {e}") + print(f"Exception args: {e.args}") + +# Getting full traceback +import traceback + +try: + result = 1 / 0 +except ZeroDivisionError: + print("\nFull traceback:") + traceback.print_exc() + +# ----------------------------------------------------------------------------- +# 8. Common Built-in Exceptions +# ----------------------------------------------------------------------------- + +print("\n--- Common Exceptions ---") + +exceptions_demo = [ + ("ValueError", "int('abc')"), + ("TypeError", "'2' + 2"), + ("IndexError", "[1,2,3][10]"), + ("KeyError", "{}['missing']"), + ("AttributeError", "'string'.missing_method()"), + ("FileNotFoundError", "open('nonexistent.txt')"), + ("ZeroDivisionError", "1/0"), + ("ImportError", "import nonexistent_module"), + ("NameError", "undefined_variable"), +] + +print("Common exception types:") +for name, example in exceptions_demo: + print(f" {name}: {example}") + +# ----------------------------------------------------------------------------- +# 9. Exception Hierarchy +# ----------------------------------------------------------------------------- + +print("\n--- Exception Hierarchy ---") + +# All exceptions inherit from BaseException +# Most use Exception as base + +hierarchy = """ +BaseException +├── SystemExit +├── KeyboardInterrupt +├── GeneratorExit +└── Exception + ├── StopIteration + ├── ArithmeticError + │ ├── ZeroDivisionError + │ ├── FloatingPointError + │ └── OverflowError + ├── LookupError + │ ├── IndexError + │ └── KeyError + ├── ValueError + ├── TypeError + ├── AttributeError + ├── OSError + │ ├── FileNotFoundError + │ └── PermissionError + └── RuntimeError +""" + +print("Exception hierarchy (simplified):") +print(hierarchy) + +# Catching parent catches all children +try: + result = 1 / 0 +except ArithmeticError: # Catches ZeroDivisionError too! + print("Caught an arithmetic error (parent class)") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# Example 1: User input validation +def get_positive_number(prompt): + """Get a positive number from user (simulated).""" + test_inputs = ["abc", "-5", "0", "10"] + + for user_input in test_inputs: + print(f" Input: '{user_input}'", end=" → ") + try: + number = float(user_input) + if number <= 0: + raise ValueError("Number must be positive") + print(f"Valid! ({number})") + return number + except ValueError as e: + print(f"Invalid: {e}") + return None + +print("User input validation:") +get_positive_number("Enter a positive number: ") + +# Example 2: Safe dictionary access +def safe_get(dictionary, *keys, default=None): + """Safely navigate nested dictionary.""" + current = dictionary + try: + for key in keys: + current = current[key] + return current + except (KeyError, TypeError): + return default + +data = {"user": {"profile": {"name": "Alice"}}} +print(f"\nNested access: {safe_get(data, 'user', 'profile', 'name')}") +print(f"Missing key: {safe_get(data, 'user', 'missing', 'name', default='N/A')}") + +# Example 3: Retry pattern +def fetch_with_retry(max_retries=3): + """Simulate fetching with retry on failure.""" + import random + + for attempt in range(1, max_retries + 1): + try: + print(f" Attempt {attempt}...", end=" ") + # Simulate random failure + if random.random() < 0.7: # 70% chance of failure + raise ConnectionError("Network error") + print("Success!") + return "Data" + except ConnectionError as e: + print(f"Failed: {e}") + if attempt == max_retries: + print(" All retries exhausted!") + return None + +print("\nRetry pattern:") +fetch_with_retry() diff --git a/07_error_handling/02_custom_exceptions.py b/07_error_handling/02_custom_exceptions.py new file mode 100644 index 0000000..34195f6 --- /dev/null +++ b/07_error_handling/02_custom_exceptions.py @@ -0,0 +1,408 @@ +""" +================================================================================ +File: 02_custom_exceptions.py +Topic: Creating and Using Custom Exceptions +================================================================================ + +This file demonstrates how to create your own exception classes in Python. +Custom exceptions make your code more expressive and allow you to handle +specific error conditions in a meaningful way. + +Key Concepts: +- Why use custom exceptions +- Creating exception classes +- Exception with custom attributes +- Exception chaining +- Best practices + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Why Custom Exceptions? +# ----------------------------------------------------------------------------- + +print("--- Why Custom Exceptions? ---") + +# Built-in exceptions are generic +# Custom exceptions: +# - Are more descriptive +# - Can carry additional data +# - Allow specific handling +# - Document error conditions + +print("Benefits of custom exceptions:") +print(" - More meaningful error messages") +print(" - Carry context-specific data") +print(" - Enable targeted exception handling") +print(" - Self-documenting code") + +# ----------------------------------------------------------------------------- +# 2. Basic Custom Exception +# ----------------------------------------------------------------------------- + +print("\n--- Basic Custom Exception ---") + +# Inherit from Exception (or a more specific built-in) +class InvalidAgeError(Exception): + """Raised when an age value is invalid.""" + pass + +# Using the custom exception +def set_age(age): + """Set age with validation.""" + if age < 0: + raise InvalidAgeError("Age cannot be negative") + if age > 150: + raise InvalidAgeError("Age seems unrealistically high") + return age + +# Catching custom exception +try: + set_age(-5) +except InvalidAgeError as e: + print(f"Caught InvalidAgeError: {e}") + +try: + set_age(200) +except InvalidAgeError as e: + print(f"Caught InvalidAgeError: {e}") + +# ----------------------------------------------------------------------------- +# 3. Custom Exception with Attributes +# ----------------------------------------------------------------------------- + +print("\n--- Exception with Custom Attributes ---") + +class ValidationError(Exception): + """Raised when validation fails.""" + + def __init__(self, field, value, message): + self.field = field + self.value = value + self.message = message + # Call parent constructor with full message + super().__init__(f"{field}: {message} (got: {value})") + + def to_dict(self): + """Convert error to dictionary (useful for API responses).""" + return { + "field": self.field, + "value": self.value, + "message": self.message + } + +# Using the enhanced exception +def validate_email(email): + """Validate email format.""" + if not email: + raise ValidationError("email", email, "Email is required") + if "@" not in email: + raise ValidationError("email", email, "Invalid email format") + return True + +try: + validate_email("invalid-email") +except ValidationError as e: + print(f"Error: {e}") + print(f"Field: {e.field}") + print(f"Value: {e.value}") + print(f"As dict: {e.to_dict()}") + +# ----------------------------------------------------------------------------- +# 4. Exception Hierarchy +# ----------------------------------------------------------------------------- + +print("\n--- Exception Hierarchy ---") + +# Create a base exception for your module/application +class AppError(Exception): + """Base exception for the application.""" + pass + +class DatabaseError(AppError): + """Database-related errors.""" + pass + +class ConnectionError(DatabaseError): + """Database connection errors.""" + pass + +class QueryError(DatabaseError): + """Query execution errors.""" + pass + +class AuthenticationError(AppError): + """Authentication-related errors.""" + pass + +class PermissionError(AppError): + """Permission-related errors.""" + pass + +# Now you can catch broadly or specifically +def database_operation(): + raise QueryError("Invalid SQL syntax") + +try: + database_operation() +except DatabaseError as e: + # Catches ConnectionError and QueryError + print(f"Database error: {e}") +except AppError as e: + # Catches all app errors + print(f"App error: {e}") + +# ----------------------------------------------------------------------------- +# 5. Exception Chaining +# ----------------------------------------------------------------------------- + +print("\n--- Exception Chaining ---") + +class ProcessingError(Exception): + """Error during data processing.""" + pass + +def parse_config(data): + """Parse configuration data.""" + try: + # Simulating a parsing error + if not data: + raise ValueError("Empty data") + return {"parsed": data} + except ValueError as e: + # Chain the original exception + raise ProcessingError("Failed to parse config") from e + +try: + parse_config("") +except ProcessingError as e: + print(f"Processing error: {e}") + print(f"Caused by: {e.__cause__}") + +# Using 'from None' to suppress chaining +def simple_error(): + try: + raise ValueError("original") + except ValueError: + raise RuntimeError("new error") from None # Hides original + +# ----------------------------------------------------------------------------- +# 6. Practical Example: User Registration +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: User Registration ---") + +class RegistrationError(Exception): + """Base class for registration errors.""" + pass + +class UsernameError(RegistrationError): + """Username-related errors.""" + def __init__(self, username, reason): + self.username = username + self.reason = reason + super().__init__(f"Username '{username}': {reason}") + +class PasswordError(RegistrationError): + """Password-related errors.""" + def __init__(self, issues): + self.issues = issues + super().__init__(f"Password issues: {', '.join(issues)}") + +class EmailError(RegistrationError): + """Email-related errors.""" + pass + +def validate_username(username): + """Validate username.""" + if len(username) < 3: + raise UsernameError(username, "Too short (min 3 chars)") + if not username.isalnum(): + raise UsernameError(username, "Must be alphanumeric") + # Check if username exists (simulated) + existing = ["admin", "root", "user"] + if username.lower() in existing: + raise UsernameError(username, "Already taken") + return True + +def validate_password(password): + """Validate password strength.""" + issues = [] + if len(password) < 8: + issues.append("too short") + if not any(c.isupper() for c in password): + issues.append("needs uppercase") + if not any(c.islower() for c in password): + issues.append("needs lowercase") + if not any(c.isdigit() for c in password): + issues.append("needs digit") + + if issues: + raise PasswordError(issues) + return True + +def register_user(username, password, email): + """Register a new user.""" + try: + validate_username(username) + validate_password(password) + # validate_email(email) - already defined above + print(f" ✓ User '{username}' registered successfully!") + return True + except RegistrationError as e: + print(f" ✗ Registration failed: {e}") + return False + +# Test registration +print("Registration attempts:") +register_user("ab", "password123", "test@example.com") +register_user("admin", "Password1", "test@example.com") +register_user("newuser", "weak", "test@example.com") +register_user("newuser", "StrongPass123", "test@example.com") + +# ----------------------------------------------------------------------------- +# 7. Context Manager with Exceptions +# ----------------------------------------------------------------------------- + +print("\n--- Exception in Context Manager ---") + +class ManagedResource: + """Resource with cleanup that handles exceptions.""" + + def __init__(self, name): + self.name = name + + def __enter__(self): + print(f" Acquiring resource: {self.name}") + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + print(f" Releasing resource: {self.name}") + if exc_type is not None: + print(f" Exception occurred: {exc_type.__name__}: {exc_val}") + # Return True to suppress the exception + # Return False to propagate it + return False # Don't suppress exceptions + +# Using the context manager +print("Context manager with exception:") +try: + with ManagedResource("database") as resource: + print(f" Using {resource.name}") + raise RuntimeError("Something went wrong!") +except RuntimeError: + print(" Caught exception outside") + +# ----------------------------------------------------------------------------- +# 8. Re-raising Exceptions +# ----------------------------------------------------------------------------- + +print("\n--- Re-raising Exceptions ---") + +def process_data(data): + """Process data with logging and re-raise.""" + try: + # Processing that might fail + if not data: + raise ValueError("Empty data") + return data.upper() + except ValueError as e: + print(f" Logging error: {e}") + raise # Re-raise the same exception + +try: + process_data("") +except ValueError as e: + print(f"Caught re-raised exception: {e}") + +# ----------------------------------------------------------------------------- +# 9. Exception Documentation +# ----------------------------------------------------------------------------- + +print("\n--- Exception Documentation ---") + +class APIError(Exception): + """ + Base exception for API errors. + + Attributes: + status_code: HTTP status code + message: Human-readable error message + error_code: Application-specific error code + + Example: + >>> raise APIError(404, "Resource not found", "NOT_FOUND") + """ + + def __init__(self, status_code: int, message: str, error_code: str = None): + """ + Initialize API error. + + Args: + status_code: HTTP status code (e.g., 400, 404, 500) + message: Human-readable error description + error_code: Optional application error code + """ + self.status_code = status_code + self.message = message + self.error_code = error_code + super().__init__(message) + + def to_response(self): + """Convert to API response format.""" + return { + "error": { + "code": self.error_code, + "message": self.message, + "status": self.status_code + } + } + +# Using documented exception +try: + raise APIError(404, "User not found", "USER_NOT_FOUND") +except APIError as e: + print(f"API Error Response: {e.to_response()}") + +# ----------------------------------------------------------------------------- +# 10. Best Practices +# ----------------------------------------------------------------------------- + +print("\n--- Best Practices ---") + +best_practices = """ +1. Inherit from Exception, not BaseException + - BaseException includes SystemExit, KeyboardInterrupt + +2. Create a hierarchy for related exceptions + - Base exception for your module/app + - Specific exceptions inherit from base + +3. Add useful attributes + - Include context that helps debugging + - Provide methods for serialization (API responses) + +4. Document your exceptions + - What conditions trigger them + - What attributes they have + - How to handle them + +5. Use meaningful names + - End with 'Error' or 'Exception' + - Be specific: UserNotFoundError, not JustError + +6. Don't over-catch + - except Exception: catches too much + - Be specific about what you expect + +7. Re-raise when appropriate + - Log and re-raise for debugging + - Transform to appropriate exception type + +8. Use exception chaining + - Use 'from' to preserve original exception + - Helps with debugging complex systems +""" + +print(best_practices) diff --git a/08_oop/01_classes_objects.py b/08_oop/01_classes_objects.py new file mode 100644 index 0000000..8351799 --- /dev/null +++ b/08_oop/01_classes_objects.py @@ -0,0 +1,389 @@ +""" +================================================================================ +File: 01_classes_objects.py +Topic: Classes and Objects in Python +================================================================================ + +This file introduces Object-Oriented Programming (OOP) in Python, focusing on +classes and objects. Classes are blueprints for creating objects, which bundle +data (attributes) and functionality (methods) together. + +Key Concepts: +- Defining classes +- Creating objects (instances) +- Instance attributes and methods +- Class attributes +- The self parameter + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. What is a Class? +# ----------------------------------------------------------------------------- +# A class is a blueprint for creating objects + +print("--- What is a Class? ---") + +# Simple class definition +class Dog: + """A simple class representing a dog.""" + pass # Empty class (for now) + +# Creating objects (instances) from the class +dog1 = Dog() +dog2 = Dog() + +print(f"dog1 is a: {type(dog1)}") +print(f"dog1 and dog2 are different objects: {dog1 is not dog2}") + +# ----------------------------------------------------------------------------- +# 2. Instance Attributes +# ----------------------------------------------------------------------------- +# Each object can have its own data + +print("\n--- Instance Attributes ---") + +class Person: + """A class representing a person.""" + + def __init__(self, name, age): + """Initialize person with name and age.""" + # self refers to the instance being created + self.name = name + self.age = age + +# Creating instances with attributes +person1 = Person("Alice", 25) +person2 = Person("Bob", 30) + +print(f"Person 1: {person1.name}, age {person1.age}") +print(f"Person 2: {person2.name}, age {person2.age}") + +# Modifying attributes +person1.age = 26 +print(f"After birthday: {person1.name} is now {person1.age}") + +# Adding new attributes to instance +person1.email = "alice@example.com" +print(f"Added email: {person1.email}") + +# ----------------------------------------------------------------------------- +# 3. Instance Methods +# ----------------------------------------------------------------------------- +# Functions defined inside a class that operate on instances + +print("\n--- Instance Methods ---") + +class Rectangle: + """A class representing a rectangle.""" + + def __init__(self, width, height): + """Initialize rectangle dimensions.""" + self.width = width + self.height = height + + def area(self): + """Calculate the area of the rectangle.""" + return self.width * self.height + + def perimeter(self): + """Calculate the perimeter of the rectangle.""" + return 2 * (self.width + self.height) + + def describe(self): + """Return a description of the rectangle.""" + return f"Rectangle {self.width}x{self.height}" + +# Using methods +rect = Rectangle(5, 3) +print(f"Rectangle: {rect.describe()}") +print(f"Area: {rect.area()}") +print(f"Perimeter: {rect.perimeter()}") + +# ----------------------------------------------------------------------------- +# 4. The self Parameter +# ----------------------------------------------------------------------------- +# self refers to the current instance + +print("\n--- Understanding self ---") + +class Counter: + """A class demonstrating self.""" + + def __init__(self): + """Initialize counter to 0.""" + self.count = 0 + + def increment(self): + """Increase count by 1.""" + self.count += 1 # self.count refers to this instance's count + return self + + def decrement(self): + """Decrease count by 1.""" + self.count -= 1 + return self + + def reset(self): + """Reset count to 0.""" + self.count = 0 + return self + + def get_count(self): + """Return current count.""" + return self.count + +counter1 = Counter() +counter2 = Counter() + +counter1.increment() +counter1.increment() +counter1.increment() + +counter2.increment() + +print(f"Counter 1: {counter1.get_count()}") # 3 +print(f"Counter 2: {counter2.get_count()}") # 1 - separate instance! + +# Method chaining (returning self) +counter1.reset().increment().increment() +print(f"After chaining: {counter1.get_count()}") + +# ----------------------------------------------------------------------------- +# 5. Class Attributes +# ----------------------------------------------------------------------------- +# Attributes shared by all instances of a class + +print("\n--- Class Attributes ---") + +class Car: + """A class with class attributes.""" + + # Class attribute - shared by all instances + wheels = 4 + count = 0 # Track number of cars created + + def __init__(self, brand, model): + """Initialize car with brand and model.""" + self.brand = brand # Instance attribute + self.model = model # Instance attribute + Car.count += 1 # Increment class attribute + + def describe(self): + """Describe the car.""" + return f"{self.brand} {self.model} ({Car.wheels} wheels)" + +# Creating cars +car1 = Car("Toyota", "Camry") +car2 = Car("Honda", "Civic") +car3 = Car("Ford", "Mustang") + +print(f"Car 1: {car1.describe()}") +print(f"Car 2: {car2.describe()}") +print(f"Total cars created: {Car.count}") + +# Accessing class attribute from class or instance +print(f"Car.wheels: {Car.wheels}") +print(f"car1.wheels: {car1.wheels}") + +# Modifying class attribute +Car.wheels = 6 # Now all cars have 6 wheels +print(f"After modification - car1.wheels: {car1.wheels}") +print(f"After modification - car2.wheels: {car2.wheels}") + +# ----------------------------------------------------------------------------- +# 6. Private Attributes (Convention) +# ----------------------------------------------------------------------------- +# Python uses naming conventions for privacy + +print("\n--- Private Attributes ---") + +class BankAccount: + """A class demonstrating private attributes.""" + + def __init__(self, owner, balance=0): + """Initialize bank account.""" + self.owner = owner # Public + self._balance = balance # Protected (convention) + self.__id = id(self) # Private (name mangling) + + def deposit(self, amount): + """Deposit money into account.""" + if amount > 0: + self._balance += amount + return True + return False + + def withdraw(self, amount): + """Withdraw money from account.""" + if 0 < amount <= self._balance: + self._balance -= amount + return True + return False + + def get_balance(self): + """Get current balance.""" + return self._balance + +account = BankAccount("Alice", 1000) +account.deposit(500) +account.withdraw(200) +print(f"Account owner: {account.owner}") +print(f"Balance: {account.get_balance()}") + +# Can still access protected (but shouldn't) +print(f"Protected _balance: {account._balance}") + +# Mangled name for private +print(f"Mangled private __id: {account._BankAccount__id}") + +# ----------------------------------------------------------------------------- +# 7. Methods that Return New Objects +# ----------------------------------------------------------------------------- + +print("\n--- Returning Objects ---") + +class Point: + """A class representing a 2D point.""" + + def __init__(self, x, y): + """Initialize point coordinates.""" + self.x = x + self.y = y + + def move(self, dx, dy): + """Return a new point moved by (dx, dy).""" + return Point(self.x + dx, self.y + dy) + + def distance_to(self, other): + """Calculate distance to another point.""" + return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5 + + def __str__(self): + """String representation.""" + return f"Point({self.x}, {self.y})" + +p1 = Point(0, 0) +p2 = p1.move(3, 4) +print(f"Original point: {p1}") +print(f"New point: {p2}") +print(f"Distance: {p1.distance_to(p2)}") + +# ----------------------------------------------------------------------------- +# 8. Special Methods (Dunder Methods) +# ----------------------------------------------------------------------------- +# Methods with double underscores have special meanings + +print("\n--- Special Methods ---") + +class Vector: + """A class demonstrating special methods.""" + + def __init__(self, x, y): + """Initialize vector.""" + self.x = x + self.y = y + + def __str__(self): + """Human-readable string representation.""" + return f"Vector({self.x}, {self.y})" + + def __repr__(self): + """Developer representation (for debugging).""" + return f"Vector(x={self.x}, y={self.y})" + + def __len__(self): + """Length (in this case, magnitude as int).""" + return int((self.x ** 2 + self.y ** 2) ** 0.5) + + def __add__(self, other): + """Add two vectors.""" + return Vector(self.x + other.x, self.y + other.y) + + def __eq__(self, other): + """Check equality.""" + return self.x == other.x and self.y == other.y + +v1 = Vector(3, 4) +v2 = Vector(1, 2) +v3 = v1 + v2 # Uses __add__ + +print(f"v1: {v1}") # Uses __str__ +print(f"v1 + v2 = {v3}") # Uses __add__ and __str__ +print(f"len(v1): {len(v1)}") # Uses __len__ +print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}") # Uses __eq__ + +# ----------------------------------------------------------------------------- +# 9. Practical Example: Student Class +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: Student ---") + +class Student: + """A class representing a student.""" + + school_name = "Python Academy" # Class attribute + + def __init__(self, name, student_id): + """Initialize student.""" + self.name = name + self.student_id = student_id + self.grades = [] + + def add_grade(self, grade): + """Add a grade (0-100).""" + if 0 <= grade <= 100: + self.grades.append(grade) + return True + return False + + def get_average(self): + """Calculate grade average.""" + if not self.grades: + return 0 + return sum(self.grades) / len(self.grades) + + def get_letter_grade(self): + """Get letter grade based on average.""" + avg = self.get_average() + if avg >= 90: return 'A' + if avg >= 80: return 'B' + if avg >= 70: return 'C' + if avg >= 60: return 'D' + return 'F' + + def __str__(self): + """String representation.""" + return f"{self.name} (ID: {self.student_id}) - {self.school_name}" + +# Using the Student class +student = Student("Baraa", "S12345") +student.add_grade(85) +student.add_grade(90) +student.add_grade(78) + +print(student) +print(f"Grades: {student.grades}") +print(f"Average: {student.get_average():.1f}") +print(f"Letter Grade: {student.get_letter_grade()}") + +# ----------------------------------------------------------------------------- +# 10. Summary +# ----------------------------------------------------------------------------- + +print("\n--- Summary ---") + +summary = """ +Key OOP concepts: + - Class: Blueprint for objects + - Object: Instance of a class + - self: Reference to current instance + - __init__: Constructor method + - Instance attributes: Unique to each object + - Class attributes: Shared by all instances + - Methods: Functions that belong to a class + - Special methods: __str__, __repr__, __add__, etc. +""" + +print(summary) diff --git a/08_oop/02_init_methods.py b/08_oop/02_init_methods.py new file mode 100644 index 0000000..ea31975 --- /dev/null +++ b/08_oop/02_init_methods.py @@ -0,0 +1,446 @@ +""" +================================================================================ +File: 02_init_methods.py +Topic: Constructors and Initialization in Python +================================================================================ + +This file explores the __init__ method and other initialization patterns +in Python classes. The __init__ method is called when creating new objects +and sets up the initial state of the instance. + +Key Concepts: +- The __init__ constructor +- Required vs optional parameters +- Default values +- Property decorators +- Alternative constructors (classmethod) + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic __init__ Method +# ----------------------------------------------------------------------------- +# __init__ initializes newly created objects + +print("--- Basic __init__ ---") + +class Book: + """A class representing a book.""" + + def __init__(self, title, author): + """ + Initialize a new book. + + Args: + title: The book's title + author: The book's author + """ + self.title = title + self.author = author + +# Creating instances calls __init__ automatically +book = Book("1984", "George Orwell") +print(f"Book: '{book.title}' by {book.author}") + +# Without proper arguments, you get an error: +# book = Book() # TypeError: missing 2 required positional arguments + +# ----------------------------------------------------------------------------- +# 2. Default Parameter Values +# ----------------------------------------------------------------------------- +# Make some parameters optional + +print("\n--- Default Values ---") + +class User: + """A class with default parameter values.""" + + def __init__(self, username, email=None, role="user", active=True): + """ + Initialize a new user. + + Args: + username: Required username + email: Optional email address + role: User role (default: "user") + active: Is user active (default: True) + """ + self.username = username + self.email = email + self.role = role + self.active = active + + def __str__(self): + email = self.email or "No email" + status = "Active" if self.active else "Inactive" + return f"{self.username} ({self.role}) - {email} - {status}" + +# Various ways to create users +user1 = User("alice") +user2 = User("bob", "bob@example.com") +user3 = User("charlie", "charlie@example.com", "admin") +user4 = User("dave", active=False) + +print(user1) +print(user2) +print(user3) +print(user4) + +# ----------------------------------------------------------------------------- +# 3. Validation in __init__ +# ----------------------------------------------------------------------------- +# Validate and process data during initialization + +print("\n--- Validation in __init__ ---") + +class Temperature: + """A class with validation in __init__.""" + + def __init__(self, celsius): + """ + Initialize temperature in Celsius. + + Args: + celsius: Temperature in Celsius + + Raises: + ValueError: If temperature is below absolute zero + """ + if celsius < -273.15: + raise ValueError("Temperature cannot be below absolute zero!") + self._celsius = celsius + + @property + def celsius(self): + """Get temperature in Celsius.""" + return self._celsius + + @property + def fahrenheit(self): + """Get temperature in Fahrenheit.""" + return (self._celsius * 9/5) + 32 + + @property + def kelvin(self): + """Get temperature in Kelvin.""" + return self._celsius + 273.15 + +temp = Temperature(25) +print(f"Temperature: {temp.celsius}°C = {temp.fahrenheit}°F = {temp.kelvin}K") + +# Validation in action +try: + invalid_temp = Temperature(-300) +except ValueError as e: + print(f"Validation error: {e}") + +# ----------------------------------------------------------------------------- +# 4. Mutable Default Arguments Warning +# ----------------------------------------------------------------------------- +# Never use mutable objects as default arguments! + +print("\n--- Mutable Defaults Warning ---") + +# BAD - mutable default argument +class BadStudent: + def __init__(self, name, grades=[]): # DON'T DO THIS! + self.name = name + self.grades = grades + +s1 = BadStudent("Alice") +s1.grades.append(90) + +s2 = BadStudent("Bob") +print(f"Bob's grades (should be empty): {s2.grades}") # Contains 90! + +# GOOD - use None and create new list +class GoodStudent: + def __init__(self, name, grades=None): + self.name = name + self.grades = grades if grades is not None else [] + +g1 = GoodStudent("Carol") +g1.grades.append(85) + +g2 = GoodStudent("Dan") +print(f"Dan's grades (correctly empty): {g2.grades}") + +# ----------------------------------------------------------------------------- +# 5. Property Decorators +# ----------------------------------------------------------------------------- +# Control attribute access with getters and setters + +print("\n--- Property Decorators ---") + +class Circle: + """A circle with property decorators.""" + + def __init__(self, radius): + """Initialize circle with radius.""" + self._radius = radius # Protected attribute + + @property + def radius(self): + """Get the radius.""" + return self._radius + + @radius.setter + def radius(self, value): + """Set the radius with validation.""" + if value <= 0: + raise ValueError("Radius must be positive") + self._radius = value + + @property + def diameter(self): + """Get the diameter (read-only computed property).""" + return self._radius * 2 + + @property + def area(self): + """Get the area (read-only computed property).""" + import math + return math.pi * self._radius ** 2 + +circle = Circle(5) +print(f"Radius: {circle.radius}") +print(f"Diameter: {circle.diameter}") +print(f"Area: {circle.area:.2f}") + +# Using setter +circle.radius = 10 +print(f"New radius: {circle.radius}") +print(f"New area: {circle.area:.2f}") + +# Validation in setter +try: + circle.radius = -5 +except ValueError as e: + print(f"Setter validation: {e}") + +# ----------------------------------------------------------------------------- +# 6. Alternative Constructors with @classmethod +# ----------------------------------------------------------------------------- +# Create objects in different ways + +print("\n--- Alternative Constructors ---") + +class Person: + """A class with alternative constructors.""" + + def __init__(self, first_name, last_name, age): + """Standard constructor.""" + self.first_name = first_name + self.last_name = last_name + self.age = age + + @classmethod + def from_birth_year(cls, first_name, last_name, birth_year): + """Create person from birth year instead of age.""" + import datetime + age = datetime.datetime.now().year - birth_year + return cls(first_name, last_name, age) + + @classmethod + def from_full_name(cls, full_name, age): + """Create person from full name string.""" + parts = full_name.split() + first_name = parts[0] + last_name = parts[-1] if len(parts) > 1 else "" + return cls(first_name, last_name, age) + + @classmethod + def from_dict(cls, data): + """Create person from dictionary.""" + return cls( + data.get('first_name', ''), + data.get('last_name', ''), + data.get('age', 0) + ) + + def __str__(self): + return f"{self.first_name} {self.last_name}, age {self.age}" + +# Using different constructors +p1 = Person("John", "Doe", 30) +p2 = Person.from_birth_year("Jane", "Smith", 1995) +p3 = Person.from_full_name("Bob Johnson", 25) +p4 = Person.from_dict({"first_name": "Alice", "last_name": "Williams", "age": 28}) + +print("Created using different constructors:") +print(f" Standard: {p1}") +print(f" From birth year: {p2}") +print(f" From full name: {p3}") +print(f" From dict: {p4}") + +# ----------------------------------------------------------------------------- +# 7. __new__ vs __init__ +# ----------------------------------------------------------------------------- +# __new__ creates the instance, __init__ initializes it + +print("\n--- __new__ vs __init__ ---") + +class Singleton: + """A singleton class using __new__.""" + + _instance = None + + def __new__(cls): + """Create instance only if one doesn't exist.""" + if cls._instance is None: + print(" Creating new instance...") + cls._instance = super().__new__(cls) + else: + print(" Returning existing instance...") + return cls._instance + + def __init__(self): + """Initialize (called every time).""" + pass + +# Both variables point to the same instance +print("Creating s1:") +s1 = Singleton() +print("Creating s2:") +s2 = Singleton() +print(f"Same instance? {s1 is s2}") + +# ----------------------------------------------------------------------------- +# 8. Initialization with Inheritance +# ----------------------------------------------------------------------------- + +print("\n--- Initialization with Inheritance ---") + +class Animal: + """Base class for animals.""" + + def __init__(self, name, species): + """Initialize animal.""" + self.name = name + self.species = species + + def speak(self): + """Make a sound.""" + return "Some sound" + +class Dog(Animal): + """Dog class inheriting from Animal.""" + + def __init__(self, name, breed): + """Initialize dog with name and breed.""" + # Call parent's __init__ + super().__init__(name, species="Canis familiaris") + self.breed = breed + + def speak(self): + """Dogs bark.""" + return "Woof!" + +class Cat(Animal): + """Cat class inheriting from Animal.""" + + def __init__(self, name, indoor=True): + """Initialize cat.""" + super().__init__(name, species="Felis catus") + self.indoor = indoor + + def speak(self): + """Cats meow.""" + return "Meow!" + +dog = Dog("Buddy", "Golden Retriever") +cat = Cat("Whiskers", indoor=True) + +print(f"Dog: {dog.name} ({dog.breed}) says {dog.speak()}") +print(f"Cat: {cat.name} (Indoor: {cat.indoor}) says {cat.speak()}") + +# ----------------------------------------------------------------------------- +# 9. Complex Initialization Example +# ----------------------------------------------------------------------------- + +print("\n--- Complex Initialization ---") + +from datetime import datetime + +class Order: + """A class with complex initialization.""" + + _order_counter = 0 + + def __init__(self, customer_name, items=None, discount=0): + """ + Initialize an order. + + Args: + customer_name: Name of the customer + items: List of (item_name, price, quantity) tuples + discount: Discount percentage (0-100) + """ + # Auto-generate order ID + Order._order_counter += 1 + self.order_id = f"ORD-{Order._order_counter:04d}" + + # Set basic attributes + self.customer_name = customer_name + self.items = items or [] + self.discount = max(0, min(100, discount)) # Clamp to 0-100 + + # Auto-generated attributes + self.created_at = datetime.now() + self._subtotal = None # Cached calculation + + def add_item(self, name, price, quantity=1): + """Add an item to the order.""" + self.items.append((name, price, quantity)) + self._subtotal = None # Invalidate cache + + @property + def subtotal(self): + """Calculate subtotal.""" + if self._subtotal is None: + self._subtotal = sum(price * qty for _, price, qty in self.items) + return self._subtotal + + @property + def total(self): + """Calculate total with discount.""" + return self.subtotal * (1 - self.discount / 100) + + def __str__(self): + return f"Order {self.order_id} for {self.customer_name}: ${self.total:.2f}" + +# Create orders +order1 = Order("Alice") +order1.add_item("Widget", 9.99, 2) +order1.add_item("Gadget", 14.99, 1) + +order2 = Order("Bob", discount=10) +order2.add_item("Premium Widget", 29.99, 1) + +print(order1) +print(f" Subtotal: ${order1.subtotal:.2f}") +print(f" Total: ${order1.total:.2f}") + +print(order2) +print(f" Subtotal: ${order2.subtotal:.2f}") +print(f" Total (10% off): ${order2.total:.2f}") + +# ----------------------------------------------------------------------------- +# 10. Summary +# ----------------------------------------------------------------------------- + +print("\n--- Summary ---") + +summary = """ +Initialization patterns: + - __init__: Main constructor, initializes attributes + - Default values: Make parameters optional + - Validation: Raise errors for invalid input + - Properties: Computed/validated attributes + - @classmethod: Alternative constructors + - __new__: Instance creation (before __init__) + - super().__init__(): Call parent constructor +""" + +print(summary) diff --git a/08_oop/03_inheritance.py b/08_oop/03_inheritance.py new file mode 100644 index 0000000..7e55d26 --- /dev/null +++ b/08_oop/03_inheritance.py @@ -0,0 +1,436 @@ +""" +================================================================================ +File: 03_inheritance.py +Topic: Inheritance in Python +================================================================================ + +This file demonstrates inheritance in Python, a fundamental OOP concept that +allows classes to inherit attributes and methods from other classes. This +promotes code reuse and establishes relationships between classes. + +Key Concepts: +- Single inheritance +- Method overriding +- super() function +- Multiple inheritance +- Method Resolution Order (MRO) +- Abstract base classes + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic Inheritance +# ----------------------------------------------------------------------------- +# Child class inherits from parent class + +print("--- Basic Inheritance ---") + +class Animal: + """Base class for all animals.""" + + def __init__(self, name): + """Initialize animal with a name.""" + self.name = name + + def speak(self): + """Make a generic sound.""" + return "Some generic sound" + + def describe(self): + """Describe the animal.""" + return f"I am {self.name}" + +class Dog(Animal): + """Dog class inheriting from Animal.""" + pass # Inherits everything from Animal + +# Dog has all Animal's methods +buddy = Dog("Buddy") +print(buddy.describe()) +print(f"Says: {buddy.speak()}") +print(f"Dog is instance of Animal: {isinstance(buddy, Animal)}") + +# ----------------------------------------------------------------------------- +# 2. Method Overriding +# ----------------------------------------------------------------------------- +# Child class can override parent methods + +print("\n--- Method Overriding ---") + +class Cat(Animal): + """Cat class with overridden method.""" + + def speak(self): + """Cats meow instead of generic sound.""" + return "Meow!" + +class Cow(Animal): + """Cow class with overridden method.""" + + def speak(self): + """Cows moo.""" + return "Moo!" + +# Each animal speaks differently +animals = [Dog("Rex"), Cat("Whiskers"), Cow("Bessie")] + +print("Each animal speaks:") +for animal in animals: + print(f" {animal.name}: {animal.speak()}") + +# ----------------------------------------------------------------------------- +# 3. Extending Parent Class with super() +# ----------------------------------------------------------------------------- +# Use super() to call parent methods + +print("\n--- Using super() ---") + +class Bird(Animal): + """Bird class extending Animal.""" + + def __init__(self, name, can_fly=True): + """Initialize bird with flying ability.""" + super().__init__(name) # Call parent's __init__ + self.can_fly = can_fly + + def speak(self): + """Birds chirp.""" + return "Chirp!" + + def describe(self): + """Extend parent's describe method.""" + base = super().describe() # Get parent's description + fly_status = "can fly" if self.can_fly else "cannot fly" + return f"{base} and I {fly_status}" + +sparrow = Bird("Sparrow", can_fly=True) +penguin = Bird("Penny", can_fly=False) + +print(sparrow.describe()) +print(penguin.describe()) + +# ----------------------------------------------------------------------------- +# 4. Inheritance Chain +# ----------------------------------------------------------------------------- +# Classes can inherit from other child classes + +print("\n--- Inheritance Chain ---") + +class Vehicle: + """Base vehicle class.""" + + def __init__(self, brand): + self.brand = brand + + def start(self): + return "Vehicle starting..." + +class Car(Vehicle): + """Car inherits from Vehicle.""" + + def __init__(self, brand, model): + super().__init__(brand) + self.model = model + + def start(self): + return f"{self.brand} {self.model} engine starting..." + +class ElectricCar(Car): + """ElectricCar inherits from Car.""" + + def __init__(self, brand, model, battery_kw): + super().__init__(brand, model) + self.battery_kw = battery_kw + + def start(self): + return f"{self.brand} {self.model} powering up silently... (Battery: {self.battery_kw}kW)" + + def charge(self): + return f"Charging {self.battery_kw}kW battery..." + +# Create instances +regular_car = Car("Toyota", "Camry") +electric_car = ElectricCar("Tesla", "Model 3", 75) + +print(regular_car.start()) +print(electric_car.start()) +print(electric_car.charge()) + +# Check inheritance +print(f"\nElectricCar is Car: {isinstance(electric_car, Car)}") +print(f"ElectricCar is Vehicle: {isinstance(electric_car, Vehicle)}") + +# ----------------------------------------------------------------------------- +# 5. Multiple Inheritance +# ----------------------------------------------------------------------------- +# A class can inherit from multiple parent classes + +print("\n--- Multiple Inheritance ---") + +class Flyable: + """Mixin class for flying ability.""" + + def fly(self): + return f"{self.name} is flying!" + + def land(self): + return f"{self.name} has landed." + +class Swimmable: + """Mixin class for swimming ability.""" + + def swim(self): + return f"{self.name} is swimming!" + + def dive(self): + return f"{self.name} dives underwater." + +class Duck(Animal, Flyable, Swimmable): + """Duck can do everything!""" + + def speak(self): + return "Quack!" + +# Duck has methods from all parent classes +duck = Duck("Donald") +print(f"{duck.name} says: {duck.speak()}") +print(duck.fly()) +print(duck.swim()) +print(duck.describe()) + +# ----------------------------------------------------------------------------- +# 6. Method Resolution Order (MRO) +# ----------------------------------------------------------------------------- +# Python determines method lookup order + +print("\n--- Method Resolution Order (MRO) ---") + +class A: + def method(self): + return "A" + +class B(A): + def method(self): + return "B" + +class C(A): + def method(self): + return "C" + +class D(B, C): + pass + +class E(B, C): + def method(self): + return "E" + +# Check MRO +print("MRO for class D:") +for cls in D.__mro__: + print(f" {cls.__name__}") + +d = D() +e = E() +print(f"\nd.method(): {d.method()}") # Returns "B" (first in MRO after D) +print(f"e.method(): {e.method()}") # Returns "E" (overridden) + +# ----------------------------------------------------------------------------- +# 7. Calling Methods from Specific Parent +# ----------------------------------------------------------------------------- + +print("\n--- Calling Specific Parent Methods ---") + +class Parent1: + def greet(self): + return "Hello from Parent1" + +class Parent2: + def greet(self): + return "Hello from Parent2" + +class Child(Parent1, Parent2): + def greet(self): + return "Hello from Child" + + def greet_all(self): + """Call all parent greet methods.""" + return [ + f"Child: {self.greet()}", + f"Parent1: {Parent1.greet(self)}", + f"Parent2: {Parent2.greet(self)}" + ] + +child = Child() +print("Calling all greet methods:") +for greeting in child.greet_all(): + print(f" {greeting}") + +# ----------------------------------------------------------------------------- +# 8. Abstract Base Classes +# ----------------------------------------------------------------------------- +# Define interfaces that must be implemented + +print("\n--- Abstract Base Classes ---") + +from abc import ABC, abstractmethod + +class Shape(ABC): + """Abstract base class for shapes.""" + + @abstractmethod + def area(self): + """Calculate area - must be implemented.""" + pass + + @abstractmethod + def perimeter(self): + """Calculate perimeter - must be implemented.""" + pass + + def describe(self): + """Non-abstract method - can be used as-is.""" + return f"A shape with area {self.area():.2f}" + +class Rectangle(Shape): + """Concrete rectangle class.""" + + def __init__(self, width, height): + self.width = width + self.height = height + + def area(self): + return self.width * self.height + + def perimeter(self): + return 2 * (self.width + self.height) + +class Circle(Shape): + """Concrete circle class.""" + + def __init__(self, radius): + self.radius = radius + + def area(self): + import math + return math.pi * self.radius ** 2 + + def perimeter(self): + import math + return 2 * math.pi * self.radius + +# Cannot instantiate abstract class +# shape = Shape() # TypeError! + +# Concrete classes work fine +rect = Rectangle(5, 3) +circle = Circle(4) + +print(f"Rectangle: area={rect.area()}, perimeter={rect.perimeter()}") +print(f"Circle: area={circle.area():.2f}, perimeter={circle.perimeter():.2f}") +print(rect.describe()) + +# ----------------------------------------------------------------------------- +# 9. Using isinstance() and issubclass() +# ----------------------------------------------------------------------------- + +print("\n--- Type Checking ---") + +class Mammal(Animal): + pass + +class Reptile(Animal): + pass + +class DogV2(Mammal): + def speak(self): + return "Woof!" + +class Snake(Reptile): + def speak(self): + return "Hiss!" + +dog = DogV2("Rex") +snake = Snake("Slinky") + +# isinstance() checks if object is instance of class +print("isinstance checks:") +print(f" dog isinstance of DogV2: {isinstance(dog, DogV2)}") +print(f" dog isinstance of Mammal: {isinstance(dog, Mammal)}") +print(f" dog isinstance of Animal: {isinstance(dog, Animal)}") +print(f" dog isinstance of Reptile: {isinstance(dog, Reptile)}") + +# issubclass() checks class relationships +print("\nissubclass checks:") +print(f" DogV2 issubclass of Mammal: {issubclass(DogV2, Mammal)}") +print(f" DogV2 issubclass of Animal: {issubclass(DogV2, Animal)}") +print(f" DogV2 issubclass of Reptile: {issubclass(DogV2, Reptile)}") + +# ----------------------------------------------------------------------------- +# 10. Practical Example: Employee Hierarchy +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: Employee Hierarchy ---") + +class Employee: + """Base employee class.""" + + def __init__(self, name, employee_id, salary): + self.name = name + self.employee_id = employee_id + self._salary = salary + + @property + def salary(self): + return self._salary + + def get_annual_salary(self): + return self._salary * 12 + + def __str__(self): + return f"{self.name} (ID: {self.employee_id})" + +class Manager(Employee): + """Manager with team and bonus.""" + + def __init__(self, name, employee_id, salary, team_size=0): + super().__init__(name, employee_id, salary) + self.team_size = team_size + + def get_annual_salary(self): + # Managers get bonus based on team size + base = super().get_annual_salary() + bonus = self.team_size * 1000 # $1000 per team member + return base + bonus + + def __str__(self): + return f"Manager {super().__str__()} - Team: {self.team_size}" + +class Developer(Employee): + """Developer with tech stack.""" + + def __init__(self, name, employee_id, salary, languages): + super().__init__(name, employee_id, salary) + self.languages = languages + + def get_annual_salary(self): + # Developers get extra for each language + base = super().get_annual_salary() + language_bonus = len(self.languages) * 500 # $500 per language + return base + language_bonus + + def __str__(self): + langs = ", ".join(self.languages) + return f"Developer {super().__str__()} - Languages: {langs}" + +# Create employees +manager = Manager("Alice", "M001", 8000, team_size=5) +dev1 = Developer("Bob", "D001", 6000, ["Python", "JavaScript", "SQL"]) +dev2 = Developer("Charlie", "D002", 5500, ["Python"]) + +employees = [manager, dev1, dev2] + +print("Employee Details:") +for emp in employees: + print(f" {emp}") + print(f" Annual Salary: ${emp.get_annual_salary():,}") diff --git a/08_oop/04_polymorphism.py b/08_oop/04_polymorphism.py new file mode 100644 index 0000000..2db7a43 --- /dev/null +++ b/08_oop/04_polymorphism.py @@ -0,0 +1,530 @@ +""" +================================================================================ +File: 04_polymorphism.py +Topic: Polymorphism in Python +================================================================================ + +This file demonstrates polymorphism in Python - the ability of different +objects to respond to the same method call in different ways. This is a +core OOP principle that enables flexible and extensible code. + +Key Concepts: +- Duck typing +- Method polymorphism +- Operator overloading +- Protocols and ABC +- Practical polymorphism patterns + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. What is Polymorphism? +# ----------------------------------------------------------------------------- +# "Many forms" - same interface, different implementations + +print("--- What is Polymorphism? ---") + +class Dog: + def speak(self): + return "Woof!" + +class Cat: + def speak(self): + return "Meow!" + +class Duck: + def speak(self): + return "Quack!" + +# Same method call, different behavior +animals = [Dog(), Cat(), Duck()] + +print("Different animals, same method:") +for animal in animals: + print(f" {type(animal).__name__}: {animal.speak()}") + +# ----------------------------------------------------------------------------- +# 2. Duck Typing +# ----------------------------------------------------------------------------- +# "If it walks like a duck and quacks like a duck, it's a duck" + +print("\n--- Duck Typing ---") + +class RealDuck: + """A real duck.""" + def quack(self): + return "Quack quack!" + + def fly(self): + return "Flap flap, flying!" + +class RobotDuck: + """A robot that acts like a duck.""" + def quack(self): + return "Beep boop quack!" + + def fly(self): + return "Propellers spinning, ascending!" + +class Person: + """A person pretending to be a duck.""" + def quack(self): + return "*Person making quack sounds*" + + def fly(self): + return "*Person flapping arms*" + +def duck_demo(duck): + """Demonstrate a duck (or anything duck-like).""" + print(f" {type(duck).__name__}:") + print(f" Quacking: {duck.quack()}") + print(f" Flying: {duck.fly()}") + +# All these work because they have the required methods +print("Duck typing demo:") +duck_demo(RealDuck()) +duck_demo(RobotDuck()) +duck_demo(Person()) + +# ----------------------------------------------------------------------------- +# 3. Method Polymorphism with Inheritance +# ----------------------------------------------------------------------------- + +print("\n--- Method Polymorphism ---") + +class Shape: + """Base shape class.""" + + def area(self): + raise NotImplementedError("Subclass must implement area()") + + def describe(self): + return f"{self.__class__.__name__} with area {self.area():.2f}" + +class Rectangle(Shape): + def __init__(self, width, height): + self.width = width + self.height = height + + def area(self): + return self.width * self.height + +class Circle(Shape): + def __init__(self, radius): + self.radius = radius + + def area(self): + import math + return math.pi * self.radius ** 2 + +class Triangle(Shape): + def __init__(self, base, height): + self.base = base + self.height = height + + def area(self): + return 0.5 * self.base * self.height + +# Polymorphism in action +shapes = [ + Rectangle(5, 3), + Circle(4), + Triangle(6, 4) +] + +print("Shape descriptions:") +for shape in shapes: + print(f" {shape.describe()}") + +# Calculate total area polymorphically +total_area = sum(shape.area() for shape in shapes) +print(f"\nTotal area of all shapes: {total_area:.2f}") + +# ----------------------------------------------------------------------------- +# 4. Operator Overloading +# ----------------------------------------------------------------------------- +# Same operators, different behaviors + +print("\n--- Operator Overloading ---") + +class Vector: + """A 2D vector with overloaded operators.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + def __str__(self): + return f"Vector({self.x}, {self.y})" + + def __repr__(self): + return self.__str__() + + def __add__(self, other): + """Vector addition: v1 + v2""" + return Vector(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + """Vector subtraction: v1 - v2""" + return Vector(self.x - other.x, self.y - other.y) + + def __mul__(self, scalar): + """Scalar multiplication: v * n""" + return Vector(self.x * scalar, self.y * scalar) + + def __rmul__(self, scalar): + """Reverse multiplication: n * v""" + return self.__mul__(scalar) + + def __neg__(self): + """Negation: -v""" + return Vector(-self.x, -self.y) + + def __eq__(self, other): + """Equality: v1 == v2""" + return self.x == other.x and self.y == other.y + + def __abs__(self): + """Magnitude: abs(v)""" + return (self.x ** 2 + self.y ** 2) ** 0.5 + +v1 = Vector(3, 4) +v2 = Vector(1, 2) + +print(f"v1 = {v1}") +print(f"v2 = {v2}") +print(f"v1 + v2 = {v1 + v2}") +print(f"v1 - v2 = {v1 - v2}") +print(f"v1 * 3 = {v1 * 3}") +print(f"2 * v2 = {2 * v2}") +print(f"-v1 = {-v1}") +print(f"|v1| = {abs(v1)}") +print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}") + +# ----------------------------------------------------------------------------- +# 5. Polymorphism with Built-in Functions +# ----------------------------------------------------------------------------- + +print("\n--- Built-in Function Polymorphism ---") + +class Playlist: + """A playlist that works with len() and iteration.""" + + def __init__(self, name): + self.name = name + self.songs = [] + + def add_song(self, song): + self.songs.append(song) + + def __len__(self): + """Enable len(playlist)""" + return len(self.songs) + + def __iter__(self): + """Enable for song in playlist""" + return iter(self.songs) + + def __getitem__(self, index): + """Enable playlist[index]""" + return self.songs[index] + + def __contains__(self, song): + """Enable 'song' in playlist""" + return song in self.songs + +playlist = Playlist("My Favorites") +playlist.add_song("Song A") +playlist.add_song("Song B") +playlist.add_song("Song C") + +print(f"Playlist '{playlist.name}':") +print(f" Length: {len(playlist)}") +print(f" First song: {playlist[0]}") +print(f" 'Song B' in playlist: {'Song B' in playlist}") +print(" All songs:") +for song in playlist: + print(f" - {song}") + +# ----------------------------------------------------------------------------- +# 6. Polymorphic Functions +# ----------------------------------------------------------------------------- + +print("\n--- Polymorphic Functions ---") + +def process_payment(payment_method): + """ + Process any payment method polymorphically. + Any object with a process() method works! + """ + print(f" Processing with {type(payment_method).__name__}...") + return payment_method.process() + +class CreditCard: + def __init__(self, card_number): + self.card_number = card_number[-4:] # Last 4 digits + + def process(self): + return f"Charged to card ending in {self.card_number}" + +class PayPal: + def __init__(self, email): + self.email = email + + def process(self): + return f"Payment sent via PayPal ({self.email})" + +class CryptoCurrency: + def __init__(self, wallet): + self.wallet = wallet[:8] + + def process(self): + return f"Crypto transferred from {self.wallet}..." + +# Same function, different payment methods +payment_methods = [ + CreditCard("4111111111111234"), + PayPal("user@example.com"), + CryptoCurrency("0x1234567890abcdef") +] + +print("Processing payments:") +for method in payment_methods: + result = process_payment(method) + print(f" Result: {result}") + +# ----------------------------------------------------------------------------- +# 7. Protocols (Informal Interfaces) +# ----------------------------------------------------------------------------- + +print("\n--- Protocols (Informal Interfaces) ---") + +# Python 3.8+ has typing.Protocol for formal protocols +# Here's the concept with duck typing: + +class Drawable: + """Protocol: anything with a draw() method.""" + def draw(self): + raise NotImplementedError + +class Circle2D: + def draw(self): + return "Drawing a circle: O" + +class Square2D: + def draw(self): + return "Drawing a square: □" + +class Triangle2D: + def draw(self): + return "Drawing a triangle: △" + +class Text2D: + def __init__(self, text): + self.text = text + + def draw(self): + return f"Drawing text: '{self.text}'" + +def render_canvas(drawables): + """Render anything that has a draw() method.""" + print("Canvas:") + for drawable in drawables: + print(f" {drawable.draw()}") + +# All these can be rendered +elements = [Circle2D(), Square2D(), Triangle2D(), Text2D("Hello")] +render_canvas(elements) + +# ----------------------------------------------------------------------------- +# 8. Polymorphism with Abstract Base Classes +# ----------------------------------------------------------------------------- + +print("\n--- ABC Polymorphism ---") + +from abc import ABC, abstractmethod + +class DataExporter(ABC): + """Abstract base class for data exporters.""" + + @abstractmethod + def export(self, data): + """Export data - must be implemented.""" + pass + + def validate(self, data): + """Common validation (can be overridden).""" + if not data: + raise ValueError("No data to export") + return True + +class JSONExporter(DataExporter): + def export(self, data): + import json + self.validate(data) + return json.dumps(data, indent=2) + +class CSVExporter(DataExporter): + def export(self, data): + self.validate(data) + if not data: + return "" + headers = ",".join(data[0].keys()) + rows = [",".join(str(v) for v in row.values()) for row in data] + return headers + "\n" + "\n".join(rows) + +class XMLExporter(DataExporter): + def export(self, data): + self.validate(data) + xml = "\n" + for item in data: + xml += " \n" + for key, value in item.items(): + xml += f" <{key}>{value}\n" + xml += " \n" + xml += "" + return xml + +# Same data, different formats +sample_data = [ + {"name": "Alice", "age": 25}, + {"name": "Bob", "age": 30} +] + +exporters = [JSONExporter(), CSVExporter(), XMLExporter()] + +print("Exporting same data in different formats:") +for exporter in exporters: + print(f"\n{type(exporter).__name__}:") + print(exporter.export(sample_data)) + +# ----------------------------------------------------------------------------- +# 9. Method Dispatch Based on Type +# ----------------------------------------------------------------------------- + +print("\n--- Type-Based Method Dispatch ---") + +from functools import singledispatch + +@singledispatch +def process(value): + """Default processing for unknown types.""" + return f"Don't know how to process {type(value).__name__}" + +@process.register(int) +def _(value): + """Process integers.""" + return f"Integer: {value * 2}" + +@process.register(str) +def _(value): + """Process strings.""" + return f"String: {value.upper()}" + +@process.register(list) +def _(value): + """Process lists.""" + return f"List with {len(value)} items" + +# Same function name, different behavior based on type +print("Single dispatch polymorphism:") +print(f" process(42): {process(42)}") +print(f" process('hello'): {process('hello')}") +print(f" process([1,2,3]): {process([1,2,3])}") +print(f" process(3.14): {process(3.14)}") + +# ----------------------------------------------------------------------------- +# 10. Practical Example: Notification System +# ----------------------------------------------------------------------------- + +print("\n--- Practical Example: Notification System ---") + +class NotificationService(ABC): + """Abstract base for notification services.""" + + @abstractmethod + def send(self, recipient, message): + """Send a notification.""" + pass + + @abstractmethod + def get_status(self): + """Get service status.""" + pass + +class EmailNotification(NotificationService): + def __init__(self, smtp_server="mail.example.com"): + self.smtp_server = smtp_server + + def send(self, recipient, message): + return f"📧 Email sent to {recipient}: '{message}'" + + def get_status(self): + return f"Email service connected to {self.smtp_server}" + +class SMSNotification(NotificationService): + def __init__(self, provider="TwilioMock"): + self.provider = provider + + def send(self, recipient, message): + return f"📱 SMS sent to {recipient}: '{message[:50]}...'" + + def get_status(self): + return f"SMS service using {self.provider}" + +class PushNotification(NotificationService): + def __init__(self, app_name="MyApp"): + self.app_name = app_name + + def send(self, recipient, message): + return f"🔔 Push notification to {recipient}: '{message}'" + + def get_status(self): + return f"Push service for {self.app_name}" + +class SlackNotification(NotificationService): + def __init__(self, workspace="MyWorkspace"): + self.workspace = workspace + + def send(self, recipient, message): + return f"💬 Slack message to #{recipient}: '{message}'" + + def get_status(self): + return f"Slack connected to {self.workspace}" + +# Polymorphic notification manager +class NotificationManager: + """Manages multiple notification services.""" + + def __init__(self): + self.services = [] + + def add_service(self, service): + self.services.append(service) + + def send_all(self, recipient, message): + """Send via all services.""" + results = [] + for service in self.services: + results.append(service.send(recipient, message)) + return results + + def status(self): + """Get status of all services.""" + return [service.get_status() for service in self.services] + +# Create manager and add services +manager = NotificationManager() +manager.add_service(EmailNotification()) +manager.add_service(SMSNotification()) +manager.add_service(PushNotification()) +manager.add_service(SlackNotification()) + +print("Service status:") +for status in manager.status(): + print(f" ✓ {status}") + +print("\nSending notification via all channels:") +for result in manager.send_all("user123", "Your order has been shipped!"): + print(f" {result}") diff --git a/09_advanced_python/01_list_comprehensions.py b/09_advanced_python/01_list_comprehensions.py new file mode 100644 index 0000000..4dd475e --- /dev/null +++ b/09_advanced_python/01_list_comprehensions.py @@ -0,0 +1,304 @@ +""" +================================================================================ +File: 01_list_comprehensions.py +Topic: List Comprehensions and Other Comprehensions +================================================================================ + +This file demonstrates comprehensions in Python - a concise and powerful way +to create lists, dictionaries, sets, and generators. Comprehensions are more +readable and often faster than traditional loops. + +Key Concepts: +- List comprehensions +- Dictionary comprehensions +- Set comprehensions +- Generator expressions +- Nested comprehensions +- When to use comprehensions + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Basic List Comprehension +# ----------------------------------------------------------------------------- +# [expression for item in iterable] + +print("--- Basic List Comprehension ---") + +# Traditional way +squares_loop = [] +for x in range(1, 6): + squares_loop.append(x ** 2) +print(f"Loop method: {squares_loop}") + +# List comprehension way +squares_comp = [x ** 2 for x in range(1, 6)] +print(f"Comprehension: {squares_comp}") + +# More examples +numbers = [1, 2, 3, 4, 5] +doubled = [n * 2 for n in numbers] +strings = [str(n) for n in numbers] + +print(f"\nOriginal: {numbers}") +print(f"Doubled: {doubled}") +print(f"As strings: {strings}") + +# ----------------------------------------------------------------------------- +# 2. List Comprehension with Condition +# ----------------------------------------------------------------------------- +# [expression for item in iterable if condition] + +print("\n--- Comprehension with Condition ---") + +# Get only even numbers +numbers = range(1, 21) +evens = [n for n in numbers if n % 2 == 0] +print(f"Even numbers: {evens}") + +# Filter and transform +words = ["hello", "world", "python", "AI", "ml"] +long_upper = [w.upper() for w in words if len(w) > 3] +print(f"Long words (uppercase): {long_upper}") + +# Multiple conditions (AND) +numbers = range(1, 51) +divisible_by_3_and_5 = [n for n in numbers if n % 3 == 0 and n % 5 == 0] +print(f"Divisible by 3 and 5: {divisible_by_3_and_5}") + +# ----------------------------------------------------------------------------- +# 3. Conditional Expression in Comprehension +# ----------------------------------------------------------------------------- +# [expr1 if condition else expr2 for item in iterable] + +print("\n--- Conditional Expression ---") + +# Replace negatives with zero +numbers = [-3, -1, 0, 2, 5, -4, 8] +non_negative = [n if n >= 0 else 0 for n in numbers] +print(f"Original: {numbers}") +print(f"Non-negative: {non_negative}") + +# Categorize numbers +categorized = ["even" if n % 2 == 0 else "odd" for n in range(1, 6)] +print(f"Categories: {categorized}") + +# Pass/fail based on score +scores = [85, 42, 91, 78, 55] +results = ["Pass" if s >= 60 else "Fail" for s in scores] +print(f"Scores: {scores}") +print(f"Results: {results}") + +# ----------------------------------------------------------------------------- +# 4. Nested Loops in Comprehension +# ----------------------------------------------------------------------------- + +print("\n--- Nested Loops ---") + +# Flatten a 2D list +matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +flat = [num for row in matrix for num in row] +print(f"Matrix: {matrix}") +print(f"Flattened: {flat}") + +# All combinations +colors = ["red", "green"] +sizes = ["S", "M", "L"] +combinations = [(color, size) for color in colors for size in sizes] +print(f"\nCombinations: {combinations}") + +# Multiplication table +table = [[i * j for j in range(1, 4)] for i in range(1, 4)] +print("\nMultiplication table:") +for row in table: + print(f" {row}") + +# ----------------------------------------------------------------------------- +# 5. Dictionary Comprehensions +# ----------------------------------------------------------------------------- +# {key: value for item in iterable} + +print("\n--- Dictionary Comprehensions ---") + +# Square numbers dictionary +squares_dict = {x: x ** 2 for x in range(1, 6)} +print(f"Squares dict: {squares_dict}") + +# From two lists +keys = ["name", "age", "city"] +values = ["Alice", 25, "NYC"] +person = {k: v for k, v in zip(keys, values)} +print(f"Person dict: {person}") + +# Swap keys and values +original = {"a": 1, "b": 2, "c": 3} +swapped = {v: k for k, v in original.items()} +print(f"Original: {original}") +print(f"Swapped: {swapped}") + +# Filter dictionary +scores = {"Alice": 85, "Bob": 42, "Charlie": 91, "Dave": 55} +passing = {name: score for name, score in scores.items() if score >= 60} +print(f"\nPasssing students: {passing}") + +# Transform dictionary +prices = {"apple": 1.5, "banana": 0.75, "cherry": 2.0} +taxed = {item: price * 1.1 for item, price in prices.items()} +print(f"With tax: {taxed}") + +# ----------------------------------------------------------------------------- +# 6. Set Comprehensions +# ----------------------------------------------------------------------------- +# {expression for item in iterable} + +print("\n--- Set Comprehensions ---") + +# Unique squares +numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] +unique_squares = {x ** 2 for x in numbers} +print(f"Numbers: {numbers}") +print(f"Unique squares: {unique_squares}") + +# Unique first letters +words = ["apple", "banana", "apricot", "blueberry", "cherry"] +first_letters = {w[0] for w in words} +print(f"First letters: {first_letters}") + +# Get unique word lengths +sentences = "the quick brown fox jumps over the lazy dog" +word_lengths = {len(word) for word in sentences.split()} +print(f"Unique word lengths: {word_lengths}") + +# ----------------------------------------------------------------------------- +# 7. Generator Expressions +# ----------------------------------------------------------------------------- +# (expression for item in iterable) - lazy evaluation! + +print("\n--- Generator Expressions ---") + +# Generator expression (note the parentheses) +squares_gen = (x ** 2 for x in range(1, 6)) +print(f"Generator object: {squares_gen}") +print(f"As list: {list(squares_gen)}") + +# Memory efficient for large data +# List: creates all values immediately +# Generator: creates values on demand + +# Sum of squares (generator is more memory efficient) +total = sum(x ** 2 for x in range(1, 1001)) +print(f"Sum of squares 1-1000: {total}") + +# Check if any/all +numbers = [1, 3, 5, 7, 9] +any_even = any(n % 2 == 0 for n in numbers) +all_positive = all(n > 0 for n in numbers) +print(f"\nNumbers: {numbers}") +print(f"Any even? {any_even}") +print(f"All positive? {all_positive}") + +# Find first match +names = ["Alice", "Bob", "Charlie", "Diana"] +first_long = next((name for name in names if len(name) > 5), None) +print(f"First name > 5 chars: {first_long}") + +# ----------------------------------------------------------------------------- +# 8. Nested Comprehensions +# ----------------------------------------------------------------------------- + +print("\n--- Nested Comprehensions ---") + +# Create a matrix +rows, cols = 3, 4 +matrix = [[0 for _ in range(cols)] for _ in range(rows)] +print(f"Zero matrix {rows}x{cols}: {matrix}") + +# Identity matrix +identity = [[1 if i == j else 0 for j in range(3)] for i in range(3)] +print("\nIdentity matrix:") +for row in identity: + print(f" {row}") + +# Transpose matrix +original = [[1, 2, 3], [4, 5, 6]] +transposed = [[row[i] for row in original] for i in range(len(original[0]))] +print(f"\nOriginal: {original}") +print(f"Transposed: {transposed}") + +# ----------------------------------------------------------------------------- +# 9. When NOT to Use Comprehensions +# ----------------------------------------------------------------------------- + +print("\n--- When NOT to Use Comprehensions ---") + +# 1. Complex logic - use regular loop +# BAD: +# result = [func1(x) if x > 0 else func2(x) if x < 0 else func3(x) for x in data if valid(x)] + +# GOOD: Use regular loop for complex logic +def process_numbers(data): + result = [] + for x in data: + if not x: # Skip None or 0 + continue + if x > 0: + result.append(x ** 2) + else: + result.append(abs(x)) + return result + +# 2. Side effects - use regular loop +# BAD: [print(x) for x in items] # Creates unnecessary list + +# GOOD: +# for x in items: +# print(x) + +# 3. Very long comprehensions that require wrapping +# Consider regular loop for readability + +print("Use regular loops when:") +print(" - Logic is complex") +print(" - There are side effects (print, modify, etc.)") +print(" - Comprehension becomes too long to read") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# 1. Parse CSV-like data +csv_data = "name,age,city\nAlice,25,NYC\nBob,30,LA\nCharlie,35,Chicago" +rows = [line.split(",") for line in csv_data.split("\n")] +print(f"Parsed CSV: {rows}") + +# 2. Filter and transform objects +users = [ + {"name": "Alice", "age": 25, "active": True}, + {"name": "Bob", "age": 17, "active": True}, + {"name": "Charlie", "age": 30, "active": False}, + {"name": "Diana", "age": 22, "active": True} +] +active_adults = [ + user["name"] + for user in users + if user["active"] and user["age"] >= 18 +] +print(f"\nActive adults: {active_adults}") + +# 3. Word frequency (dict comprehension) +text = "the quick brown fox jumps over the lazy dog" +words = text.split() +frequency = {word: words.count(word) for word in set(words)} +print(f"\nWord frequency: {frequency}") + +# 4. File path manipulation +files = ["report.pdf", "data.csv", "image.png", "document.pdf", "log.txt"] +pdf_files = [f for f in files if f.endswith(".pdf")] +print(f"\nPDF files: {pdf_files}") + +# 5. Coordinate pairs +coords = [(x, y) for x in range(3) for y in range(3) if x != y] +print(f"\nCoordinate pairs (x != y): {coords}") diff --git a/09_advanced_python/02_generators.py b/09_advanced_python/02_generators.py new file mode 100644 index 0000000..383fa59 --- /dev/null +++ b/09_advanced_python/02_generators.py @@ -0,0 +1,363 @@ +""" +================================================================================ +File: 02_generators.py +Topic: Generators and Iterators in Python +================================================================================ + +This file demonstrates generators and iterators in Python. Generators are +special functions that can pause and resume execution, yielding values one +at a time. They're memory-efficient for processing large data. + +Key Concepts: +- Iterator protocol (__iter__, __next__) +- Generator functions (yield) +- Generator expressions +- yield from +- Practical applications + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Understanding Iterators +# ----------------------------------------------------------------------------- +# Iterators implement __iter__ and __next__ + +print("--- Understanding Iterators ---") + +# Lists are iterable (have __iter__) +numbers = [1, 2, 3] +iterator = iter(numbers) # Get iterator + +print(f"First: {next(iterator)}") +print(f"Second: {next(iterator)}") +print(f"Third: {next(iterator)}") +# next(iterator) # Would raise StopIteration + +# Custom iterator class +class CountDown: + """A countdown iterator.""" + + def __init__(self, start): + self.current = start + + def __iter__(self): + return self + + def __next__(self): + if self.current <= 0: + raise StopIteration + value = self.current + self.current -= 1 + return value + +print("\nCountdown iterator:") +for num in CountDown(5): + print(f" {num}") + +# ----------------------------------------------------------------------------- +# 2. Generator Functions +# ----------------------------------------------------------------------------- +# Use yield to create generators + +print("\n--- Generator Functions ---") + +def countdown(n): + """Generator function for countdown.""" + while n > 0: + yield n # Pause here and return value + n -= 1 # Resume from here on next() + +# Using the generator +print("Generator countdown:") +for num in countdown(5): + print(f" {num}") + +# Generator is an iterator +gen = countdown(3) +print(f"\nGenerator object: {gen}") +print(f"next(): {next(gen)}") +print(f"next(): {next(gen)}") +print(f"next(): {next(gen)}") + +# ----------------------------------------------------------------------------- +# 3. Generators vs Lists - Memory Efficiency +# ----------------------------------------------------------------------------- + +print("\n--- Memory Efficiency ---") + +import sys + +# List - all values in memory at once +list_nums = [x ** 2 for x in range(1000)] +list_size = sys.getsizeof(list_nums) + +# Generator - values created on demand +gen_nums = (x ** 2 for x in range(1000)) +gen_size = sys.getsizeof(gen_nums) + +print(f"List of 1000 squares: {list_size:,} bytes") +print(f"Generator for 1000 squares: {gen_size} bytes") +print(f"Memory saved: {list_size - gen_size:,} bytes") + +# For very large sequences, generators are essential +# This would crash with a list: sum(range(10**12)) +# But works with generator: sum(range(10**6)) # Uses constant memory + +# ----------------------------------------------------------------------------- +# 4. Infinite Generators +# ----------------------------------------------------------------------------- + +print("\n--- Infinite Generators ---") + +def infinite_counter(start=0): + """Generate numbers infinitely.""" + n = start + while True: + yield n + n += 1 + +def fibonacci(): + """Generate Fibonacci numbers infinitely.""" + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + +# Use islice to take limited values from infinite generator +from itertools import islice + +print("First 10 counter values:") +counter = infinite_counter(10) +first_10 = list(islice(counter, 10)) +print(f" {first_10}") + +print("\nFirst 10 Fibonacci numbers:") +fib = fibonacci() +fib_10 = list(islice(fib, 10)) +print(f" {fib_10}") + +# ----------------------------------------------------------------------------- +# 5. Generator Methods +# ----------------------------------------------------------------------------- + +print("\n--- Generator Methods ---") + +def interactive_generator(): + """Generator that can receive values.""" + print(" Generator started") + + # yield can receive values via send() + received = yield "First yield" + print(f" Received: {received}") + + received = yield "Second yield" + print(f" Received: {received}") + + yield "Final yield" + +gen = interactive_generator() +print(f"next(): {next(gen)}") # Start and get first yield +print(f"send('Hello'): {gen.send('Hello')}") # Send value, get second yield +print(f"send('World'): {gen.send('World')}") # Send value, get final yield + +# ----------------------------------------------------------------------------- +# 6. yield from - Delegating Generators +# ----------------------------------------------------------------------------- + +print("\n--- yield from ---") + +def sub_generator(): + """A simple sub-generator.""" + yield 1 + yield 2 + yield 3 + +# Without yield from +def main_generator_old(): + for item in sub_generator(): + yield item + yield 4 + yield 5 + +# With yield from (cleaner) +def main_generator(): + yield from sub_generator() + yield 4 + yield 5 + +print("Using yield from:") +result = list(main_generator()) +print(f" {result}") + +# Flatten nested structure with yield from +def flatten(nested): + """Flatten arbitrarily nested lists.""" + for item in nested: + if isinstance(item, list): + yield from flatten(item) + else: + yield item + +nested = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]] +print(f"\nFlattening: {nested}") +print(f"Result: {list(flatten(nested))}") + +# ----------------------------------------------------------------------------- +# 7. Generator Pipelines +# ----------------------------------------------------------------------------- + +print("\n--- Generator Pipelines ---") + +# Process data through multiple generators (like Unix pipes) + +def read_data(): + """Simulate reading data.""" + data = [" John,25,NYC ", " Jane,30,LA ", " Bob,35,Chicago ", ""] + for line in data: + yield line + +def strip_lines(lines): + """Remove whitespace from lines.""" + for line in lines: + yield line.strip() + +def filter_empty(lines): + """Filter out empty lines.""" + for line in lines: + if line: + yield line + +def parse_csv(lines): + """Parse CSV lines into dictionaries.""" + for line in lines: + name, age, city = line.split(",") + yield {"name": name, "age": int(age), "city": city} + +# Chain generators together +pipeline = parse_csv(filter_empty(strip_lines(read_data()))) + +print("Pipeline processing:") +for record in pipeline: + print(f" {record}") + +# ----------------------------------------------------------------------------- +# 8. Context Manager with Generator +# ----------------------------------------------------------------------------- + +print("\n--- Generator as Context Manager ---") + +from contextlib import contextmanager + +@contextmanager +def managed_resource(name): + """A context manager using generator.""" + print(f" Acquiring: {name}") + try: + yield name # Resource is available here + finally: + print(f" Releasing: {name}") + +# Using the context manager +print("Using managed resource:") +with managed_resource("Database Connection") as conn: + print(f" Using: {conn}") + +# ----------------------------------------------------------------------------- +# 9. Built-in Generator Functions +# ----------------------------------------------------------------------------- + +print("\n--- Built-in Generator Functions ---") + +# range() is a generator-like object +print(f"range(5): {list(range(5))}") + +# enumerate() yields (index, value) +fruits = ["apple", "banana", "cherry"] +print(f"\nenumerate(): {list(enumerate(fruits))}") + +# zip() yields tuples from multiple iterables +names = ["Alice", "Bob"] +ages = [25, 30] +print(f"zip(): {list(zip(names, ages))}") + +# map() yields transformed values +print(f"map(str.upper): {list(map(str.upper, fruits))}") + +# filter() yields filtered values +numbers = [1, 2, 3, 4, 5, 6] +print(f"filter(even): {list(filter(lambda x: x % 2 == 0, numbers))}") + +# itertools has many useful generators +from itertools import chain, cycle, repeat, takewhile + +# chain - combine iterables +print(f"\nchain([1,2], [3,4]): {list(chain([1,2], [3,4]))}") + +# takewhile - yield while condition is true +nums = [2, 4, 6, 7, 8, 10] +print(f"takewhile(even): {list(takewhile(lambda x: x % 2 == 0, nums))}") + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# 1. Reading large files line by line +def read_large_file(filepath): + """Read file lazily, one line at a time.""" + # In real code: yield from open(filepath) + # Here we simulate: + lines = ["Line 1", "Line 2", "Line 3"] + for line in lines: + yield line + +print("Reading 'file' lazily:") +for line in read_large_file("data.txt"): + print(f" Processing: {line}") + +# 2. Paginated API results +def fetch_paginated_data(total_pages=3): + """Simulate fetching paginated API data.""" + for page in range(1, total_pages + 1): + print(f" Fetching page {page}...") + yield [f"item_{page}_{i}" for i in range(3)] + +print("\nPaginated data:") +for items in fetch_paginated_data(): + for item in items: + print(f" {item}") + +# 3. Sliding window +def sliding_window(iterable, size): + """Generate sliding windows over data.""" + from collections import deque + window = deque(maxlen=size) + + for item in iterable: + window.append(item) + if len(window) == size: + yield tuple(window) + +data = [1, 2, 3, 4, 5, 6] +print(f"\nSliding window (size 3) over {data}:") +for window in sliding_window(data, 3): + print(f" {window}") + +# 4. Batch processing +def batch(iterable, size): + """Yield items in batches.""" + batch = [] + for item in iterable: + batch.append(item) + if len(batch) == size: + yield batch + batch = [] + if batch: + yield batch + +items = list(range(1, 11)) +print(f"\nBatching {items} into groups of 3:") +for b in batch(items, 3): + print(f" {b}") diff --git a/09_advanced_python/03_decorators.py b/09_advanced_python/03_decorators.py new file mode 100644 index 0000000..13ced1a --- /dev/null +++ b/09_advanced_python/03_decorators.py @@ -0,0 +1,421 @@ +""" +================================================================================ +File: 03_decorators.py +Topic: Decorators in Python +================================================================================ + +This file demonstrates decorators in Python. Decorators are a powerful feature +that allows you to modify or enhance functions without changing their code. +They use the @ syntax and are widely used for logging, authentication, caching, +and more. + +Key Concepts: +- Function as first-class objects +- Simple decorators +- Decorators with arguments +- Preserving function metadata +- Class decorators +- Built-in decorators + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. Functions as First-Class Objects +# ----------------------------------------------------------------------------- +# Functions can be assigned, passed, and returned + +print("--- Functions as First-Class Objects ---") + +def greet(name): + return f"Hello, {name}!" + +# Assign to variable +say_hello = greet +print(say_hello("World")) + +# Pass as argument +def apply_func(func, value): + return func(value) + +print(apply_func(greet, "Python")) + +# Return from function +def get_greeter(): + def inner_greet(name): + return f"Hi, {name}!" + return inner_greet + +greeter = get_greeter() +print(greeter("Alice")) + +# ----------------------------------------------------------------------------- +# 2. Simple Decorator +# ----------------------------------------------------------------------------- +# A decorator wraps a function to add behavior + +print("\n--- Simple Decorator ---") + +def my_decorator(func): + """A simple decorator.""" + def wrapper(*args, **kwargs): + print(" Before function call") + result = func(*args, **kwargs) + print(" After function call") + return result + return wrapper + +# Applying decorator manually +def say_hello_v1(name): + print(f" Hello, {name}!") + +decorated = my_decorator(say_hello_v1) +decorated("World") + +# Using @ syntax (syntactic sugar) +@my_decorator +def say_hello_v2(name): + print(f" Hello, {name}!") + +print("\nWith @ syntax:") +say_hello_v2("Python") + +# ----------------------------------------------------------------------------- +# 3. Preserving Function Metadata +# ----------------------------------------------------------------------------- +# Use functools.wraps to preserve original function info + +print("\n--- Preserving Metadata ---") + +from functools import wraps + +def better_decorator(func): + """Decorator that preserves function metadata.""" + @wraps(func) # Preserve original function's metadata + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + +@better_decorator +def example_function(): + """This is the docstring.""" + pass + +print(f"Function name: {example_function.__name__}") +print(f"Function docstring: {example_function.__doc__}") + +# Without @wraps, these would show 'wrapper' info instead + +# ----------------------------------------------------------------------------- +# 4. Practical Decorators +# ----------------------------------------------------------------------------- + +print("\n--- Practical Decorators ---") + +import time + +# Timing decorator +def timer(func): + """Measure function execution time.""" + @wraps(func) + def wrapper(*args, **kwargs): + start = time.time() + result = func(*args, **kwargs) + end = time.time() + print(f" {func.__name__} took {end - start:.4f} seconds") + return result + return wrapper + +@timer +def slow_function(): + """A deliberately slow function.""" + time.sleep(0.1) + return "Done" + +result = slow_function() + +# Logging decorator +def logger(func): + """Log function calls.""" + @wraps(func) + def wrapper(*args, **kwargs): + args_str = ", ".join(map(repr, args)) + kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) + all_args = ", ".join(filter(None, [args_str, kwargs_str])) + print(f" Calling {func.__name__}({all_args})") + result = func(*args, **kwargs) + print(f" {func.__name__} returned {result!r}") + return result + return wrapper + +@logger +def add(a, b): + return a + b + +print("\nLogger example:") +add(3, 5) + +# ----------------------------------------------------------------------------- +# 5. Decorators with Arguments +# ----------------------------------------------------------------------------- +# Create configurable decorators with an extra layer + +print("\n--- Decorators with Arguments ---") + +def repeat(times): + """Decorator to repeat function calls.""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + results = [] + for _ in range(times): + results.append(func(*args, **kwargs)) + return results + return wrapper + return decorator + +@repeat(times=3) +def greet_user(name): + print(f" Hello, {name}!") + return f"Greeted {name}" + +print("Repeat decorator:") +results = greet_user("Alice") +print(f"Results: {results}") + +# Retry decorator with custom attempts +def retry(max_attempts=3, exceptions=(Exception,)): + """Retry function on failure.""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + for attempt in range(1, max_attempts + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + print(f" Attempt {attempt} failed: {e}") + if attempt == max_attempts: + raise + return wrapper + return decorator + +attempt_count = 0 + +@retry(max_attempts=3) +def unstable_function(): + global attempt_count + attempt_count += 1 + if attempt_count < 3: + raise ValueError("Not ready yet!") + return "Success!" + +print("\nRetry decorator:") +try: + result = unstable_function() + print(f" Final result: {result}") +except ValueError: + print(" All attempts failed") + +# ----------------------------------------------------------------------------- +# 6. Multiple Decorators +# ----------------------------------------------------------------------------- +# Decorators stack from bottom to top + +print("\n--- Multiple Decorators ---") + +def bold(func): + @wraps(func) + def wrapper(*args, **kwargs): + return f"{func(*args, **kwargs)}" + return wrapper + +def italic(func): + @wraps(func) + def wrapper(*args, **kwargs): + return f"{func(*args, **kwargs)}" + return wrapper + +@bold +@italic +def greet_html(name): + return f"Hello, {name}" + +# Applied bottom-up: italic first, then bold +# Same as: bold(italic(greet_html)) +print(f"Stacked decorators: {greet_html('World')}") + +# ----------------------------------------------------------------------------- +# 7. Class-Based Decorators +# ----------------------------------------------------------------------------- +# Use a class as a decorator + +print("\n--- Class-Based Decorators ---") + +class CallCounter: + """Decorator class to count function calls.""" + + def __init__(self, func): + self.func = func + self.count = 0 + # Preserve function attributes + self.__name__ = func.__name__ + self.__doc__ = func.__doc__ + + def __call__(self, *args, **kwargs): + self.count += 1 + print(f" {self.func.__name__} has been called {self.count} time(s)") + return self.func(*args, **kwargs) + +@CallCounter +def hello(): + """Say hello.""" + print(" Hello!") + +hello() +hello() +hello() +print(f"Total calls: {hello.count}") + +# ----------------------------------------------------------------------------- +# 8. Built-in Decorators +# ----------------------------------------------------------------------------- + +print("\n--- Built-in Decorators ---") + +# @property - create getter/setter +class Circle: + def __init__(self, radius): + self._radius = radius + + @property + def radius(self): + return self._radius + + @radius.setter + def radius(self, value): + if value < 0: + raise ValueError("Radius cannot be negative") + self._radius = value + + @property + def area(self): + import math + return math.pi * self._radius ** 2 + +circle = Circle(5) +print(f"Circle radius: {circle.radius}") +print(f"Circle area: {circle.area:.2f}") + +# @staticmethod - method without self +class Math: + @staticmethod + def add(a, b): + return a + b + +print(f"\n@staticmethod: Math.add(3, 4) = {Math.add(3, 4)}") + +# @classmethod - method with cls instead of self +class Counter: + count = 0 + + def __init__(self): + Counter.count += 1 + + @classmethod + def get_count(cls): + return cls.count + +c1, c2, c3 = Counter(), Counter(), Counter() +print(f"@classmethod: Counter.get_count() = {Counter.get_count()}") + +# @functools.lru_cache - memoization +from functools import lru_cache + +@lru_cache(maxsize=128) +def fibonacci(n): + if n < 2: + return n + return fibonacci(n-1) + fibonacci(n-2) + +print(f"\n@lru_cache: fibonacci(30) = {fibonacci(30)}") +print(f"Cache info: {fibonacci.cache_info()}") + +# ----------------------------------------------------------------------------- +# 9. Decorator for Methods +# ----------------------------------------------------------------------------- + +print("\n--- Decorating Methods ---") + +def debug_method(func): + """Debug decorator for class methods.""" + @wraps(func) + def wrapper(self, *args, **kwargs): + class_name = self.__class__.__name__ + print(f" {class_name}.{func.__name__} called") + return func(self, *args, **kwargs) + return wrapper + +class Calculator: + @debug_method + def add(self, a, b): + return a + b + + @debug_method + def multiply(self, a, b): + return a * b + +calc = Calculator() +calc.add(3, 5) +calc.multiply(4, 6) + +# ----------------------------------------------------------------------------- +# 10. Practical Examples +# ----------------------------------------------------------------------------- + +print("\n--- Practical Examples ---") + +# 1. Authorization decorator +def require_auth(func): + """Check if user is authenticated.""" + @wraps(func) + def wrapper(user, *args, **kwargs): + if not user.get("authenticated", False): + raise PermissionError("Authentication required") + return func(user, *args, **kwargs) + return wrapper + +@require_auth +def get_secret_data(user): + return f"Secret data for {user['name']}" + +print("Authorization decorator:") +try: + result = get_secret_data({"name": "Guest", "authenticated": False}) +except PermissionError as e: + print(f" Denied: {e}") + +result = get_secret_data({"name": "Admin", "authenticated": True}) +print(f" Allowed: {result}") + +# 2. Validation decorator +def validate_positive(func): + """Ensure all numeric arguments are positive.""" + @wraps(func) + def wrapper(*args, **kwargs): + for arg in args: + if isinstance(arg, (int, float)) and arg < 0: + raise ValueError(f"Negative value not allowed: {arg}") + return func(*args, **kwargs) + return wrapper + +@validate_positive +def calculate_area(width, height): + return width * height + +print("\nValidation decorator:") +try: + calculate_area(-5, 10) +except ValueError as e: + print(f" Validation error: {e}") + +print(f" Valid call: {calculate_area(5, 10)}") diff --git a/09_advanced_python/04_context_managers.py b/09_advanced_python/04_context_managers.py new file mode 100644 index 0000000..01ce22b --- /dev/null +++ b/09_advanced_python/04_context_managers.py @@ -0,0 +1,382 @@ +""" +================================================================================ +File: 04_context_managers.py +Topic: Context Managers in Python +================================================================================ + +This file demonstrates context managers in Python. Context managers are objects +that define setup and cleanup actions, typically used with the 'with' statement. +They ensure resources are properly managed even when errors occur. + +Key Concepts: +- The 'with' statement +- __enter__ and __exit__ methods +- contextlib module +- File handling +- Resource management patterns + +================================================================================ +""" + +# ----------------------------------------------------------------------------- +# 1. The 'with' Statement +# ----------------------------------------------------------------------------- +# Ensures cleanup happens automatically + +print("--- The 'with' Statement ---") + +# Most common use: file handling +# Without 'with' (error-prone) +""" +file = open('example.txt', 'w') +try: + file.write('Hello') +finally: + file.close() # Must remember to close! +""" + +# With 'with' (automatic cleanup) +""" +with open('example.txt', 'w') as file: + file.write('Hello') +# File is automatically closed here +""" + +print("The 'with' statement:") +print(" - Automatically handles setup and cleanup") +print(" - Ensures cleanup even if errors occur") +print(" - Cleaner, more readable code") + +# ----------------------------------------------------------------------------- +# 2. Creating a Context Manager Class +# ----------------------------------------------------------------------------- +# Implement __enter__ and __exit__ + +print("\n--- Context Manager Class ---") + +class ManagedFile: + """A custom context manager for file handling.""" + + def __init__(self, filename, mode='r'): + self.filename = filename + self.mode = mode + self.file = None + + def __enter__(self): + """Called when entering 'with' block.""" + print(f" Opening file: {self.filename}") + # In real code: self.file = open(self.filename, self.mode) + self.file = f"" + return self.file # This is assigned to the 'as' variable + + def __exit__(self, exc_type, exc_val, exc_tb): + """Called when exiting 'with' block.""" + print(f" Closing file: {self.filename}") + # In real code: self.file.close() + + # exc_type, exc_val, exc_tb contain exception info if an error occurred + if exc_type is not None: + print(f" Exception occurred: {exc_type.__name__}: {exc_val}") + + # Return True to suppress the exception, False to propagate + return False + +# Using the context manager +print("Normal usage:") +with ManagedFile("data.txt", "w") as f: + print(f" Working with: {f}") + +print("\nWith exception:") +try: + with ManagedFile("data.txt") as f: + print(f" Working with: {f}") + raise ValueError("Something went wrong!") +except ValueError: + print(" Exception was propagated") + +# ----------------------------------------------------------------------------- +# 3. Context Manager with Exception Suppression +# ----------------------------------------------------------------------------- + +print("\n--- Suppressing Exceptions ---") + +class SuppressErrors: + """Context manager that suppresses specified exceptions.""" + + def __init__(self, *exceptions): + self.exceptions = exceptions + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None and issubclass(exc_type, self.exceptions): + print(f" Suppressed: {exc_type.__name__}: {exc_val}") + return True # Suppress the exception + return False # Don't suppress + +# Using the suppressor +print("Suppressing ValueError:") +with SuppressErrors(ValueError, TypeError): + print(" Before error") + raise ValueError("This will be suppressed") + print(" After error") # Never reached + +print(" Code continues after 'with' block") + +# ----------------------------------------------------------------------------- +# 4. Using contextlib +# ----------------------------------------------------------------------------- +# Create context managers without classes + +print("\n--- Using contextlib ---") + +from contextlib import contextmanager + +@contextmanager +def managed_resource(name): + """A context manager using @contextmanager decorator.""" + print(f" Acquiring: {name}") + try: + yield name # Control is passed to 'with' block here + finally: + print(f" Releasing: {name}") + +print("@contextmanager decorator:") +with managed_resource("Database") as resource: + print(f" Using: {resource}") + +# Timer context manager +import time + +@contextmanager +def timer(description="Operation"): + """Time the enclosed code block.""" + start = time.time() + try: + yield + finally: + elapsed = time.time() - start + print(f" {description} took {elapsed:.4f} seconds") + +print("\nTimer context manager:") +with timer("Sleep operation"): + time.sleep(0.1) + +# ----------------------------------------------------------------------------- +# 5. Multiple Context Managers +# ----------------------------------------------------------------------------- + +print("\n--- Multiple Context Managers ---") + +@contextmanager +def open_mock_file(name): + """Mock file context manager.""" + print(f" Opening: {name}") + yield f"<{name}>" + print(f" Closing: {name}") + +# Multiple context managers in one 'with' +print("Multiple context managers:") +with open_mock_file("input.txt") as infile, \ + open_mock_file("output.txt") as outfile: + print(f" Reading from: {infile}") + print(f" Writing to: {outfile}") + +# Python 3.10+ allows parenthesized context managers +""" +with ( + open_mock_file("input.txt") as infile, + open_mock_file("output.txt") as outfile +): + ... +""" + +# ----------------------------------------------------------------------------- +# 6. Practical Context Managers +# ----------------------------------------------------------------------------- + +print("\n--- Practical Context Managers ---") + +# 1. Database connection (mock) +@contextmanager +def database_connection(host): + """Mock database connection.""" + print(f" Connecting to {host}...") + connection = {"host": host, "connected": True} + try: + yield connection + finally: + connection["connected"] = False + print(f" Disconnected from {host}") + +print("Database connection:") +with database_connection("localhost") as db: + print(f" Connected: {db['connected']}") + # Perform database operations + +# 2. Temporary working directory +import os +from contextlib import contextmanager + +@contextmanager +def temp_directory_context(): + """Change to temp directory and restore.""" + original_dir = os.getcwd() + temp_dir = os.path.dirname(original_dir) if original_dir else original_dir + try: + # In real code: os.chdir(temp_dir) + print(f" Changed to: {temp_dir}") + yield temp_dir + finally: + # os.chdir(original_dir) + print(f" Restored to: {original_dir}") + +print("\nDirectory change:") +with temp_directory_context(): + print(" Working in temp directory") + +# 3. Lock context manager +@contextmanager +def locked(lock_name="default"): + """Mock lock context manager.""" + print(f" Acquiring lock: {lock_name}") + try: + yield + finally: + print(f" Releasing lock: {lock_name}") + +print("\nLocking:") +with locked("resource_lock"): + print(" Critical section") + +# ----------------------------------------------------------------------------- +# 7. contextlib Utilities +# ----------------------------------------------------------------------------- + +print("\n--- contextlib Utilities ---") + +from contextlib import closing, suppress + +# closing() - for objects with close() but no __exit__ +class Connection: + def close(self): + print(" Connection closed") + +print("using closing():") +with closing(Connection()) as conn: + print(" Using connection") + +# suppress() - ignore specified exceptions +print("\nsuppress() example:") +from contextlib import suppress + +# Instead of try/except for simple cases +with suppress(FileNotFoundError, KeyError): + # This would normally raise an error + print(" Attempting risky operations...") + # raise FileNotFoundError("No such file") +print(" Continued after suppress") + +# nullcontext() - do-nothing context manager (Python 3.7+) +from contextlib import nullcontext + +def process_data(data, lock=None): + """Process with optional lock.""" + with lock if lock else nullcontext(): + print(f" Processing: {data}") + +print("\nnullcontext() example:") +process_data("data1") # No lock +process_data("data2", locked("my_lock")) # With lock + +# ----------------------------------------------------------------------------- +# 8. Async Context Managers +# ----------------------------------------------------------------------------- + +print("\n--- Async Context Managers ---") + +# For async code, use __aenter__ and __aexit__ +async_example = ''' +class AsyncResource: + async def __aenter__(self): + print("Acquiring async resource...") + await asyncio.sleep(0.1) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + print("Releasing async resource...") + await asyncio.sleep(0.1) + return False + +# Usage: +async with AsyncResource() as resource: + print("Using resource") +''' + +print("Async context managers use __aenter__ and __aexit__") +print("Use 'async with' statement") + +# ----------------------------------------------------------------------------- +# 9. Reentrant and Reusable Context Managers +# ----------------------------------------------------------------------------- + +print("\n--- Reentrant Context Managers ---") + +# A context manager that can be used multiple times +class ReusableContext: + """Context manager that can be reused.""" + + def __init__(self, name): + self.name = name + self.uses = 0 + + def __enter__(self): + self.uses += 1 + print(f" Entering {self.name} (use #{self.uses})") + return self + + def __exit__(self, *args): + print(f" Exiting {self.name}") + return False + +ctx = ReusableContext("MyContext") + +print("Reusing context manager:") +with ctx: + print(" First use") + +with ctx: + print(" Second use") + +print(f"Total uses: {ctx.uses}") + +# ----------------------------------------------------------------------------- +# 10. Real-World Pattern: Configuration Override +# ----------------------------------------------------------------------------- + +print("\n--- Real-World: Config Override ---") + +class Config: + """Application configuration.""" + settings = {"debug": False, "log_level": "INFO"} + + @classmethod + @contextmanager + def override(cls, **overrides): + """Temporarily override configuration.""" + original = cls.settings.copy() + try: + cls.settings.update(overrides) + print(f" Config overridden: {overrides}") + yield cls.settings + finally: + cls.settings = original + print(" Config restored") + +print(f"Original config: {Config.settings}") + +with Config.override(debug=True, log_level="DEBUG") as settings: + print(f" Inside 'with': {settings}") + +print(f"After 'with': {Config.settings}") diff --git a/10_best_practices/01_pep8.py b/10_best_practices/01_pep8.py new file mode 100644 index 0000000..fc13d2c --- /dev/null +++ b/10_best_practices/01_pep8.py @@ -0,0 +1,334 @@ +""" +================================================================================ +File: 01_pep8.py +Topic: PEP 8 - Python Style Guide +================================================================================ + +This file demonstrates PEP 8, the official Python style guide. Following PEP 8 +makes your code more readable, consistent, and professional. These are +conventions, not strict rules, but following them is highly recommended. + +Key Concepts: +- Indentation and whitespace +- Naming conventions +- Line length and wrapping +- Imports organization +- Comments and docstrings + +Reference: https://peps.python.org/pep-0008/ + +================================================================================ +""" + +# ============================================================================= +# 1. INDENTATION +# ============================================================================= +# Use 4 spaces per indentation level. Never mix tabs and spaces. + +# GOOD +def function_with_proper_indentation(): + if True: + for i in range(10): + print(i) + +# Aligned with opening delimiter +foo = long_function_name(var_one, var_two, + var_three, var_four) + +# Hanging indent (add a level) +def long_function_name( + var_one, var_two, var_three, + var_four): + print(var_one) + +# ============================================================================= +# 2. LINE LENGTH +# ============================================================================= +# Limit lines to 79 characters (72 for docstrings/comments) + +# GOOD - Use implicit line continuation +total = (first_variable + + second_variable + + third_variable) + +# GOOD - Use backslash when necessary +with open('/very/long/path/to/file.txt') as file_one, \ + open('/another/long/path/to/file.txt') as file_two: + pass + +# ============================================================================= +# 3. BLANK LINES +# ============================================================================= +# - 2 blank lines around top-level functions and classes +# - 1 blank line between methods in a class + + +class FirstClass: + """First class.""" + pass + + +class SecondClass: + """Second class.""" + + def method_one(self): + """First method.""" + pass + + def method_two(self): + """Second method.""" + pass + + +def top_level_function(): + """A top-level function.""" + pass + + +# ============================================================================= +# 4. IMPORTS +# ============================================================================= +# - One import per line +# - Group in order: standard library, third-party, local +# - Use absolute imports + +# GOOD +import os +import sys +from typing import List, Optional + +# Third party imports (after blank line) +# import numpy as np +# import pandas as pd + +# Local imports (after blank line) +# from mypackage import mymodule + +# BAD - Multiple imports on one line +# import os, sys + +# ============================================================================= +# 5. WHITESPACE +# ============================================================================= + +# GOOD - No extra whitespace inside brackets +spam = [1, 2, 3] +ham = {"key": "value"} +eggs = (1,) + +# BAD +# spam = [ 1, 2, 3 ] +# ham = { 'key': 'value' } + +# GOOD - One space around operators +x = 1 +y = 2 +z = x + y + +# BAD - No space around = in keyword arguments +# def function(x, y = 5): +def function(x, y=5): + pass + +# GOOD - Space after comma +some_list = [1, 2, 3] + +# GOOD - No space before colon in slices +some_list[1:3] +some_list[::2] + +# ============================================================================= +# 6. NAMING CONVENTIONS +# ============================================================================= + +# Variables and functions: lowercase_with_underscores (snake_case) +user_name = "John" +total_count = 42 + +def calculate_average(numbers): + return sum(numbers) / len(numbers) + +# Constants: UPPERCASE_WITH_UNDERSCORES +MAX_BUFFER_SIZE = 4096 +DEFAULT_TIMEOUT = 30 +PI = 3.14159 + +# Classes: CapitalizedWords (PascalCase) +class UserAccount: + pass + +class HttpConnection: + pass + +# Private: prefix with underscore +_internal_variable = "private" +def _internal_function(): + pass + +class MyClass: + def _protected_method(self): + """Single underscore = protected (convention).""" + pass + + def __private_method(self): + """Double underscore = name mangling (truly private).""" + pass + +# ============================================================================= +# 7. COMMENTS +# ============================================================================= + +# GOOD - Inline comments have at least 2 spaces before # +x = 5 # This is an inline comment + +# Block comments precede the code they describe +# This is a block comment that explains +# the following piece of code. +complex_calculation = 1 + 2 + 3 + +# Don't state the obvious +# BAD: x = 5 # Assign 5 to x +# GOOD: x = 5 # Default timeout in seconds + +# ============================================================================= +# 8. DOCSTRINGS +# ============================================================================= + +def example_function(param1, param2): + """ + Brief description of the function. + + Longer description if needed. Explain what the function + does, not how it does it. + + Args: + param1: Description of first parameter + param2: Description of second parameter + + Returns: + Description of what is returned + + Raises: + ValueError: When param1 is negative + + Example: + >>> example_function(1, 2) + 3 + """ + return param1 + param2 + + +class ExampleClass: + """Brief description of the class. + + Longer description of the class including its purpose + and usage patterns. + + Attributes: + attr1: Description of first attribute + attr2: Description of second attribute + """ + + def __init__(self, attr1, attr2): + """Initialize ExampleClass.""" + self.attr1 = attr1 + self.attr2 = attr2 + +# ============================================================================= +# 9. COMPARISON AND BOOLEAN +# ============================================================================= + +# Use 'is' and 'is not' for None comparisons +# GOOD +x = None +if x is None: + pass + +# BAD +# if x == None: + +# Use 'is not' instead of 'not ... is' +# GOOD +if x is not None: + pass + +# BAD +# if not x is None: + +# Don't compare boolean values with == or != +# GOOD +flag = True +if flag: + pass + +# BAD +# if flag == True: +# if flag is True: # Only use when testing identity + +# ============================================================================= +# 10. EXCEPTION HANDLING +# ============================================================================= + +# Catch specific exceptions +# GOOD +try: + value = int(user_input) +except ValueError: + print("Invalid input") + +# BAD - Too broad +# try: +# value = int(user_input) +# except: # or except Exception: +# print("Error") + +# Use 'raise' to re-raise exception +try: + process_data() +except ValueError: + logger.error("Bad value") + raise + +# ============================================================================= +# 11. FUNCTION ANNOTATIONS (Type Hints) +# ============================================================================= + +def greeting(name: str) -> str: + """Return a greeting.""" + return f"Hello, {name}!" + +# Complex types +from typing import List, Dict, Optional + +def process_items( + items: List[str], + config: Dict[str, int], + default: Optional[str] = None +) -> bool: + """Process items with configuration.""" + return True + +# ============================================================================= +# 12. SUMMARY: KEY RULES +# ============================================================================= + +print("PEP 8 Summary - Key Rules:") +print(""" +1. Use 4 spaces for indentation +2. Limit lines to 79 characters +3. Use blank lines to separate functions and classes +4. Organize imports: standard lib, third-party, local +5. Use snake_case for functions and variables +6. Use PascalCase for classes +7. Use UPPER_CASE for constants +8. Use spaces around operators and after commas +9. Write docstrings for all public modules, functions, classes +10. Compare with 'is' for None, use bool directly +""") + +# Use flake8 or black to automatically check/format code +print("Tools to help with PEP 8:") +print(" - flake8: Check for PEP 8 violations") +print(" - black: Automatic code formatting") +print(" - isort: Sort imports automatically") +print(" - pylint: Comprehensive code analysis") diff --git a/10_best_practices/02_type_hinting.py b/10_best_practices/02_type_hinting.py new file mode 100644 index 0000000..7e2eb65 --- /dev/null +++ b/10_best_practices/02_type_hinting.py @@ -0,0 +1,392 @@ +""" +================================================================================ +File: 02_type_hinting.py +Topic: Type Hints in Python +================================================================================ + +This file demonstrates type hints (type annotations) in Python. Type hints make +code more readable, enable better IDE support, and can catch bugs early using +static type checkers like mypy. + +Key Concepts: +- Basic type hints +- typing module (List, Dict, Optional, etc.) +- Function annotations +- Class type hints +- Generics +- Type checking tools + +Note: Type hints are OPTIONAL and don't affect runtime behavior! + +================================================================================ +""" + +# ============================================================================= +# 1. BASIC TYPE HINTS +# ============================================================================= +# Syntax: variable: Type = value + +print("--- Basic Type Hints ---") + +# Simple types +name: str = "Alice" +age: int = 25 +height: float = 1.75 +is_active: bool = True + +print(f"name: str = '{name}'") +print(f"age: int = {age}") +print(f"height: float = {height}") +print(f"is_active: bool = {is_active}") + +# Type hints are just hints - they don't enforce types at runtime! +# This works but is wrong (would be caught by type checker): +# age: int = "not an int" + +# ============================================================================= +# 2. FUNCTION TYPE HINTS +# ============================================================================= +# Parameters and return types + +print("\n--- Function Type Hints ---") + +def greet(name: str) -> str: + """Return a greeting string.""" + return f"Hello, {name}!" + +def add_numbers(a: int, b: int) -> int: + """Add two integers.""" + return a + b + +def is_even(n: int) -> bool: + """Check if number is even.""" + return n % 2 == 0 + +# Function with no return value +def print_message(message: str) -> None: + """Print a message (returns None).""" + print(message) + +print(f"greet('World'): {greet('World')}") +print(f"add_numbers(3, 5): {add_numbers(3, 5)}") +print(f"is_even(4): {is_even(4)}") + +# ============================================================================= +# 3. TYPING MODULE - COLLECTION TYPES +# ============================================================================= + +print("\n--- Collection Types ---") + +from typing import List, Dict, Set, Tuple + +# List of specific type +def process_numbers(numbers: List[int]) -> int: + """Sum a list of integers.""" + return sum(numbers) + +# Dictionary with key and value types +def get_user_ages(users: Dict[str, int]) -> List[str]: + """Get names of users older than 18.""" + return [name for name, age in users.items() if age > 18] + +# Set of specific type +def get_unique_words(text: str) -> Set[str]: + """Get unique words from text.""" + return set(text.lower().split()) + +# Tuple with specific types (fixed length) +def get_point() -> Tuple[float, float]: + """Return an (x, y) coordinate.""" + return (1.5, 2.5) + +# Tuple with variable length of same type +def get_scores() -> Tuple[int, ...]: + """Return any number of scores.""" + return (85, 90, 78, 92) + +# Examples +print(f"process_numbers([1,2,3]): {process_numbers([1, 2, 3])}") +print(f"get_unique_words('hello hello world'): {get_unique_words('hello hello world')}") +print(f"get_point(): {get_point()}") + +# ============================================================================= +# 4. OPTIONAL AND UNION TYPES +# ============================================================================= + +print("\n--- Optional and Union ---") + +from typing import Optional, Union + +# Optional = can be None +def find_user(user_id: int) -> Optional[str]: + """Find user by ID, return None if not found.""" + users = {1: "Alice", 2: "Bob"} + return users.get(user_id) # Returns None if not found + +# Union = can be one of several types +def process_input(value: Union[int, str]) -> str: + """Process either int or string input.""" + if isinstance(value, int): + return f"Number: {value}" + return f"String: {value}" + +# Python 3.10+ syntax: X | Y instead of Union[X, Y] +# def process_input(value: int | str) -> str: + +print(f"find_user(1): {find_user(1)}") +print(f"find_user(99): {find_user(99)}") +print(f"process_input(42): {process_input(42)}") +print(f"process_input('hello'): {process_input('hello')}") + +# ============================================================================= +# 5. SPECIAL TYPES +# ============================================================================= + +print("\n--- Special Types ---") + +from typing import Any, Callable, Sequence, Iterable + +# Any - accepts any type (avoid when possible) +def log_value(value: Any) -> None: + """Log any value.""" + print(f" Logged: {value}") + +log_value(42) +log_value("hello") +log_value([1, 2, 3]) + +# Callable - function type +def apply_function(func: Callable[[int, int], int], a: int, b: int) -> int: + """Apply a function to two numbers.""" + return func(a, b) + +print(f"\napply_function(lambda x,y: x+y, 3, 4): {apply_function(lambda x, y: x + y, 3, 4)}") + +# Sequence - any ordered collection (list, tuple, str) +def get_first(items: Sequence[int]) -> int: + """Get first item from sequence.""" + return items[0] + +# Iterable - anything you can iterate over +def count_items(items: Iterable[str]) -> int: + """Count items in iterable.""" + return sum(1 for _ in items) + +# ============================================================================= +# 6. TYPE ALIASES +# ============================================================================= + +print("\n--- Type Aliases ---") + +from typing import List, Tuple + +# Create readable aliases for complex types +UserId = int +Username = str +Coordinate = Tuple[float, float] +UserList = List[Dict[str, Union[str, int]]] + +def get_user(user_id: UserId) -> Username: + """Get username by ID.""" + return f"user_{user_id}" + +def calculate_distance(point1: Coordinate, point2: Coordinate) -> float: + """Calculate distance between two points.""" + return ((point2[0] - point1[0])**2 + (point2[1] - point1[1])**2)**0.5 + +print(f"get_user(123): {get_user(123)}") +print(f"distance((0,0), (3,4)): {calculate_distance((0, 0), (3, 4))}") + +# ============================================================================= +# 7. CLASS TYPE HINTS +# ============================================================================= + +print("\n--- Class Type Hints ---") + +from typing import Optional, List, ClassVar + +class Person: + """A person with type-hinted attributes.""" + + # Class variables + species: ClassVar[str] = "Homo sapiens" + + def __init__(self, name: str, age: int) -> None: + self.name: str = name + self.age: int = age + self.email: Optional[str] = None + + def set_email(self, email: str) -> None: + """Set the person's email.""" + self.email = email + + def get_info(self) -> str: + """Get person's info.""" + return f"{self.name}, {self.age} years old" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Person": + """Create Person from dictionary.""" + return cls(data["name"], data["age"]) + +person = Person("Alice", 25) +person.set_email("alice@example.com") +print(f"Person: {person.get_info()}") +print(f"Email: {person.email}") + +# ============================================================================= +# 8. GENERICS +# ============================================================================= + +print("\n--- Generics ---") + +from typing import TypeVar, Generic, List + +# Define a type variable +T = TypeVar('T') + +# Generic function +def first_item(items: List[T]) -> T: + """Get first item of any list type.""" + return items[0] + +# Generic class +class Stack(Generic[T]): + """A generic stack.""" + + def __init__(self) -> None: + self._items: List[T] = [] + + def push(self, item: T) -> None: + """Push item onto stack.""" + self._items.append(item) + + def pop(self) -> T: + """Pop item from stack.""" + return self._items.pop() + + def is_empty(self) -> bool: + """Check if stack is empty.""" + return len(self._items) == 0 + +# Using generics +print(f"first_item([1, 2, 3]): {first_item([1, 2, 3])}") +print(f"first_item(['a', 'b']): {first_item(['a', 'b'])}") + +stack: Stack[int] = Stack() +stack.push(1) +stack.push(2) +stack.push(3) +print(f"Stack pop: {stack.pop()}") + +# ============================================================================= +# 9. LITERAL AND FINAL +# ============================================================================= + +print("\n--- Literal and Final ---") + +from typing import Literal, Final + +# Literal - exact values only +Mode = Literal["r", "w", "a"] + +def open_file(path: str, mode: Mode) -> str: + """Open file with specific mode.""" + return f"Opening {path} in mode '{mode}'" + +print(open_file("data.txt", "r")) +# open_file("data.txt", "x") # Type error! "x" is not allowed + +# Final - cannot be reassigned +MAX_SIZE: Final[int] = 100 +# MAX_SIZE = 200 # Type checker would flag this + +print(f"MAX_SIZE (Final): {MAX_SIZE}") + +# ============================================================================= +# 10. TYPE CHECKING TOOLS +# ============================================================================= + +print("\n--- Type Checking ---") + +print(""" +Type hints are NOT enforced at runtime! +Use these tools to check types statically: + +1. mypy - The most popular type checker + pip install mypy + mypy your_script.py + +2. pyright - Microsoft's type checker (fast) + pip install pyright + pyright your_script.py + +3. IDE Support + - VS Code with Pylance + - PyCharm (built-in) + +Example mypy output: + error: Argument 1 to "greet" has incompatible type "int"; expected "str" + +Benefits of type hints: + ✓ Catch bugs early (before runtime) + ✓ Better IDE autocomplete + ✓ Self-documenting code + ✓ Easier refactoring + ✓ Better code reviews +""") + +# ============================================================================= +# 11. BEST PRACTICES +# ============================================================================= + +print("--- Best Practices ---") + +print(""" +1. Start with function signatures + - Hint parameters and return types first + +2. Use Optional[X] for nullable values + - Not: x: str = None (wrong type!) + - Yes: x: Optional[str] = None + +3. Prefer specific types over Any + - Any defeats the purpose of type hints + +4. Use type aliases for complex types + - Makes code more readable + +5. Enable strict mode in mypy + - mypy --strict your_script.py + +6. Type hint public APIs first + - Internal code can be less strict + +7. Use TypedDict for dictionaries with known structure + +8. Consider using dataclasses for structured data +""") + +# Example: TypedDict and dataclass +from typing import TypedDict +from dataclasses import dataclass + +class UserDict(TypedDict): + """User data as dictionary.""" + name: str + age: int + email: Optional[str] + +@dataclass +class UserDataClass: + """User data as dataclass.""" + name: str + age: int + email: Optional[str] = None + +# Both provide type safety for structured data +user_dict: UserDict = {"name": "Alice", "age": 25, "email": None} +user_obj = UserDataClass(name="Bob", age=30) + +print(f"\nTypedDict: {user_dict}") +print(f"Dataclass: {user_obj}") diff --git a/10_best_practices/03_virtual_envs.py b/10_best_practices/03_virtual_envs.py new file mode 100644 index 0000000..92d5c11 --- /dev/null +++ b/10_best_practices/03_virtual_envs.py @@ -0,0 +1,455 @@ +""" +================================================================================ +File: 03_virtual_envs.py +Topic: Virtual Environments in Python +================================================================================ + +This file explains virtual environments in Python. Virtual environments are +isolated Python environments that allow you to manage dependencies separately +for each project, avoiding conflicts between different projects. + +Key Concepts: +- What are virtual environments +- Creating and activating venvs +- Managing dependencies with pip +- requirements.txt +- Best practices + +Note: This is a reference/tutorial file - the commands are shown as examples. + +================================================================================ +""" + +# ============================================================================= +# 1. WHAT ARE VIRTUAL ENVIRONMENTS? +# ============================================================================= + +print("=== What Are Virtual Environments? ===") + +print(""" +A virtual environment is an ISOLATED Python environment that: + + ✓ Has its own Python interpreter + ✓ Has its own site-packages directory + ✓ Doesn't affect global Python installation + ✓ Allows different projects to have different dependencies + +Why use virtual environments? + - Project A needs Django 3.2 + - Project B needs Django 4.1 + - Without venvs, you'd have conflicts! + +With virtual environments, each project has its own Django version. +""") + +# ============================================================================= +# 2. CREATING A VIRTUAL ENVIRONMENT +# ============================================================================= + +print("\n=== Creating a Virtual Environment ===") + +print(""" +Using the built-in 'venv' module (Python 3.3+): + + # Navigate to your project directory + cd my_project + + # Create a virtual environment named 'venv' + python -m venv venv + + # Or name it something else + python -m venv .venv # Hidden folder (common convention) + python -m venv env # Another common name + python -m venv my_env # Custom name + +This creates a folder with: + venv/ + ├── Include/ # C header files + ├── Lib/ # Python packages + │ └── site-packages/ + ├── Scripts/ # Activation scripts (Windows) + │ ├── activate + │ ├── activate.bat + │ └── python.exe + └── pyvenv.cfg # Configuration file +""") + +# ============================================================================= +# 3. ACTIVATING AND DEACTIVATING +# ============================================================================= + +print("\n=== Activating and Deactivating ===") + +print(""" +ACTIVATION (must do before using the venv): + + Windows (Command Prompt): + venv\\Scripts\\activate.bat + + Windows (PowerShell): + venv\\Scripts\\Activate.ps1 + + If you get execution policy error: + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + + macOS/Linux: + source venv/bin/activate + +When activated, your prompt changes: + (venv) C:\\my_project> + (venv) user@computer:~/my_project$ + +DEACTIVATION: + deactivate + +After deactivation, prompt returns to normal. +""") + +# ============================================================================= +# 4. INSTALLING PACKAGES +# ============================================================================= + +print("\n=== Installing Packages ===") + +print(""" +After activating your venv, use pip to install packages: + + # Install a package + pip install requests + + # Install specific version + pip install requests==2.28.0 + + # Install minimum version + pip install 'requests>=2.25.0' + + # Install multiple packages + pip install requests numpy pandas + + # Install from requirements.txt + pip install -r requirements.txt + + # Upgrade a package + pip install --upgrade requests + + # Uninstall a package + pip uninstall requests + + # List installed packages + pip list + + # Show package info + pip show requests +""") + +# ============================================================================= +# 5. REQUIREMENTS FILE +# ============================================================================= + +print("\n=== Requirements File ===") + +print(""" +The requirements.txt file lists all project dependencies: + +CREATING requirements.txt: + # Generate from current environment + pip freeze > requirements.txt + +EXAMPLE requirements.txt: + # requirements.txt + requests==2.28.1 + numpy>=1.21.0 + pandas~=1.5.0 + python-dotenv + +VERSION SPECIFIERS: + package==1.0.0 # Exact version + package>=1.0.0 # Minimum version + package<=1.0.0 # Maximum version + package~=1.0.0 # Compatible version (>=1.0.0, <2.0.0) + package!=1.0.0 # Exclude version + package>=1.0,<2.0 # Version range + +INSTALLING FROM requirements.txt: + pip install -r requirements.txt + +BEST PRACTICE: Create different files for different environments: + requirements.txt # Production dependencies + requirements-dev.txt # Development dependencies + requirements-test.txt # Testing dependencies +""") + +# ============================================================================= +# 6. VIRTUAL ENVIRONMENT ALTERNATIVES +# ============================================================================= + +print("\n=== Alternative Tools ===") + +print(""" +1. venv (built-in) + - Simple, included with Python + - Good for basic needs + + python -m venv venv + +2. virtualenv + - More features than venv + - Faster environment creation + + pip install virtualenv + virtualenv venv + +3. pipenv + - Combines venv + pip + - Uses Pipfile instead of requirements.txt + - Automatic locking of dependencies + + pip install pipenv + pipenv install requests + pipenv shell + +4. poetry + - Modern dependency management + - Better dependency resolution + - Build and publish packages + + pip install poetry + poetry new my_project + poetry add requests + +5. conda + - Package manager + environment manager + - Great for data science (NumPy, SciPy pre-compiled) + + conda create -n myenv python=3.10 + conda activate myenv + conda install numpy +""") + +# ============================================================================= +# 7. PROJECT STRUCTURE WITH VENV +# ============================================================================= + +print("\n=== Project Structure ===") + +print(""" +Recommended project structure: + +my_project/ +├── venv/ # Virtual environment (don't commit!) +├── src/ +│ └── my_package/ +│ ├── __init__.py +│ └── main.py +├── tests/ +│ ├── __init__.py +│ └── test_main.py +├── .gitignore # Include 'venv/' here! +├── requirements.txt +├── requirements-dev.txt +├── setup.py or pyproject.toml +└── README.md + +.gitignore should include: + venv/ + .venv/ + env/ + __pycache__/ + *.pyc + .pytest_cache/ +""") + +# ============================================================================= +# 8. COMMON COMMANDS REFERENCE +# ============================================================================= + +print("\n=== Quick Reference ===") + +print(""" +╔══════════════════════════════════════════════════════════════════╗ +║ VIRTUAL ENVIRONMENT COMMANDS ║ +╠══════════════════════════════════════════════════════════════════╣ +║ CREATE ║ +║ python -m venv venv ║ +║ ║ +║ ACTIVATE ║ +║ Windows: venv\\Scripts\\activate ║ +║ Mac/Linux: source venv/bin/activate ║ +║ ║ +║ DEACTIVATE ║ +║ deactivate ║ +║ ║ +║ INSTALL PACKAGES ║ +║ pip install package_name ║ +║ pip install -r requirements.txt ║ +║ ║ +║ EXPORT DEPENDENCIES ║ +║ pip freeze > requirements.txt ║ +║ ║ +║ CHECK PYTHON LOCATION ║ +║ Windows: where python ║ +║ Mac/Linux: which python ║ +║ ║ +║ DELETE VENV (just remove the folder) ║ +║ Windows: rmdir /s /q venv ║ +║ Mac/Linux: rm -rf venv ║ +╚══════════════════════════════════════════════════════════════════╝ +""") + +# ============================================================================= +# 9. BEST PRACTICES +# ============================================================================= + +print("\n=== Best Practices ===") + +print(""" +1. ALWAYS use virtual environments + - Even for small projects + - Prevents "works on my machine" problems + +2. Don't commit venv to version control + - Add 'venv/' to .gitignore + - Share requirements.txt instead + +3. Use meaningful venv names + - venv, .venv, or env are common + - Use .venv to hide the folder + +4. Pin your versions in production + - Use pip freeze > requirements.txt + - Review and clean up before committing + +5. Separate dev and production dependencies + - requirements.txt for production + - requirements-dev.txt for testing, linting, etc. + +6. Document how to set up the environment + - Include setup instructions in README.md + +7. Use pip-tools for better dependency management + - pip install pip-tools + - Create requirements.in with direct dependencies + - pip-compile requirements.in creates pinned file +""") + +# ============================================================================= +# 10. TROUBLESHOOTING +# ============================================================================= + +print("\n=== Troubleshooting ===") + +print(""" +COMMON ISSUES AND SOLUTIONS: + +1. "python not recognized" after activation + Solution: Use full path or reinstall Python with PATH option + +2. PowerShell "execution policy" error + Solution: + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +3. "pip install" uses global pip + Solution: Make sure venv is activated first + Check: python -c "import sys; print(sys.prefix)" + +4. Can't delete venv folder (files in use) + Solution: + - Deactivate first + - Close any terminals/IDEs using it + - Restart computer if needed + +5. requirements.txt has too many packages + Solution: Use pipreqs to generate minimal requirements + pip install pipreqs + pipreqs /path/to/project + +6. Package conflicts + Solution: Create fresh venv and install carefully + Or use pip-compile for dependency resolution +""") + +# ============================================================================= +# 11. IDE INTEGRATION +# ============================================================================= + +print("\n=== IDE Integration ===") + +print(""" +VS CODE: + 1. Open project folder + 2. Select Python Interpreter (Ctrl+Shift+P) + 3. Choose "Python: Select Interpreter" + 4. Select the venv Python (./venv/Scripts/python.exe) + 5. New terminals auto-activate venv + +PYCHARM: + 1. File > Settings > Project > Python Interpreter + 2. Click gear icon > Add + 3. Select "Existing environment" or create new + 4. Point to venv/Scripts/python.exe + +JUPYTER NOTEBOOK: + 1. Activate venv + 2. Install: pip install ipykernel + 3. Register: python -m ipykernel install --user --name=myenv + 4. Select kernel in Jupyter +""") + +# ============================================================================= +# 12. EXAMPLE WORKFLOW +# ============================================================================= + +print("\n=== Example Workflow ===") + +print(""" +STARTING A NEW PROJECT: + + # 1. Create project folder + mkdir my_awesome_project + cd my_awesome_project + + # 2. Create virtual environment + python -m venv venv + + # 3. Activate it + venv\\Scripts\\activate # Windows + source venv/bin/activate # Mac/Linux + + # 4. Upgrade pip (good practice) + python -m pip install --upgrade pip + + # 5. Install packages + pip install requests numpy pandas + + # 6. Create requirements.txt + pip freeze > requirements.txt + + # 7. Create .gitignore + echo "venv/" > .gitignore + echo "__pycache__/" >> .gitignore + + # 8. Start coding! + code . + +JOINING AN EXISTING PROJECT: + + # 1. Clone the repository + git clone https://github.com/user/project.git + cd project + + # 2. Create virtual environment + python -m venv venv + + # 3. Activate it + venv\\Scripts\\activate # Windows + + # 4. Install dependencies + pip install -r requirements.txt + + # 5. Start working! +""") + +print("\n" + "=" * 60) +print("Virtual environments are essential for Python development!") +print("Always use them to keep your projects isolated and reproducible.") +print("=" * 60) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe9d25a --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# Python Review - Complete Tutorial + +A comprehensive Python tutorial covering everything from basics to advanced topics. +Each file is self-contained with detailed explanations and executable examples. + +## 📚 Topics Covered + +### 01. Basics +| File | Topic | +|------|-------| +| `01_print.py` | Print function, formatting, escape characters | +| `02_comments.py` | Single-line, multi-line comments, docstrings | +| `03_variables.py` | Variable assignment, naming, multiple assignment | +| `04_data_types.py` | Numbers, strings, booleans, type conversion | + +### 02. Control Flow +| File | Topic | +|------|-------| +| `01_if_else.py` | Conditionals, comparison operators, logical operators | +| `02_elif.py` | Multiple conditions, grading systems, ranges | +| `03_match_case.py` | Pattern matching (Python 3.10+), guards, unpacking | + +### 03. Loops +| File | Topic | +|------|-------| +| `01_for_loop.py` | For loops, range(), enumerate(), zip() | +| `02_while_loop.py` | While loops, counters, sentinels, infinite loops | +| `03_break_continue.py` | Loop control, break, continue, pass, for-else | + +### 04. Data Structures +| File | Topic | +|------|-------| +| `01_lists.py` | Lists, indexing, slicing, methods, comprehensions | +| `02_tuples.py` | Tuples, immutability, unpacking, named tuples | +| `03_sets.py` | Sets, operations, frozen sets, practical uses | +| `04_dictionaries.py` | Dictionaries, methods, nesting, comprehensions | + +### 05. Functions +| File | Topic | +|------|-------| +| `01_function_basics.py` | Defining functions, scope, docstrings, nested functions | +| `02_arguments.py` | Positional, keyword, *args, **kwargs, type hints | +| `03_return_values.py` | Returns, multiple returns, early returns, generators | +| `04_lambda_functions.py` | Lambda, map, filter, reduce, functional patterns | + +### 06. Modules & Packages +| File | Topic | +|------|-------| +| `01_imports.py` | Import patterns, standard library, module organization | +| `02_custom_modules.py` | Creating modules, packages, __init__.py, __name__ | + +### 07. Error Handling +| File | Topic | +|------|-------| +| `01_try_except.py` | try/except/else/finally, catching exceptions | +| `02_custom_exceptions.py` | Creating exceptions, hierarchy, best practices | + +### 08. Object-Oriented Programming +| File | Topic | +|------|-------| +| `01_classes_objects.py` | Classes, objects, attributes, methods, self | +| `02_init_methods.py` | Constructors, properties, classmethods | +| `03_inheritance.py` | Single/multiple inheritance, super(), MRO, ABC | +| `04_polymorphism.py` | Duck typing, operator overloading, protocols | + +### 09. Advanced Python +| File | Topic | +|------|-------| +| `01_list_comprehensions.py` | List, dict, set comprehensions, generators | +| `02_generators.py` | Yield, iterators, memory efficiency, pipelines | +| `03_decorators.py` | Simple/parameterized decorators, functools | +| `04_context_managers.py` | with statement, __enter__/__exit__, contextlib | + +### 10. Best Practices +| File | Topic | +|------|-------| +| `01_pep8.py` | Python style guide, naming, formatting | +| `02_type_hinting.py` | Type annotations, typing module, generics | +| `03_virtual_envs.py` | venv, pip, requirements.txt, project setup | + +## 🚀 How to Use + +1. **Start from basics**: Begin with `01_basics` and work your way up +2. **Run the files**: Each file is executable: `python 01_print.py` +3. **Read the comments**: Each section is thoroughly explained +4. **Experiment**: Modify the code and observe the results + +## 🛠️ Prerequisites + +- Python 3.10+ recommended (for match/case support) +- Python 3.8+ minimum (for most features) + +## 📝 File Structure + +Each file follows this structure: +```python +""" +================================================================================ +File: filename.py +Topic: Topic Name +================================================================================ + +Description of what the file covers... + +Key Concepts: +- Concept 1 +- Concept 2 +- ... + +================================================================================ +""" + +# Section 1: Basic Example +# ... code with inline comments ... + +# Section 2: Advanced Example +# ... more code ... +``` + +## ✅ Learning Path + +``` +Beginner: 01_basics → 02_control_flow → 03_loops +Intermediate: 04_data_structures → 05_functions → 06_modules +Advanced: 07_error_handling → 08_oop → 09_advanced +Professional: 10_best_practices +``` + +--- +*Happy Learning! 🐍*