""" ================================================================================ 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)