Worksheet 6 - Python Object Oriented Programming Introduction

From Sensors in Schools
Jump to navigation Jump to search

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%.