16_oop_basics.py

Download
python 588 lines 14.4 KB
  1"""
  2Object-Oriented Programming Basics
  3
  4Demonstrates:
  5- Classes and instances
  6- Instance and class attributes
  7- Instance and class methods
  8- Static methods
  9- Inheritance and super()
 10- Method overriding
 11- @property decorator
 12- Dunder (magic) methods
 13- Composition vs inheritance
 14- Abstract base classes
 15"""
 16
 17from abc import ABC, abstractmethod
 18from typing import List
 19
 20
 21def section(title: str) -> None:
 22    """Print a section header."""
 23    print("\n" + "=" * 60)
 24    print(f"  {title}")
 25    print("=" * 60)
 26
 27
 28# =============================================================================
 29# Basic Class
 30# =============================================================================
 31
 32section("Basic Class")
 33
 34
 35class Person:
 36    """Simple person class."""
 37
 38    def __init__(self, name: str, age: int):
 39        """Initialize person."""
 40        self.name = name
 41        self.age = age
 42
 43    def greet(self) -> str:
 44        """Greet method."""
 45        return f"Hello, I'm {self.name}"
 46
 47    def have_birthday(self):
 48        """Increment age."""
 49        self.age += 1
 50
 51
 52person = Person("Alice", 30)
 53print(f"Created: {person.name}, age {person.age}")
 54print(f"Greeting: {person.greet()}")
 55
 56person.have_birthday()
 57print(f"After birthday: age {person.age}")
 58
 59
 60# =============================================================================
 61# Class and Instance Attributes
 62# =============================================================================
 63
 64section("Class and Instance Attributes")
 65
 66
 67class Dog:
 68    """Dog class with class and instance attributes."""
 69
 70    # Class attribute (shared by all instances)
 71    species = "Canis familiaris"
 72    count = 0
 73
 74    def __init__(self, name: str, breed: str):
 75        """Initialize dog."""
 76        # Instance attributes (unique to each instance)
 77        self.name = name
 78        self.breed = breed
 79        Dog.count += 1
 80
 81
 82dog1 = Dog("Buddy", "Golden Retriever")
 83dog2 = Dog("Max", "Beagle")
 84
 85print(f"dog1: {dog1.name}, {dog1.breed}, {dog1.species}")
 86print(f"dog2: {dog2.name}, {dog2.breed}, {dog2.species}")
 87print(f"Total dogs: {Dog.count}")
 88
 89# Modifying class attribute
 90Dog.species = "Canis lupus familiaris"
 91print(f"Updated species: {dog1.species}, {dog2.species}")
 92
 93
 94# =============================================================================
 95# Class Methods and Static Methods
 96# =============================================================================
 97
 98section("Class Methods and Static Methods")
 99
