Worksheet 6 - Python Object Oriented Programming Introduction
Here are some beginner-friendly examples to introduce Object-Oriented Programming (OOP) concepts in Python. Each example includes example code and a simple challenge to reinforce learning.
Classes and Objects
Concept: A class is a blueprint for creating objects (instances). Think of a cookie cutter to make biscuit shapes.
class Dog:
def __init__(self, name):
self.name = name
# Creating an object
my_dog = Dog("Buddy")
print(my_dog.name) # Output: Buddy
Challenge: Create a class Cat with an attribute name and create an instance of it. Print the name of the cat.
Methods
Concept: Methods are functions defined inside a class.
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
return "Woof!"
my_dog = Dog("Buddy")
print(my_dog.bark()) # Output: Woof!
Challenge: Add a method sit that returns "Sitting" when called.
Inheritance
Concept: Inheritance allows a class to inherit attributes and methods from another class.
class Animal:
def speak(self):
return "Animal sound"
class Dog(Animal):
def speak(self):
return "Woof!"
my_dog = Dog()
print(my_dog.speak()) # Output: Woof!
Challenge: Create a class Cat that inherits from Animal and overrides the speak method to return "Meow!".
Encapsulation
Concept: Encapsulation restricts access to certain attributes or methods.
Why Encapsulation?
- Data Protection: Prevent accidental or malicious changes to critical data.
- Validation: Enforce rules (e.g., valid ranges) when setting attributes.
- Cleaner Code: Centralize how attributes are accessed and modified, making it easier to update or debug.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute
def get_balance(self):
return self.__balance
account = BankAccount(100)
print(account.get_balance()) # Output: 100
Challenge: Try to directly access __balance from outside the class and observe the error.
Encapsulation using Set and Get methods
Explicit Methods (set/get):
- Useful when teaching OOP concepts to beginners, as the method names make the behavior explicit.
- Preferred in codebases where getters and setters are the convention (e.g., Java).
class Thermostat:
def __init__(self):
self._temperature = 20 # Default temperature in Celsius
self._humidity = 50 # Default humidity in percentage
# Getter and Setter for temperature
def getTemperature(self):
return self._temperature
def setTemperature(self, new_temp):
if -10 <= new_temp <= 50:
self._temperature = new_temp
else:
print("Temperature out of range! Must be between -10 and 50°C.")
# Getter and Setter for humidity
def getHumidity(self):
return self._humidity
def setHumidity(self, new_humidity):
if 0 <= new_humidity <= 100:
self._humidity = new_humidity
else:
print("Humidity out of range! Must be between 0 and 100%.")
# How to use Set and Get methods==
thermostat = Thermostat()
# Get and set temperature
print(thermostat.getTemperature()) # Output: 20
thermostat.setTemperature(25) # Valid update
print(thermostat.getTemperature()) # Output: 25
thermostat.setTemperature(60) # Output: Temperature out of range! Must be between -10 and 50°C.
# Get and set humidity
print(thermostat.getHumidity()) # Output: 50
thermostat.setHumidity(70) # Valid update
print(thermostat.getHumidity()) # Output: 70
thermostat.setHumidity(110) # Output: Humidity out of range! Must be between 0 and 100%.
Basic Bank Account Example
This example ensures that users cannot directly access or modify the account balance but must use specific methods.
class BankAccount:
def __init__(self, owner, balance):
self.__owner = owner # Private attribute
self.__balance = balance # Private attribute
# Getter for balance
def get_balance(self):
return self.__balance
# Setter for balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
print("Deposit amount must be positive!")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
print("Invalid withdrawal amount!")
# Using the class
account = BankAccount("Alice", 1000)
print(account.get_balance()) # Output: 1000
account.deposit(200)
print(account.get_balance()) # Output: 1200
account.withdraw(1500) # Output: Invalid withdrawal amount!
Car Speed Control Example
This example demonstrates encapsulating the speed of a car to ensure it stays within valid limits.
class Car:
def __init__(self, model):
self.__model = model # Private attribute
self.__speed = 0 # Private attribute
# Getter for speed
def get_speed(self):
return self.__speed
# Setter for speed
def set_speed(self, speed):
if 0 <= speed <= 200:
self.__speed = speed
else:
print("Speed must be between 0 and 200 km/h.")
# Using the class
car = Car("Toyota")
car.set_speed(100)
print(f"The car's speed is {car.get_speed()} km/h.") # Output: The car's speed is 100 km/h.
car.set_speed(250) # Output: Speed must be between 0 and 200 km/h.
Employee Management Example
This example encapsulates an employee's salary and restricts direct modification.
class Employee:
def __init__(self, name, salary):
self.__name = name
self.__salary = salary # Private attribute
# Getter for salary
def get_salary(self):
return self.__salary
# Setter for salary with validation
def set_salary(self, new_salary):
if new_salary > 0:
self.__salary = new_salary
else:
print("Salary must be positive!")
# Using the class
employee = Employee("John", 50000)
print(f"{employee.get_salary()}") # Output: 50000
employee.set_salary(55000)
print(f"{employee.get_salary()}") # Output: 55000
employee.set_salary(-1000) # Output: Salary must be positive!
Encapsulation with Read-Only Attributes
In this example, the getter provides read-only access to private data.
class ReadOnlyExample:
def __init__(self, data):
self.__data = data # Private attribute
# Read-only access
def get_data(self):
return self.__data
# Using the class
example = ReadOnlyExample("This is private data")
print(example.get_data()) # Output: This is private data
# example.__data = "New data" # This will raise an AttributeError
Polymorphism
Concept: Polymorphism allows different classes to be treated as instances of the same class through a common interface.
- Think of polymorphism like a universal remote control.
- You can use the same remote to control different devices like a TV, DVD player, or sound system.
- Even though the devices are different, they all respond to the same commands (like power, volume, and play).
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
def animal_sound(animal):
print(animal.speak())
my_dog = Dog()
my_cat = Cat()
animal_sound(my_dog) # Output: Woof!
animal_sound(my_cat) # Output: Meow!
Challenge: Create a new class Cow with a speak method returning "Moo!". Test it with the animal_sound function.
Class Variables
Concept: Class variables are shared among all instances of a class.
class Dog:
species = "Canis familiaris" # Class variable
def __init__(self, name):
self.name = name
my_dog = Dog("Buddy")
print(my_dog.species) # Output: Canis familiaris
Challenge: Change the species for the Dog class and print it from a different instance.
Static Methods
Concept: Static methods do not modify class or instance state.
- A static method in Python is a method that belongs to a class rather than any specific instance of that class.
- It does not require an instance to be called and cannot modify the instance or class state.
- Static methods are defined using the @staticmethod decorator and are used to perform operations that are related to the class but do not need access to instance or class variables.
class Math:
@staticmethod
def add(a, b):
return a + b
print(Math.add(5, 3)) # Output: 8
Challenge: Create a static method subtract that returns the difference of two numbers.
Class Methods
Concept: Class methods can access class variables and modify class state.
- Class methods in Python are special types of methods that belong to the class itself rather than to instances (objects) of the class.
- They are defined using the @classmethod decorator and take the class as their first parameter, conventionally named cls.
- This allows class methods to access class-level data.
class Dog:
count = 0. # Class variable 'count'
def __init__(self, name):
self.name = name
Dog.count += 1 # Class variable 'count' increases by 1 every time a new instance is created.
@classmethod
def total_dogs(cls):
return cls.count
my_dog1 = Dog("Buddy")
my_dog2 = Dog("Max")
print(Dog.total_dogs()) # Output: 2
Challenge: Create another class method reset_count to reset the dog count to zero.
str Methods
Concept: These methods define how an object is represented as a string.
- __str__: This method is called when you use print() on an instance of the class.
- It is meant to be readable and provide a friendly representation of the object.
class Dog:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Dog: {self.name}" # User-friendly string representation
my_dog = Dog("Buddy")
print(my_dog) # Output: Dog: Buddy
Challenge: Implement the __str__ method for a class Cat to return a string representation that includes the class name and attributes.
Properties
Concept: Properties allow for controlled access to an attribute.
- In Python, properties are a way to manage the attributes of a class by defining methods that get or set the values of those attributes.
- They allow you to encapsulate the access to instance variables while providing a clean and readable interface.
Why Use @property?
- Readability: Access and modify attributes as if they are variables (obj.attr instead of obj.get_attr()).
- Encapsulation: You can add validation or other logic while keeping the attribute private (e.g., _attr).
- Flexibility: You can later change how the attribute is computed without modifying external code that uses the class.
class Dog:
def __init__(self, name):
self._name = name # Protected attribute
@property
def name(self):
return self._name.title()
def getName(self):
"""Get the dog's name in title case."""
return self._name.title()
def setName(self, new_name):
"""Set the dog's name, ensuring it's in lower case."""
if not new_name:
raise ValueError("Name cannot be empty!")
self._name = new_name.lower() # Store as lower case
my_dog = Dog("buddy")
print(my_dog.name) # Output: Buddy
# Using the setter to change the name
my_dog.setName("Rex") # Changing the name
# Using the getter again to see the updated name
print(my_dog.getName()) # Output: Rex
Challenge: Add a setter for the name property to allow changing the name while ensuring it's always capitalised.
@temperature.setter
The @temperature.setter is a companion to the @property decorator. While @property is used to define a getter for an attribute, the @temperature.setter decorator is used to define the setter method, which is triggered when you assign a value to the property.
This makes it possible to control how the value of a property is updated, including adding validation or other logic, while still allowing the attribute to be accessed and modified like a normal variable.
How @temperature.setter Works
- Links to the Property: The @temperature.setter decorator is associated with the property defined by @property and must have the same name (in this case, temperature).
- Defines the Setter Logic: You write the logic for what happens when a new value is assigned to the property.
class Thermostat:
def __init__(self):
self._temperature = 20 # Default value
@property
def temperature(self):
# This method is called when accessing thermostat.temperature
return self._temperature
@temperature.setter
def temperature(self, new_temp):
# This method is called when setting thermostat.temperature = value
if -10 <= new_temp <= 50:
self._temperature = new_temp
else:
print("Temperature out of range! Must be between -10 and 50°C.")
# Usage
thermostat = Thermostat()
# Accessing the temperature (calls the getter)
print(thermostat.temperature) # Output: 20
# Setting a valid temperature (calls the setter)
thermostat.temperature = 25
print(thermostat.temperature) # Output: 25
# Attempting to set an invalid temperature
thermostat.temperature = 60 # Output: Temperature out of range! Must be between -10 and 50°C.
Example with Multiple Variables
Here’s an example of a class representing a thermostat that manages both temperature and humidity:
class Thermostat:
def __init__(self):
self._temperature = 20 # Default temperature in Celsius
self._humidity = 50 # Default humidity in percentage
# Temperature property
@property
def temperature(self):
return self._temperature
@temperature.setter
def temperature(self, new_temp):
if -10 <= new_temp <= 50:
self._temperature = new_temp
else:
print("Temperature out of range! Must be between -10 and 50°C.")
# Humidity property
@property
def humidity(self):
return self._humidity
@humidity.setter
def humidity(self, new_humidity):
if 0 <= new_humidity <= 100:
self._humidity = new_humidity
else:
print("Humidity out of range! Must be between 0 and 100%.")
# Usage
thermostat = Thermostat()
# Access and set temperature
print(thermostat.temperature) # Output: 20
thermostat.temperature = 25 # Update temperature
print(thermostat.temperature) # Output: 25
thermostat.temperature = 60 # Output: Temperature out of range! Must be between -10 and 50°C.
# Access and set humidity
print(thermostat.humidity) # Output: 50
thermostat.humidity = 70 # Update humidity
print(thermostat.humidity) # Output: 70
thermostat.humidity = 110 # Output: Humidity out of range! Must be between 0 and 100%.