B3.1.2 Construct class designs, methods, behavior (AO3)
B3.1.2_1 Classes, methods per requirements
Classes
- Description: Define blueprints for objects, encapsulating attributes (data) and methods (behaviors) to model real-world entities based on requirements.
- Construction: Create classes with attributes and methods tailored to the problem, ensuring clear and relevant functionality.
Example (Python)
class Student:
def __init__(self, student_id, name, grade):
self.student_id = student_id # Attribute: Unique identifier
self.name = name # Attribute: Student name
self.grade = grade # Attribute: Academic grade
def update_grade(self, new_grade): # Method: Update grade
self.grade = new_grade
def display_info(self): # Method: Display student details
return f"ID: {self.student_id}, Name: {self.name}, Grade: {self.grade}"
def __init__(self, student_id, name, grade):
self.student_id = student_id # Attribute: Unique identifier
self.name = name # Attribute: Student name
self.grade = grade # Attribute: Academic grade
def update_grade(self, new_grade): # Method: Update grade
self.grade = new_grade
def display_info(self): # Method: Display student details
return f"ID: {self.student_id}, Name: {self.name}, Grade: {self.grade}"
Purpose
- Models a student entity with attributes (student_id, name, grade) and behaviors (update_grade, display_info) to meet a school management system's requirements.
Methods
- Description: Define behaviors as functions within a class, operating on the object's attributes or external inputs.
- Construction: Implement methods to perform specific tasks, such as calculations, updates, or data retrieval, aligned with requirements.
- Example: The update_grade method modifies the grade attribute, and display_info returns a formatted string of student details.
B3.1.2_2 UML class diagrams for relationships, attributes, methods
UML Class Diagrams
- Description: Visual representations of classes, their attributes, methods, and relationships (e.g., inheritance, association) to document design.
Components
- Class Name: Top section of the UML box (e.g., Student).
- Attributes: Middle section, listing data with types (e.g., student_id: int, name: str, grade: float).
- Methods: Bottom section, listing behaviors with parameters and return types (e.g., update_grade(new_grade: float), display_info(): str).
- Relationships: Lines indicating associations, such as:
- Inheritance: Solid line with an arrow (e.g., SportsCar inherits from Car).
- Association: Dashed or solid line showing interaction (e.g., Student enrolls in Course).
- Multiplicity: Numbers indicating cardinality (e.g., 1..* for one student to many courses).
Example UML Diagram (for a school system)
Student
student_id: int
name: str
grade: float
name: str
grade: float
update_grade(new_grade: float)
display_info(): str
display_info(): str
1..*
Course
course_id: int
title: str
title: str
enroll_student(student: Student)
- Student and Course classes with attributes and methods; a one-to-many relationship (one student enrolls in multiple courses).
Construction Process
- Identify entities from requirements (e.g., Student, Course for a school system).
- Define attributes (e.g., student_id, name) and methods (e.g., update_grade) to meet functional needs.
- Create UML diagrams to visualize classes, attributes, methods, and relationships (e.g., Student to Course association).
- Implement in Python, ensuring methods perform required behaviors.
Implementation Example
class Course:
def __init__(self, course_id, title):
self.course_id = course_id
self.title = title
self.students = [] # List to store enrolled students
def enroll_student(self, student):
self.students.append(student)
def display_students(self):
return [student.display_info() for student in self.students]
# Example usage
student = Student(101, "Alice", 85.5)
course = Course(1, "Computer Science")
course.enroll_student(student)
print(course.display_students()) # Output: ['ID: 101, Name: Alice, Grade: 85.5']
def __init__(self, course_id, title):
self.course_id = course_id
self.title = title
self.students = [] # List to store enrolled students
def enroll_student(self, student):
self.students.append(student)
def display_students(self):
return [student.display_info() for student in self.students]
# Example usage
student = Student(101, "Alice", 85.5)
course = Course(1, "Computer Science")
course.enroll_student(student)
print(course.display_students()) # Output: ['ID: 101, Name: Alice, Grade: 85.5']
Use Case
- Designing a library system: Create a Book class (attributes: book_id, title; methods: borrow(), return_book()) and a Member class (attributes: member_id, name; methods: list_borrowed()).
- Use UML to show a many-to-many relationship via a Loan class, then implement in Python to manage book loans.
B3.1.3 Distinguish static vs non-static variables, methods (AO2)
B3.1.3_1 Differences in usage, scope
Static Variables
- Usage: Shared across all instances of a class, belonging to the class itself rather than any specific object.
- Example: A counter tracking the total number of objects created for a class.
- Scope: Accessible without creating an instance, using the class name (e.g., ClassName.variable).
- Characteristics: Initialized once, persists for the program's lifetime, and retains its value across all instances.
Non-Static (Instance) Variables
- Usage: Unique to each instance of a class, storing data specific to an object.
- Example: A name variable for a specific Student object.
- Scope: Accessible only through an instance of the class (e.g., student.name).
- Characteristics: Created for each object, stored separately in memory for each instance.
Static Methods
- Usage: Operate at the class level, not requiring an instance; typically used for utility functions or operations on static variables.
- Example: A method to get the total number of students without needing a specific Student object.
- Scope: Called using the class name (e.g., ClassName.method()), cannot access non-static variables or methods directly.
Non-Static (Instance) Methods
- Usage: Operate on instance data, accessing or modifying non-static variables of a specific object.
- Example: A method to update a student's grade for a specific Student object.
- Scope: Called on an instance (e.g., student.method()), with access to both instance and static variables.
B3.1.3_2 Instance vs class variables usage
Instance Variables Usage
- Store data unique to each object, such as attributes specific to an individual entity.
- Example: In a Car class, color and speed are instance variables, as each car has its own color and speed.
class Car:
def __init__(self, color, speed):
self.color = color # Instance variable
self.speed = speed # Instance variable
def display(self): # Instance method
return f"Color: {self.color}, Speed: {self.speed}"
car1 = Car("Red", 120)
car2 = Car("Blue", 150)
print(car1.display()) # Output: Color: Red, Speed: 120
print(car2.display()) # Output: Color: Blue, Speed: 150
def __init__(self, color, speed):
self.color = color # Instance variable
self.speed = speed # Instance variable
def display(self): # Instance method
return f"Color: {self.color}, Speed: {self.speed}"
car1 = Car("Red", 120)
car2 = Car("Blue", 150)
print(car1.display()) # Output: Color: Red, Speed: 120
print(car2.display()) # Output: Color: Blue, Speed: 150
- Use Case: Instance variables are used when each object needs its own copy of data (e.g., student names in a school system).
Class Variables Usage
- Store data shared across all instances, such as constants or counters.
- Example: A total_cars variable in a Car class to track the number of cars created.
class Car:
total_cars = 0 # Class variable
def __init__(self, color, speed):
self.color = color # Instance variable
self.speed = speed
Car.total_cars += 1 # Increment class variable
@staticmethod
def get_total_cars(): # Static method
return Car.total_cars
car1 = Car("Red", 120)
car2 = Car("Blue", 150)
print(Car.get_total_cars()) # Output: 2
total_cars = 0 # Class variable
def __init__(self, color, speed):
self.color = color # Instance variable
self.speed = speed
Car.total_cars += 1 # Increment class variable
@staticmethod
def get_total_cars(): # Static method
return Car.total_cars
car1 = Car("Red", 120)
car2 = Car("Blue", 150)
print(Car.get_total_cars()) # Output: 2
- Use Case: Class variables are used for shared data, like tracking the total number of objects or storing a constant like tax rate across all instances.
Key Differences
- Instance Variables/Methods: Unique to each object, accessed via self in Python; used for object-specific operations.
- Class Variables/Methods: Shared across all objects, accessed via the class name; used for class-level operations or shared data.
class BankAccount:
interest_rate = 0.05 # Class variable (shared)
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance # Instance variable (unique per account)
def deposit(self, amount): # Instance method
self.balance += amount
@staticmethod
def get_interest_rate(): # Static method
return BankAccount.interest_rate
# Usage
account1 = BankAccount("12345", 1000)
account2 = BankAccount("67890", 2000)
# Each account has its own balance
account1.deposit(500)
print(account1.balance) # Output: 1500
print(account2.balance) # Output: 2000
# Interest rate is shared across all accounts
print(BankAccount.get_interest_rate()) # Output: 0.05
interest_rate = 0.05 # Class variable (shared)
def __init__(self, account_number, balance):
self.account_number = account_number
self.balance = balance # Instance variable (unique per account)
def deposit(self, amount): # Instance method
self.balance += amount
@staticmethod
def get_interest_rate(): # Static method
return BankAccount.interest_rate
# Usage
account1 = BankAccount("12345", 1000)
account2 = BankAccount("67890", 2000)
# Each account has its own balance
account1.deposit(500)
print(account1.balance) # Output: 1500
print(account2.balance) # Output: 2000
# Interest rate is shared across all accounts
print(BankAccount.get_interest_rate()) # Output: 0.05
B3.1.4 Construct code for classes, object instantiation (AO3)
B3.1.4_1 Define classes, create objects
Define Classes
- Description: Create a blueprint for objects by defining a class with attributes (data) and methods (behaviors) to represent an entity.
- Construction: Use the class keyword in Python to define attributes in the __init__ method and other methods for functionality.
class Student:
def __init__(self, student_id, name):
self.student_id = student_id # Attribute
self.name = name # Attribute
self.courses = [] # Attribute (list for course enrollment)
def enroll_course(self, course): # Method
self.courses.append(course)
def display_info(self): # Method
return f"ID: {self.student_id}, Name: {self.name}, Courses: {self.courses}"
def __init__(self, student_id, name):
self.student_id = student_id # Attribute
self.name = name # Attribute
self.courses = [] # Attribute (list for course enrollment)
def enroll_course(self, course): # Method
self.courses.append(course)
def display_info(self): # Method
return f"ID: {self.student_id}, Name: {self.name}, Courses: {self.courses}"
- Purpose: Defines a Student class to store student details and manage course enrollments.
Create Objects
- Description: Instantiate objects from a class by calling the class with arguments to initialize attributes.
- Construction: Use the class name like a function, passing required parameters to the __init__ method.
# Object instantiation
student1 = Student(101, "Alice")
student2 = Student(102, "Bob")
# Interact with objects
student1.enroll_course("Math")
student2.enroll_course("Science")
print(student1.display_info()) # Output: ID: 101, Name: Alice, Courses: ['Math']
print(student2.display_info()) # Output: ID: 102, Name: Bob, Courses: ['Science']
student1 = Student(101, "Alice")
student2 = Student(102, "Bob")
# Interact with objects
student1.enroll_course("Math")
student2.enroll_course("Science")
print(student1.display_info()) # Output: ID: 101, Name: Alice, Courses: ['Math']
print(student2.display_info()) # Output: ID: 102, Name: Bob, Courses: ['Science']
- Purpose: Creates specific instances (student1, student2) with unique data, enabling individual operations.
B3.1.4_2 Constructors for object state initialization
Constructors
- Description: Special methods (e.g., __init__ in Python) that initialize an object's state when instantiated.
- Purpose: Set initial values for attributes, ensuring objects start with valid data.
- Construction: Define __init__ with parameters for required attributes, assigning them to instance variables using self.
class Book:
def __init__(self, book_id, title, author, available=True):
self.book_id = book_id # Initialize book ID
self.title = title # Initialize title
self.author = author # Initialize author
self.available = available # Initialize availability status
def borrow(self): # Method to update state
if self.available:
self.available = False
return f"{self.title} borrowed"
return f"{self.title} is unavailable"
def display(self): # Method to show state
return f"ID: {self.book_id}, Title: {self.title}, Author: {self.author}, Available: {self.available}"
# Instantiate objects with constructor
book1 = Book(1, "1984", "George Orwell")
book2 = Book(2, "Pride and Prejudice", "Jane Austen", False)
# Interact with objects
print(book1.display()) # Output: ID: 1, Title: 1984, Author: George Orwell, Available: True
print(book1.borrow()) # Output: 1984 borrowed
print(book2.borrow()) # Output: Pride and Prejudice is unavailable
def __init__(self, book_id, title, author, available=True):
self.book_id = book_id # Initialize book ID
self.title = title # Initialize title
self.author = author # Initialize author
self.available = available # Initialize availability status
def borrow(self): # Method to update state
if self.available:
self.available = False
return f"{self.title} borrowed"
return f"{self.title} is unavailable"
def display(self): # Method to show state
return f"ID: {self.book_id}, Title: {self.title}, Author: {self.author}, Available: {self.available}"
# Instantiate objects with constructor
book1 = Book(1, "1984", "George Orwell")
book2 = Book(2, "Pride and Prejudice", "Jane Austen", False)
# Interact with objects
print(book1.display()) # Output: ID: 1, Title: 1984, Author: George Orwell, Available: True
print(book1.borrow()) # Output: 1984 borrowed
print(book2.borrow()) # Output: Pride and Prejudice is unavailable
Constructor Role
- Ensures objects are initialized with required attributes (e.g., book_id, title) and optional attributes (e.g., available defaults to True).
- Prevents invalid states by setting constraints or default values.
- Example: A BankAccount constructor ensures balance starts at 0 or a specified amount, preventing undefined states.
Use Case
- In a library system, define a Book class with a constructor initializing book_id, title, author, and available.
- Instantiate objects for each book, then use methods to manage borrowing, ensuring each object's state is correctly initialized and manipulated.
B3.1.5 Explain, apply encapsulation, information hiding (AO2)
B3.1.5_1 Principles of encapsulation, information hiding
Encapsulation
- Description: Bundling data (attributes) and methods (behaviors) within a class, restricting direct access to some components to protect object integrity.
- Purpose: Ensures controlled interaction with an object's state, preventing unintended modifications and maintaining consistency.
- Example: A BankAccount class encapsulates balance and provides methods like deposit() to modify it safely.
Information Hiding
- Description: Concealing an object's internal implementation details, exposing only necessary interfaces to the outside world.
- Purpose: Reduces complexity, enhances security, and allows internal changes without affecting external code.
- Example: Hiding the balance attribute in a BankAccount class, accessible only via a get_balance() method.
Principles
- Combine related data and methods in a single class to create a cohesive unit.
- Restrict direct access to sensitive data, using methods to control interactions.
- Expose minimal, well-defined interfaces to maintain flexibility and security.
- Example: A Car class hides engine details, allowing users to interact via methods like start() or accelerate().
B3.1.5_2 Access modifiers: private, public
Private
- Description: Restricts access to attributes or methods to within the class, preventing external modification.
- Python Implementation: Conventionally indicated by prefixing with double underscores (e.g., __attribute), invoking name mangling to discourage direct access.
- Example: __balance in a BankAccount class is private, accessible only within the class.
Public
- Description: Allows unrestricted access to attributes or methods from outside the class.
- Python Implementation: No special prefix; attributes and methods are public by default (e.g., attribute).
- Example: deposit() in a BankAccount class is public, callable by external code.
Python Note
- Python uses naming conventions rather than strict access control; __attribute is mangled (e.g., _ClassName__attribute), but can still be accessed externally, though discouraged.
- Example: __balance is intended as private, but can be accessed as _BankAccount__balance (not recommended).
B3.1.5_3 Control class member access
Mechanism
- Use private attributes to store sensitive data, accessed or modified only through public methods (getters/setters).
- Public methods provide controlled interfaces to ensure valid operations on private data.
- Example: A Student class with private __grade and public set_grade() ensures grades are within a valid range (0–100).
class Student:
def __init__(self, name, grade):
self.__name = name # Private attribute
self.__grade = grade # Private attribute
# Getter method (public)
def get_grade(self):
return self.__grade
# Setter method (public)
def set_grade(self, new_grade):
if 0 <= new_grade <= 100: # Validate input
self.__grade = new_grade
return "Grade updated"
return "Invalid grade"
# Public method to display info
def display_info(self):
return f"Name: {self.__name}, Grade: {self.__grade}"
# Usage
student = Student("Alice", 85)
print(student.display_info()) # Output: Name: Alice, Grade: 85
print(student.set_grade(90)) # Output: Grade updated
print(student.get_grade()) # Output: 90
print(student.set_grade(150)) # Output: Invalid grade
# Direct access to __grade is discouraged
# print(student.__grade) # Raises AttributeError
def __init__(self, name, grade):
self.__name = name # Private attribute
self.__grade = grade # Private attribute
# Getter method (public)
def get_grade(self):
return self.__grade
# Setter method (public)
def set_grade(self, new_grade):
if 0 <= new_grade <= 100: # Validate input
self.__grade = new_grade
return "Grade updated"
return "Invalid grade"
# Public method to display info
def display_info(self):
return f"Name: {self.__name}, Grade: {self.__grade}"
# Usage
student = Student("Alice", 85)
print(student.display_info()) # Output: Name: Alice, Grade: 85
print(student.set_grade(90)) # Output: Grade updated
print(student.get_grade()) # Output: 90
print(student.set_grade(150)) # Output: Invalid grade
# Direct access to __grade is discouraged
# print(student.__grade) # Raises AttributeError
- Explanation: __name and __grade are private, accessible only via public methods get_grade(), set_grade(), and display_info(), ensuring controlled access.
B3.1.5_4 Limit access for integrity, security
Integrity
- Purpose: Prevents invalid states by controlling how data is modified, ensuring consistency with business rules.
- Example: In the Student class, set_grade() validates that new_grade is between 0 and 100, preventing invalid grades.
- Benefit: Maintains data consistency, avoiding errors like negative or unrealistic grades.
Security
- Purpose: Protects sensitive data from unauthorized access or modification, reducing risks of misuse.
- Example: Hiding __balance in a BankAccount class prevents external code from arbitrarily changing the account balance.
- Benefit: Enhances system security, especially in applications handling sensitive data like financial or personal information.
Application Example
class BankAccount:
def __init__(self, account_number, balance):
self.__account_number = account_number # Private
self.__balance = balance # Private
def deposit(self, amount): # Public method
if amount > 0:
self.__balance += amount
return f"Deposited {amount}, New Balance: {self.__balance}"
return "Invalid deposit amount"
def get_balance(self): # Public getter
return self.__balance
def get_account_number(self): # Public getter
return self.__account_number
# Usage
account = BankAccount("123456", 1000)
print(account.deposit(500)) # Output: Deposited 500, New Balance: 1500
print(account.get_balance()) # Output: 1500
# print(account.__balance) # Raises AttributeError, protecting data
def __init__(self, account_number, balance):
self.__account_number = account_number # Private
self.__balance = balance # Private
def deposit(self, amount): # Public method
if amount > 0:
self.__balance += amount
return f"Deposited {amount}, New Balance: {self.__balance}"
return "Invalid deposit amount"
def get_balance(self): # Public getter
return self.__balance
def get_account_number(self): # Public getter
return self.__account_number
# Usage
account = BankAccount("123456", 1000)
print(account.deposit(500)) # Output: Deposited 500, New Balance: 1500
print(account.get_balance()) # Output: 1500
# print(account.__balance) # Raises AttributeError, protecting data
- Use Case: A banking system uses encapsulation to hide __balance and __account_number, ensuring deposits are validated and sensitive data is secure, maintaining both integrity and security.
B3.2.1 Explain, apply inheritance for code reuse (AO2)
B3.2.1_1 Hierarchical parent-child class relationships
Inheritance
- Description: A mechanism in object-oriented programming where a child class (subclass) inherits attributes and methods from a parent class (superclass), forming a hierarchical relationship.
- Purpose: Promotes code reuse by allowing subclasses to share common functionality defined in the parent class while adding or modifying specific behaviors.
Hierarchical Relationship
- Parent class defines general attributes and methods shared by all subclasses.
- Child classes extend or specialize the parent class, inheriting its members and optionally overriding or adding new ones.
- Example: A Vehicle parent class with attributes like speed and methods like move() can be inherited by child classes Car and Bicycle, which add specific features like fuel_type or pedal_type.
Example Structure
- Parent Class: Animal with attributes name and method make_sound().
- Child Classes: Dog and Cat, inheriting name and make_sound(), but overriding make_sound() to produce specific sounds ("bark" or "meow").
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
return "Some generic sound"
class Dog(Animal): # Inherits from Animal
def make_sound(self): # Override method
return "Bark"
class Cat(Animal): # Inherits from Animal
def make_sound(self): # Override method
return "Meow"
# Usage
dog = Dog("Rex")
cat = Cat("Whiskers")
print(dog.name) # Output: Rex (inherited from Animal)
print(cat.name) # Output: Whiskers (inherited from Animal)
print(dog.make_sound()) # Output: Bark (overridden in Dog)
print(cat.make_sound()) # Output: Meow (overridden in Cat)
def __init__(self, name):
self.name = name
def make_sound(self):
return "Some generic sound"
class Dog(Animal): # Inherits from Animal
def make_sound(self): # Override method
return "Bark"
class Cat(Animal): # Inherits from Animal
def make_sound(self): # Override method
return "Meow"
# Usage
dog = Dog("Rex")
cat = Cat("Whiskers")
print(dog.name) # Output: Rex (inherited from Animal)
print(cat.name) # Output: Whiskers (inherited from Animal)
print(dog.make_sound()) # Output: Bark (overridden in Dog)
print(cat.make_sound()) # Output: Meow (overridden in Cat)
B3.2.1_2 Extend classes for functionality reuse
Extending Classes
- Description: Child classes inherit all public and protected members of the parent class, reusing existing functionality while adding new attributes or methods.
- Mechanism: Use the class ChildClass(ParentClass): syntax in Python to establish inheritance.
Reuse Benefits
- Avoids duplicating code by leveraging parent class functionality.
- Simplifies maintenance, as changes to the parent class propagate to subclasses.
- Example: A Person class with a greet() method can be reused by Student and Teacher subclasses, which add specific attributes like grade or subject.
Python Example
class Vehicle:
def __init__(self, brand, speed):
self.brand = brand
self.speed = speed
def move(self):
return f"{self.brand} is moving at {self.speed} km/h"
class Car(Vehicle): # Inherits from Vehicle
def __init__(self, brand, speed, fuel_type):
super().__init__(brand, speed) # Call parent constructor
self.fuel_type = fuel_type # Add new attribute
def honk(self): # Add new method
return "Beep beep!"
# Instantiate and use
car = Car("Toyota", 120, "Petrol")
print(car.move()) # Output: Toyota is moving at 120 km/h (reused from Vehicle)
print(car.honk()) # Output: Beep beep! (specific to Car)
def __init__(self, brand, speed):
self.brand = brand
self.speed = speed
def move(self):
return f"{self.brand} is moving at {self.speed} km/h"
class Car(Vehicle): # Inherits from Vehicle
def __init__(self, brand, speed, fuel_type):
super().__init__(brand, speed) # Call parent constructor
self.fuel_type = fuel_type # Add new attribute
def honk(self): # Add new method
return "Beep beep!"
# Instantiate and use
car = Car("Toyota", 120, "Petrol")
print(car.move()) # Output: Toyota is moving at 120 km/h (reused from Vehicle)
print(car.honk()) # Output: Beep beep! (specific to Car)
- Explanation: Car inherits brand, speed, and move() from Vehicle, reusing the parent's functionality, and extends it with fuel_type and honk().
B3.2.1_3 Access impact of private, public, protected modifiers
Access Modifiers in Python
- Python uses naming conventions rather than strict access control:
- Public: No prefix (e.g., brand), accessible from anywhere.
- Private: Double underscore prefix (e.g., __speed), name-mangled to discourage external access (e.g., _ClassName__speed).
- Protected: Single underscore prefix (e.g., _speed), a convention indicating restricted access, but still accessible externally (treated as a guideline).
Access Impact
- Public Members:
- Inherited and accessible in subclasses and external code.
- Example: brand in Vehicle is public, so Car and external code can access car.brand.
- Private Members:
- Not directly inherited or accessible in subclasses due to name mangling, but can be accessed indirectly (e.g., via getters or mangled names).
- Example: __speed in Vehicle is private; Car cannot directly access self.__speed, but could use _Vehicle__speed (discouraged).
- Protected Members:
- Inherited and accessible in subclasses, but external access is discouraged by convention.
- Example: _speed in Vehicle is protected; Car can access self._speed, but external code should avoid car._speed.
Python Example with Modifiers
class Person:
def __init__(self, name, age, salary):
self.name = name # Public
self._age = age # Protected
self.__salary = salary # Private
def get_salary(self): # Public getter for private attribute
return self.__salary
def display(self): # Public method
return f"Name: {self.name}, Age: {self._age}"
class Employee(Person): # Inherits from Person
def __init__(self, name, age, salary, job_title):
super().__init__(name, age, salary)
self.job_title = job_title
def work(self): # New method
# Can access public (name) and protected (_age) directly
return f"{self.name} (Age: {self._age}) is working as {self.job_title}"
def access_salary(self): # Access private via getter
return self.get_salary() # Cannot use self.__salary directly
# Usage
emp = Employee("Alice", 30, 50000, "Engineer")
print(emp.display()) # Output: Name: Alice, Age: 30
print(emp.work()) # Output: Alice (Age: 30) is working as Engineer
print(emp.access_salary()) # Output: 50000 (via getter)
print(emp.name) # Output: Alice (public access)
print(emp._age) # Output: 30 (protected, but accessible; discouraged)
# print(emp.__salary) # Raises AttributeError (private)
def __init__(self, name, age, salary):
self.name = name # Public
self._age = age # Protected
self.__salary = salary # Private
def get_salary(self): # Public getter for private attribute
return self.__salary
def display(self): # Public method
return f"Name: {self.name}, Age: {self._age}"
class Employee(Person): # Inherits from Person
def __init__(self, name, age, salary, job_title):
super().__init__(name, age, salary)
self.job_title = job_title
def work(self): # New method
# Can access public (name) and protected (_age) directly
return f"{self.name} (Age: {self._age}) is working as {self.job_title}"
def access_salary(self): # Access private via getter
return self.get_salary() # Cannot use self.__salary directly
# Usage
emp = Employee("Alice", 30, 50000, "Engineer")
print(emp.display()) # Output: Name: Alice, Age: 30
print(emp.work()) # Output: Alice (Age: 30) is working as Engineer
print(emp.access_salary()) # Output: 50000 (via getter)
print(emp.name) # Output: Alice (public access)
print(emp._age) # Output: 30 (protected, but accessible; discouraged)
# print(emp.__salary) # Raises AttributeError (private)
- Explanation:
- Employee inherits name (public) and _age (protected) from Person, accessing them directly.
- __salary (private) is inaccessible directly in Employee, but can be accessed via the public get_salary() method.
- External code can access name and _age (though _age access is discouraged), but not __salary.
Use Case
- In a library system, a Book parent class with public title, protected _status, and private __isbn is inherited by EBook and PrintBook.
- EBook reuses title and _status, adds file_format, and accesses __isbn via a getter, ensuring code reuse while controlling access for integrity and security.
B3.2.2 Construct polymorphic code, method overriding (AO3)
B3.2.2_1 Polymorphism for flexibility, reusability
Polymorphism
- Description: Allows objects of different classes to be treated as instances of a common superclass, with methods behaving differently based on the actual object type.
Purpose
- Flexibility: Enables code to work with objects of various subclasses through a single interface, adapting behavior dynamically.
- Reusability: Promotes reusable code by defining common methods in a superclass, implemented differently in subclasses.
- Example: A Shape superclass with a calculate_area() method can be used for objects of subclasses like Circle or Rectangle, each computing area differently.
Implementation
- Use inheritance to define a common method in a superclass, overridden in subclasses to provide specific behavior.
- Call the method on superclass references, allowing dynamic dispatch to the appropriate subclass implementation.
B3.2.2_2 Dynamic polymorphism via method overriding
Dynamic Polymorphism
- Description: Achieved through method overriding, where a subclass provides a specific implementation of a method defined in its superclass, resolved at runtime.
- Mechanism: When a method is called on a superclass reference, the actual object's class determines which overridden method executes.
class Animal:
def make_sound(self):
return "Some generic sound"
class Dog(Animal):
def make_sound(self): # Override method
return "Bark"
class Cat(Animal):
def make_sound(self): # Override method
return "Meow"
# Polymorphic behavior
animals = [Dog(), Cat()]
for animal in animals:
print(animal.make_sound()) # Output: Bark, Meow
def make_sound(self):
return "Some generic sound"
class Dog(Animal):
def make_sound(self): # Override method
return "Bark"
class Cat(Animal):
def make_sound(self): # Override method
return "Meow"
# Polymorphic behavior
animals = [Dog(), Cat()]
for animal in animals:
print(animal.make_sound()) # Output: Bark, Meow
- Explanation: The make_sound() method is overridden in Dog and Cat. At runtime, Python calls the appropriate method based on the object's actual type, demonstrating dynamic polymorphism.
B3.2.2_3 Static polymorphism for efficiency
Static Polymorphism
- Description: Achieved through method overloading or function overloading, resolved at compile time (less common in Python, which supports dynamic typing).
- Mechanism: Multiple methods with the same name but different parameters exist, and the correct one is chosen based on arguments at compile time.
- Python Limitation: Python does not support traditional method overloading due to dynamic typing, but static polymorphism can be simulated using default arguments or variable-length arguments.
class Calculator:
def add(self, a, b, c=0): # Simulate overloading with default argument
return a + b + c
calc = Calculator()
print(calc.add(2, 3)) # Output: 5 (uses a, b)
print(calc.add(2, 3, 4)) # Output: 9 (uses a, b, c)
def add(self, a, b, c=0): # Simulate overloading with default argument
return a + b + c
calc = Calculator()
print(calc.add(2, 3)) # Output: 5 (uses a, b)
print(calc.add(2, 3, 4)) # Output: 9 (uses a, b, c)
- Explanation: The add method uses a default parameter to handle different numbers of arguments, mimicking static polymorphism for efficiency by avoiding runtime resolution.
Constructing Polymorphic Code
Example (Python with Method Overriding)
class Shape:
def calculate_area(self):
return 0 # Default implementation
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self): # Override method
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self): # Override method
return self.width * self.height
# Polymorphic usage
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(f"Area: {shape.calculate_area():.2f}")
# Output: Area: 78.54 (Circle), Area: 24.00 (Rectangle)
def calculate_area(self):
return 0 # Default implementation
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self): # Override method
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self): # Override method
return self.width * self.height
# Polymorphic usage
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(f"Area: {shape.calculate_area():.2f}")
# Output: Area: 78.54 (Circle), Area: 24.00 (Rectangle)
- Construction:
- Define a Shape superclass with a generic calculate_area() method.
- Create subclasses Circle and Rectangle, overriding calculate_area() with specific formulas.
- Use a list of Shape objects to call calculate_area() polymorphically, demonstrating flexibility.
- Purpose: Reuses the Shape interface while allowing each subclass to provide its own area calculation, enhancing modularity and extensibility.
Use Case
- In a game development system, a Character superclass defines a attack() method, overridden by subclasses like Warrior (sword attack) and Mage (magic attack).
- Polymorphic code allows a list of Character objects to call attack() dynamically, reusing the interface while varying behavior, improving code efficiency and scalability.
B3.2.3 Explain abstraction in OOP (AO2)
B3.2.3_1 Significance for modular code
Abstraction
- Description: The process of hiding complex implementation details and exposing only essential features or interfaces to users, simplifying interaction with objects.
- Purpose: Reduces complexity, allowing developers to focus on high-level functionality without needing to understand internal workings.
Significance for Modular Code
- Promotes modularity by separating interface from implementation, enabling independent development and maintenance of components.
- Enhances code readability and usability by providing clear, simplified interfaces for complex systems.
- Facilitates teamwork, as developers can work on different modules using abstract interfaces without needing to know internal details.
- Example: A Database class provides a connect() method to access data, abstracting away the underlying SQL queries or network protocols, making the code modular and easier to use.
B3.2.3_2 Abstract classes for common interfaces
Abstract Classes
- Description: Classes that cannot be instantiated directly and are designed to serve as blueprints for subclasses, defining common methods or attributes that subclasses must implement.
- Purpose: Ensure a consistent interface across related classes, enforcing implementation of specific methods while allowing flexibility in how they are implemented.
- Mechanism in Python: Use the abc module (Abstract Base Class) to define abstract classes with abstract methods that subclasses must override.
- Example: An abstract Shape class defines a calculate_area() method that all shapes (e.g., Circle, Rectangle) must implement, ensuring a common interface.
Benefits
- Enforces a contract for subclasses, guaranteeing that key methods are implemented.
- Supports polymorphism by allowing objects of different subclasses to be treated uniformly through the abstract class interface.
- Simplifies code maintenance by centralizing shared behavior or requirements in the abstract class.
- Example: A game system uses an abstract Character class with an attack() method, ensuring all subclasses (Warrior, Mage) implement it, maintaining a consistent interface.
Applying Abstraction in Python
from abc import ABC, abstractmethod
# Abstract class
class Shape(ABC):
@abstractmethod
def calculate_area(self):
"""Calculate the area of the shape."""
pass
def display(self): # Concrete method
return f"Area: {self.calculate_area()}"
# Subclass implementing abstract method
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self): # Override abstract method
import math
return math.pi * self.radius ** 2
# Subclass implementing abstract method
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self): # Override abstract method
return self.width * self.height
# Usage
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(shape.display()) # Output: Area: 78.53981633974483, Area: 24
# Abstract class
class Shape(ABC):
@abstractmethod
def calculate_area(self):
"""Calculate the area of the shape."""
pass
def display(self): # Concrete method
return f"Area: {self.calculate_area()}"
# Subclass implementing abstract method
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self): # Override abstract method
import math
return math.pi * self.radius ** 2
# Subclass implementing abstract method
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self): # Override abstract method
return self.width * self.height
# Usage
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(shape.display()) # Output: Area: 78.53981633974483, Area: 24
- Explanation:
- Shape is an abstract class with an abstract method calculate_area(), requiring subclasses to implement it.
- Circle and Rectangle provide specific implementations of calculate_area(), adhering to the common interface.
- The display() method in Shape is a concrete method reused by all subclasses, demonstrating shared functionality.
- Abstraction allows shapes list to treat all objects as Shape, simplifying code and ensuring modularity.
Use Case
- In a payment processing system, an abstract Payment class defines an abstract process_payment() method.
- Subclasses like CreditCardPayment and PayPalPayment implement it differently, hiding complex details (e.g., API calls, encryption).
- Developers use the Payment interface to process payments without needing to know each payment type's internals, enhancing modularity and maintainability.
B3.2.4 Explain composition, aggregation in class relationships (AO2)
B3.2.4_1 Design objects with component objects
Composition
- Description: A strong "has-a" relationship where a class (whole) contains one or more instances of other classes (parts), and the parts are dependent on the whole for their lifecycle.
- Design: The containing class creates and manages the component objects, which are typically destroyed when the containing object is destroyed.
- Example: A Car class contains an Engine object; the engine is created when the car is instantiated and destroyed when the car is destroyed.
- Purpose: Models tight coupling where parts are integral to the whole, ensuring cohesive object design.
Aggregation
- Description: A weaker "has-a" relationship where a class (whole) contains references to other classes (parts), but the parts can exist independently of the whole.
- Design: The containing class references component objects, which may be created or destroyed independently.
- Example: A Library class contains a list of Book objects; books can exist outside the library (e.g., in another library or on their own).
- Purpose: Models loose coupling, allowing flexible and reusable object relationships.
Designing with Component Objects
- Use composition for strong ownership, where parts are exclusively tied to the whole (e.g., a House with Room objects).
- Use aggregation for loose relationships, where parts can be shared or exist independently (e.g., a Team with Player objects).
B3.2.4_2 Aggregation vs composition: independent vs tightly coupled subcomponents
Composition (Tightly Coupled Subcomponents)
- Characteristics:
- Parts are created and destroyed with the whole; they have no independent lifecycle.
- Strong ownership; parts cannot belong to multiple wholes simultaneously.
- Typically implemented by instantiating component objects within the containing class's constructor.
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine started"
class Car:
def __init__(self, model):
self.model = model
self.engine = Engine(200) # Composition: Engine created within Car
def drive(self):
return f"{self.model}: {self.engine.start()}"
car = Car("Toyota")
print(car.drive()) # Output: Toyota: Engine started
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return "Engine started"
class Car:
def __init__(self, model):
self.model = model
self.engine = Engine(200) # Composition: Engine created within Car
def drive(self):
return f"{self.model}: {self.engine.start()}"
car = Car("Toyota")
print(car.drive()) # Output: Toyota: Engine started
- Explanation: The Car owns an Engine, which is tightly coupled and exists only as part of the Car.
Aggregation (Independent Subcomponents)
- Characteristics:
- Parts have an independent lifecycle and can exist outside the whole.
- Loose coupling; parts can be shared across multiple wholes or reused elsewhere.
- Typically implemented by passing or referencing pre-existing component objects to the containing class.
class Employee:
def __init__(self, name):
self.name = name
def work(self):
return f"{self.name} is working"
class Department:
def __init__(self, name):
self.name = name
self.employees = [] # Aggregation: List of independent Employee objects
def add_employee(self, employee):
self.employees.append(employee)
def list_employees(self):
return [emp.work() for emp in self.employees]
# Create independent Employee objects
emp1 = Employee("Alice")
emp2 = Employee("Bob")
# Aggregate Employees in Department
dept = Department("Engineering")
dept.add_employee(emp1)
dept.add_employee(emp2)
print(dept.list_employees()) # Output: ['Alice is working', 'Bob is working']
def __init__(self, name):
self.name = name
def work(self):
return f"{self.name} is working"
class Department:
def __init__(self, name):
self.name = name
self.employees = [] # Aggregation: List of independent Employee objects
def add_employee(self, employee):
self.employees.append(employee)
def list_employees(self):
return [emp.work() for emp in self.employees]
# Create independent Employee objects
emp1 = Employee("Alice")
emp2 = Employee("Bob")
# Aggregate Employees in Department
dept = Department("Engineering")
dept.add_employee(emp1)
dept.add_employee(emp2)
print(dept.list_employees()) # Output: ['Alice is working', 'Bob is working']
- Explanation: Employee objects exist independently and are aggregated into the Department, which references them without controlling their lifecycle.
Key Differences
Aspect | Composition | Aggregation |
---|---|---|
Relationship | Strong "has-a", parts dependent on whole | Weak "has-a", parts independent of whole |
Lifecycle | Parts created/destroyed with whole | Parts exist independently |
Coupling | Tight coupling | Loose coupling |
Example | Car and Engine | Department and Employee |
- Use Case: In a school management system, use composition for a Classroom class that owns a Schedule object (the schedule is exclusive to the classroom). Use aggregation for a Course class that references Student objects (students can enroll in multiple courses or exist independently). This design ensures appropriate coupling and lifecycle management for modular, maintainable code.
B3.2.5 Explain OOP design patterns (AO2)
B3.2.5_1 Patterns: singleton, factory, observer
Singleton Pattern
- Description: Ensures a class has only one instance and provides a global point of access to it.
- Purpose: Useful for managing shared resources, such as a database connection or configuration manager, where multiple instances could cause conflicts or inefficiencies.
- Characteristics:
- Private constructor to prevent instantiation.
- Static method to access the single instance.
- Static variable to hold the instance.
- Example: A logging system where a single Logger instance writes to a file, ensuring consistent logging across the application.
Factory Pattern
- Description: Defines an interface or abstract class for creating objects, allowing subclasses to decide which class to instantiate.
- Purpose: Promotes loose coupling by delegating object creation to a factory, making it easier to extend or modify object types without changing client code.
- Characteristics:
- Factory class or method responsible for object creation.
- Hides instantiation logic from the client.
- Example: A VehicleFactory creating Car or Bike objects based on input parameters, allowing flexible vehicle type creation.
Observer Pattern
- Description: Defines a one-to-many dependency where multiple objects (observers) are notified of state changes in a subject.
- Purpose: Enables event-driven systems where changes in one object trigger updates in others, maintaining loose coupling.
- Characteristics:
- Subject maintains a list of observers.
- Observers implement an update method.
- Subject notifies observers of changes.
- Example: A news feed where subscribers (observers) receive updates when new articles (subject) are published.
B3.2.5_2 Solve recurring programming challenges
Singleton Pattern
- Challenge: Managing a single instance of a resource to avoid conflicts or redundant resource usage.
- Solution: Restricts instantiation to one object, ensuring centralized access and consistency.
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def some_method(self):
return "Singleton instance method"
# Usage
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1.some_method()) # Output: Singleton instance method
print(singleton1 is singleton2) # Output: True (same instance)
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def some_method(self):
return "Singleton instance method"
# Usage
singleton1 = Singleton()
singleton2 = Singleton()
print(singleton1.some_method()) # Output: Singleton instance method
print(singleton1 is singleton2) # Output: True (same instance)
- Use Case: A configuration manager ensuring a single instance loads settings once, used across the application.
Factory Pattern
- Challenge: Creating objects of different types without tightly coupling client code to specific classes.
- Solution: Centralizes object creation in a factory, allowing dynamic instantiation based on input or conditions.
class Car:
def drive(self):
return "Driving a car"
class Bike:
def drive(self):
return "Riding a bike"
class VehicleFactory:
@staticmethod
def create_vehicle(vehicle_type):
if vehicle_type == "car":
return Car()
elif vehicle_type == "bike":
return Bike()
raise ValueError("Unknown vehicle type")
# Usage
factory = VehicleFactory()
vehicle = factory.create_vehicle("car")
print(vehicle.drive()) # Output: Driving a car
def drive(self):
return "Driving a car"
class Bike:
def drive(self):
return "Riding a bike"
class VehicleFactory:
@staticmethod
def create_vehicle(vehicle_type):
if vehicle_type == "car":
return Car()
elif vehicle_type == "bike":
return Bike()
raise ValueError("Unknown vehicle type")
# Usage
factory = VehicleFactory()
vehicle = factory.create_vehicle("car")
print(vehicle.drive()) # Output: Driving a car
- Use Case: A game spawning different enemy types (Orc, Troll) using an EnemyFactory, simplifying addition of new types.
Observer Pattern
- Challenge: Coordinating updates across multiple objects when one object's state changes, without tight coupling.
- Solution: Allows observers to subscribe to a subject, receiving notifications when the subject's state changes.
class Subject:
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, message):
for observer in self._observers:
observer.update(message)
class Observer:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} received: {message}")
# Usage
subject = Subject()
observer1 = Observer("Observer1")
observer2 = Observer("Observer2")
subject.attach(observer1)
subject.attach(observer2)
subject.attach(observer2)
subject.notify("News update!")
# Output: Observer1 received: News update!
# Observer2 received: News update!
def __init__(self):
self._observers = []
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self, message):
for observer in self._observers:
observer.update(message)
class Observer:
def __init__(self, name):
self.name = name
def update(self, message):
print(f"{self.name} received: {message}")
# Usage
subject = Subject()
observer1 = Observer("Observer1")
observer2 = Observer("Observer2")
subject.attach(observer1)
subject.attach(observer2)
subject.attach(observer2)
subject.notify("News update!")
# Output: Observer1 received: News update!
# Observer2 received: News update!
- Use Case: A stock market app where multiple displays (observers) update when stock prices (subject) change
Significance
- Design patterns provide proven solutions to common problems, improving code structure, maintainability, and scalability
- They address recurring challenges like resource management (Singleton), object creation (Factory), and event handling (Observer), ensuring robust and flexible designs
- By using these patterns, developers can leverage collective wisdom and best practices, avoiding reinvention of solutions for well-understood problems