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