Python Notes Unit 3-5 (1)
Python Notes Unit 3-5 (1)
---
### **Example:**
```python
class Employee:
def __init__(self, name, salary, department):
self.name = name # Public
self._salary = salary # Protected
self.__department = department # Private
def display(self):
print(f"Name: {self.name}")
print(f"Salary: {self._salary}")
print(f"Department: {self.__department}")
# Accessing attributes:
print(emp.name) # Works (Public)
print(emp._salary) # Works (Protected, but discouraged)
print(emp.__department) # Error (Private, due to name mangling)
```
---
@property
def balance(self):
return self.__balance
@balance.setter
def balance(self, amount):
if amount >= 0:
self.__balance = amount
else:
print("Balance cannot be negative!")
---
4. Introduction to Abstraction**
Abstraction focuses on hiding complex implementation details and exposing only essential
features.
---
### **Example:**
```python
from abc import ABC, abstractmethod
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
def perimeter(self):
return 2 * 3.14 * self.radius
```python
from abc import ABC, abstractmethod
class DatabaseInterface(ABC):
@abstractmethod
def connect(self):
pass
@abstractmethod
def disconnect(self):
pass
class MySQLDatabase(DatabaseInterface):
def connect(self):
print("MySQL connected")
def disconnect(self):
print("MySQL disconnected")
db = MySQLDatabase()
db.connect()
```
---
---
## **Summary**
| Concept | Description |
|---------|-------------|
| **Encapsulation** | Bundling data and methods, controlling access. |
| **Public Members** | Accessible anywhere. |
| **Protected Members** | Convention: `_var` (accessible in class/subclasses). |
| **Private Members** | Convention: `__var` (name mangling prevents direct access). |
| **Getters/Setters** | Controlled access to attributes. |
| **Abstraction** | Hiding complexity, exposing only essentials. |
| **Abstract Classes** | Cannot be instantiated, must implement abstract methods. |
---
Unit IV
---
2. Composition in Python**
In **composition**, the lifetime of the part is controlled by the whole. If the whole is
destroyed, the part is also destroyed.
def start(self):
print("Engine started")
class Car:
def __init__(self, model, horsepower):
self.model = model
self.engine = Engine(horsepower) # Composition (Engine cannot exist without Car)
def start(self):
print(f"{self.model} is starting...")
self.engine.start()
---
3. Aggregation in Python**
In **aggregation**, the part can exist independently of the whole. The whole only "uses"
the part.
def teach(self):
print(f"{self.name} is teaching")
class University:
def __init__(self, name):
self.name = name
self.professors = [] # Aggregation (Professors can exist outside University)
def display_professors(self):
print(f"Professors at {self.name}:")
for prof in self.professors:
print(f"- {prof.name}")
university = University("MIT")
university.add_professor(prof1)
university.add_professor(prof2)
university.display_professors()
```
**Output:**
```
Professors at MIT:
- Dr. Smith
- Dr. Johnson
```
---
---
---
✖ **Cons:**
- Can lead to complex structures if overused.
- Requires careful management of dependencies.
### **Aggregation:**
✔ **Pros:**
- More flexible (objects can be shared).
- Promotes loose coupling.
✖ **Cons:**
- Less control over parts (they can be modified externally).
- May lead to unintended side effects if not managed properly.
### **Inheritance:**
✔ **Pros:**
- Easy to implement (code reuse).
- Models "is-a" relationships well.
✖ **Cons:**
- Can lead to rigid hierarchies.
- Tight coupling between parent and child classes.
---
class RAM:
def __init__(self, size):
self.size = size
class Storage:
def __init__(self, capacity):
self.capacity = capacity
class Computer:
def __init__(self, cpu_cores, ram_size, storage_capacity):
self.cpu = CPU(cpu_cores) # Composition (CPU is part of Computer)
self.ram = RAM(ram_size) # Composition (RAM is part of Computer)
self.storage = Storage(storage_capacity) # Composition
def specs(self):
print(f"CPU Cores: {self.cpu.cores}")
print(f"RAM: {self.ram.size}GB")
print(f"Storage: {self.storage.capacity}TB")
def display_specs(self):
for comp in self.computers:
comp.specs()
# Usage
pc1 = Computer(8, 16, 1)
pc2 = Computer(4, 8, 2)
network = Network()
network.add_computer(pc1)
network.add_computer(pc2)
network.display_specs()
```
**Output:**
```
CPU Cores: 8
RAM: 16GB
Storage: 1TB
CPU Cores: 4
RAM: 8GB
Storage: 2TB
```
---
8. Best Practices**
1. **Favor Composition Over Inheritance** (to avoid deep hierarchies).
2. **Use Aggregation for Shared Resources** (e.g., a `Library` has `Books`).
3. **Avoid Circular Dependencies** (e.g., `A` depends on `B`, and `B` depends on `A`).
4. **Encapsulate Parts Properly** (use private/protected members where needed).
---
## **Summary**
| Concept | Description | Example |
|---------|------------|---------|
| **Composition** | Strong "has-a" relationship, part dies with whole. | `Car` has an
`Engine`. |
| **Aggregation** | Weak "has-a" relationship, part can exist independently. | `University`
has `Professors`. |
| **Inheritance** | "Is-a" relationship, promotes code reuse. | `Dog` is an `Animal`. |
| **When to Use?** | Composition for tight control, aggregation for flexibility. | Prefer
composition over inheritance. |
---
Unit V
# **Lecture Notes: Advanced OOP Concepts, Design Patterns, and SOLID Principles in
Python**
```python
class MathUtils:
@staticmethod
def add(a, b):
return a + b
---
```python
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year):
age = 2024 - birth_year
return cls(name, age) # Creates a new Person instance
```python
class Circle:
def __init__(self, radius):
self._radius = radius # Protected attribute
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value > 0:
self._radius = value
else:
raise ValueError("Radius must be positive")
circle = Circle(5)
print(circle.radius) # Output: 5 (getter)
circle.radius = 10 # Valid (setter)
circle.radius = -1 # Raises ValueError
```
---
---
```python
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # Output: True (same instance)
```
```python
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
def get_pet(pet_type):
pets = {"dog": Dog(), "cat": Cat()}
return pets.get(pet_type, None)
pet = get_pet("dog")
print(pet.speak()) # Output: Woof!
```
---
class USASocket:
def voltage(self):
return 120
class Adapter:
def __init__(self, socket):
self.socket = socket
def voltage(self):
return f"Adapted {self.socket.voltage()}V"
euro_socket = EuropeanSocket()
adapter = Adapter(euro_socket)
print(adapter.voltage()) # Output: Adapted 230V
```
```python
class Coffee:
def cost(self):
return 5
class MilkDecorator:
def __init__(self, coffee):
self._coffee = coffee
def cost(self):
return self._coffee.cost() + 2
coffee = Coffee()
coffee_with_milk = MilkDecorator(coffee)
print(coffee_with_milk.cost()) # Output: 7
```
---
```python
class NewsAgency:
def __init__(self):
self._subscribers = []
class Subscriber:
def update(self, news):
print(f"Received news: {news}")
agency = NewsAgency()
sub1 = Subscriber()
agency.subscribe(sub1)
agency.notify("Python 4.0 released!") # Output: Received news: Python 4.0 released!
```
```python
class PaymentStrategy:
def pay(self, amount):
pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paid ${amount} via Credit Card")
class PayPalPayment(PaymentStrategy):
def pay(self, amount):
print(f"Paid ${amount} via PayPal")
class ShoppingCart:
def __init__(self, strategy):
self._strategy = strategy
cart = ShoppingCart(PayPalPayment())
cart.checkout(100) # Output: Paid $100 via PayPal
```
---
---
---
## **Summary**
| Topic | Key Takeaways |
|-------|--------------|
| **Static/Class Methods** | `@staticmethod` (no `self`/`cls`), `@classmethod` (factory
methods). |
| **Properties** | Controlled attribute access using `@property`. |
| **Design Patterns** | Singleton, Factory, Adapter, Observer, Strategy. |
| **SOLID Principles** | Single Responsibility, Open-Closed, Liskov Substitution, etc. |
| **Best Practices** | Testable, modular, and documented code. |
---