100
101class Circle:
102    """Circle class with various method types."""
103
104    pi = 3.14159
105
106    def __init__(self, radius: float):
107        """Initialize circle."""
108        self.radius = radius
109
110    def area(self) -> float:
111        """Instance method - uses instance data."""
112        return self.pi * self.radius ** 2
113
114    @classmethod
115    def from_diameter(cls, diameter: float):
116        """Class method - alternative constructor."""
117        return cls(diameter / 2)
118
119    @staticmethod
120    def is_valid_radius(radius: float) -> bool:
121        """Static method - utility function."""
122        return radius > 0
123
124
125# Instance method
126circle = Circle(5)
127print(f"Circle(5) area: {circle.area()}")
128
129# Class method
130circle2 = Circle.from_diameter(10)
131print(f"Circle.from_diameter(10) radius: {circle2.radius}")
132
133# Static method
134print(f"is_valid_radius(5): {Circle.is_valid_radius(5)}")
135print(f"is_valid_radius(-1): {Circle.is_valid_radius(-1)}")
136
137
138# =============================================================================
139# Inheritance
140# =============================================================================
141
142section("Inheritance")
143
144
145class Animal:
146    """Base animal class."""
147
148    def __init__(self, name: str):
149        self.name = name
150
151    def speak(self) -> str:
152        """Make sound."""
153        return "Some sound"
154
155
156class Cat(Animal):
157    """Cat inherits from Animal."""
158
159    def speak(self) -> str:
160        """Override speak method."""
161        return "Meow!"
162
163
164class Dog(Animal):
165    """Dog inherits from Animal."""
166
167    def __init__(self, name: str, breed: str):
168        """Initialize with additional attribute."""
169        super().__init__(name)  # Call parent constructor
170        self.breed = breed
171
172    def speak(self) -> str:
173        """Override speak method."""
174        return "Woof!"
175
176
177cat = Cat("Whiskers")
178dog = Dog("Buddy", "Golden Retriever")
179
180print(f"{cat.name} says: {cat.speak()}")
181print(f"{dog.name} ({dog.breed}) says: {dog.speak()}")
182
183
184# =============================================================================
185# Method Resolution Order (MRO)
186# =============================================================================
187
188section("Method Resolution Order (MRO)")
189
190
191class A:
192    def method(self):
193        return "A"
194
195
196class B(A):
197    def method(self):
198        return "B"
199
200
201class C(A):
202    def method(self):
203        return "C"
204
205
206class D(B, C):
207    pass
208
209
210d = D()
211print(f"d.method(): {d.method()}")
212print(f"MRO: {[cls.__name__ for cls in D.__mro__]}")
213
214
215# =============================================================================
216# Property Decorator
217# =============================================================================
218
219section("Property Decorator")
220
221
222class Temperature:
223    """Temperature with getter/setter."""
224
225    def __init__(self, celsius: float):
226        self._celsius = celsius
227
228    @property
229    def celsius(self) -> float:
230        """Get temperature in Celsius."""
231        return self._celsius
232
233    @celsius.setter
234    def celsius(self, value: float):
235        """Set temperature in Celsius."""
236        if value < -273.15:
237            raise ValueError("Temperature below absolute zero")
238        self._celsius = value
239
240    @property
241    def fahrenheit(self) -> float:
242        """Get temperature in Fahrenheit."""
243        return self._celsius * 9/5 + 32
244
245    @fahrenheit.setter
246    def fahrenheit(self, value: float):
247        """Set temperature in Fahrenheit."""
248        self.celsius = (value - 32) * 5/9
249
250
251temp = Temperature(25)
252print(f"Celsius: {temp.celsius}°C")
253print(f"Fahrenheit: {temp.fahrenheit}°F")
254
255temp.fahrenheit = 100
256print(f"After setting to 100°F:")
257print(f"  Celsius: {temp.celsius}°C")
258
259
260# =============================================================================
261# Dunder (Magic) Methods
262# =============================================================================
263
264section("Dunder (Magic) Methods")
265
266
267class Vector:
268    """2D vector with operator overloading."""
269
270    def __init__(self, x: float, y: float):
271        self.x = x
272        self.y = y
273
274    def __repr__(self) -> str:
275        """String representation."""
276        return f"Vector({self.x}, {self.y})"
277
278    def __str__(self) -> str:
279        """User-friendly string."""
280        return f"<{self.x}, {self.y}>"
281
282    def __add__(self, other):
283        """Vector addition."""
284        return Vector(self.x + other.x, self.y + other.y)
285
286    def __sub__(self, other):
287        """Vector subtraction."""
288        return Vector(self.x - other.x, self.y - other.y)
289
290    def __mul__(self, scalar: float):
291        """Scalar multiplication."""
292        return Vector(self.x * scalar, self.y * scalar)
293
294    def __eq__(self, other) -> bool:
295        """Equality comparison."""
296        return self.x == other.x and self.y == other.y
297
298    def __len__(self) -> int:
299        """Length (magnitude)."""
300        return int((self.x**2 + self.y**2) ** 0.5)
301
302
303v1 = Vector(3, 4)
304v2 = Vector(1, 2)
305
306print(f"v1: {v1}")
307print(f"v2: {v2}")
308print(f"v1 + v2: {v1 + v2}")
309print(f"v1 - v2: {v1 - v2}")
310print(f"v1 * 2: {v1 * 2}")
311print(f"v1 == v2: {v1 == v2}")
312print(f"len(v1): {len(v1)}")
313
314
315# =============================================================================
316# Context Manager Protocol
317# =============================================================================
318
319section("Context Manager Protocol")
320
321
322class FileManager:
323    """File manager implementing context protocol."""
324
325    def __init__(self, filename: str, mode: str):
326        self.filename = filename
327        self.mode = mode
328        self.file = None
329
330    def __enter__(self):
331        """Open file."""
332        print(f"Opening {self.filename}")
333        self.file = open(self.filename, self.mode)
334        return self.file
335
336    def __exit__(self, exc_type, exc_val, exc_tb):
337        """Close file."""
338        if self.file:
339            print(f"Closing {self.filename}")
340            self.file.close()
341        return False
342
343
344import tempfile
345import os
346
347temp_file = os.path.join(tempfile.gettempdir(), "oop_demo.txt")
348
349with FileManager(temp_file, 'w') as f:
350    f.write("Hello from context manager!\n")
351
352os.remove(temp_file)
353
354
355# =============================================================================
356# Composition vs Inheritance
357# =============================================================================
358
359section("Composition vs Inheritance")
360
361# Inheritance approach
362
363
364class Vehicle:
365    """Vehicle base class."""
366
367    def __init__(self, brand: str):
368        self.brand = brand
369
370    def start(self):
371        return f"{self.brand} starting..."
372
373
374class Car(Vehicle):
375    """Car inherits from Vehicle."""
376
377    def drive(self):
378        return f"{self.brand} driving"
379
380
381# Composition approach
382
383
384class Engine:
385    """Engine component."""
386
387    def __init__(self, horsepower: int):
388        self.horsepower = horsepower
389
390    def start(self):
391        return f"Engine ({self.horsepower}hp) starting..."
392
393
394class CarComposition:
395    """Car with engine composition."""
396
397    def __init__(self, brand: str, horsepower: int):
398        self.brand = brand
399        self.engine = Engine(horsepower)  # Composition
400
401    def start(self):
402        return self.engine.start()
403
404    def drive(self):
405        return f"{self.brand} driving"
406
407
408car1 = Car("Toyota")
409print(f"Inheritance: {car1.start()}")
410
411car2 = CarComposition("Honda", 200)
412print(f"Composition: {car2.start()}")
413
414print("\nComposition is often preferred:")
415print("  - More flexible")
416print("  - Avoids fragile base class problem")
417print("  - 'Has-a' relationship clearer than 'Is-a'")
418
419
420# =============================================================================
421# Abstract Base Classes
422# =============================================================================
423
424section("Abstract Base Classes")
425
426
427class Shape(ABC):
428    """Abstract shape base class."""
429
430    @abstractmethod
431    def area(self) -> float:
432        """Calculate area (must be implemented by subclasses)."""
433        pass
434
435    @abstractmethod
436    def perimeter(self) -> float:
437        """Calculate perimeter (must be implemented by subclasses)."""
438        pass
439
440
441class Rectangle(Shape):
442    """Concrete rectangle implementation."""
443
444    def __init__(self, width: float, height: float):
445        self.width = width
446        self.height = height
447
448    def area(self) -> float:
449        return self.width * self.height
450
451    def perimeter(self) -> float:
452        return 2 * (self.width + self.height)
453
454
455class Circle(Shape):
456    """Concrete circle implementation."""
457
458    def __init__(self, radius: float):
459        self.radius = radius
460
461    def area(self) -> float:
462        return 3.14159 * self.radius ** 2
463
464    def perimeter(self) -> float:
465        return 2 * 3.14159 * self.radius
466
467
468rect = Rectangle(5, 3)
469circ = Circle(4)
470
471print(f"Rectangle(5, 3):")
472print(f"  Area: {rect.area()}")
473print(f"  Perimeter: {rect.perimeter()}")
474
475print(f"\nCircle(4):")
476print(f"  Area: {circ.area():.2f}")
477print(f"  Perimeter: {circ.perimeter():.2f}")
478
479# Cannot instantiate abstract class
480try:
481    shape = Shape()
482except TypeError as e:
483    print(f"\nCannot instantiate ABC: {e}")
484
485
486# =============================================================================
487# Real-World Example: Bank Account
488# =============================================================================
489
490section("Real-World Example: Bank Account")
491
492
493class BankAccount:
494    """Bank account with encapsulation."""
495
496    _account_count = 0
497
498    def __init__(self, owner: str, balance: float = 0):
499        self.owner = owner
500        self._balance = balance  # Private attribute
501        self._transactions: List[str] = []
502        BankAccount._account_count += 1
503        self._account_number = BankAccount._account_count
504
505    @property
506    def balance(self) -> float:
507        """Get balance (read-only from outside)."""
508        return self._balance
509
510    def deposit(self, amount: float):
511        """Deposit money."""
512        if amount <= 0:
513            raise ValueError("Deposit amount must be positive")
514        self._balance += amount
515        self._transactions.append(f"Deposit: +${amount:.2f}")
516
517    def withdraw(self, amount: float):
518        """Withdraw money."""
519        if amount <= 0:
520            raise ValueError("Withdrawal amount must be positive")
521        if amount > self._balance:
522            raise ValueError("Insufficient funds")
523        self._balance -= amount
524        self._transactions.append(f"Withdrawal: -${amount:.2f}")
525
526    def get_transaction_history(self) -> List[str]:
527        """Get transaction history."""
528        return self._transactions.copy()
529
530    def __str__(self) -> str:
531        return f"Account #{self._account_number}: {self.owner}, Balance: ${self._balance:.2f}"
532
533
534account = BankAccount("Alice", 1000)
535print(account)
536
537account.deposit(500)
538account.withdraw(200)
539print(f"\nAfter transactions: {account}")
540
541print("\nTransaction history:")
542for transaction in account.get_transaction_history():
543    print(f"  {transaction}")
544
545
546# =============================================================================
547# Summary
548# =============================================================================
549
550section("Summary")
551
552print("""
553Object-Oriented Programming concepts:
5541. Classes and objects - blueprints and instances
5552. Attributes - instance and class variables
5563. Methods - instance, class, and static
5574. Inheritance - reuse and extend functionality
5585. super() - call parent class methods
5596. @property - getters and setters
5607. Dunder methods - operator overloading, protocols
5618. Composition - 'has-a' relationships
5629. Abstract base classes - enforce interface contracts
563
564Key principles:
565- Encapsulation - hide internal details
566- Inheritance - is-a relationship
567- Polymorphism - same interface, different implementations
568- Abstraction - hide complexity
569
570Common dunder methods:
571- __init__ - constructor
572- __repr__ - developer representation
573- __str__ - user-friendly representation
574- __eq__, __lt__, __gt__ - comparisons
575- __add__, __sub__, __mul__ - arithmetic
576- __len__ - length
577- __getitem__ - indexing
578- __enter__, __exit__ - context manager
579
580Best practices:
581- Favor composition over inheritance
582- Use @property for computed attributes
583- Keep classes focused (Single Responsibility)
584- Make attributes private with _ prefix
585- Use abstract base classes for interfaces
586- Follow naming conventions (PascalCase for classes)
587""")