Unit 5 - Notes

INT108

Unit 5: Classes and objects; Object oriented programming terminology

1. Introduction to OOP Terminology

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects," which can contain data and code: data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods).

  • Class: A user-defined prototype (blueprint) for an object that defines a set of attributes that characterize any object of the class.
  • Object: A unique instance of a data structure that's defined by its class. An object comprises both data members (class variables and instance variables) and methods.
  • Instance: An individual object of a certain class.
  • Method: A special kind of function that is defined in a class definition.
  • Attribute: A variable that belongs to an object or class.

2. Creating Classes

Defining a class in Python uses the class keyword followed by the class name and a colon. By convention, class names use PascalCase (e.g., MyClass, StudentDetails).

The __init__ Method (Constructor)

The __init__ method is a special method automatically called when a new instance of the class is created. It is used to initialize the object’s state.

The self Parameter

  • self represents the instance of the class.
  • By using the self keyword, we can access the attributes and methods of the class in Python.
  • It binds the attributes with the given arguments.
  • Note: self is not a keyword; you could call it this or me, but self is the strict convention.

Syntax and Example

PYTHON
class Dog:
    # Class Attribute (shared by all instances)
    species = "Canis familiaris"

    # The Initializer / Constructor
    def __init__(self, name, age):
        # Instance Attributes (unique to each instance)
        self.name = name
        self.age = age

    # A generic method
    def description(self):
        return f"{self.name} is {self.age} years old."


3. Creating Instance Objects

Creating an object is also known as instantiation. To create an instance of a class, you call the class using its name and pass in any arguments that the __init__ method accepts.

PYTHON
# Creating two distinct objects (instances) of the Dog class
dog1 = Dog("Buddy", 4)
dog2 = Dog("Miles", 9)


4. Accessing Attributes

Attributes can be accessed using the dot operator (.). You can access both instance attributes and methods.

Accessing Data

PYTHON
print(dog1.name)  # Output: Buddy
print(dog2.age)   # Output: 9
print(dog1.species) # Output: Canis familiaris

Calling Methods

PYTHON
print(dog1.description()) 
# Output: Buddy is 4 years old.

Modifying Attributes

You can modify the value of attributes directly:

PYTHON
dog1.age = 5
print(dog1.age) # Output: 5


5. Class Inheritance

Inheritance allows us to define a class that inherits all the methods and properties from another class. This promotes code reusability.

  • Parent Class (Base Class): The class being inherited from.
  • Child Class (Derived Class): The class that inherits from another class.

Syntax

PYTHON
class ParentClass:
    # Parent body
    pass

class ChildClass(ParentClass):
    # Child body
    pass

Example: Single Inheritance

PYTHON
# Parent Class
class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def start_engine(self):
        return "Engine started."

# Child Class
class Car(Vehicle):
    def drive(self):
        return "The car is moving."

# Usage
my_car = Car("Toyota")
print(my_car.brand)          # Accessed from Parent: Toyota
print(my_car.start_engine()) # Accessed from Parent: Engine started.
print(my_car.drive())        # Accessed from Child: The car is moving.


6. Overriding Methods

Method overriding occurs when a child class provides a specific implementation of a method that is already defined in its parent class. The method in the child class must have the same name as the method in the parent class.

Using super()

The super() function returns a temporary object of the superclass that allows you to call methods of the base class. This is useful if you want to extend the functionality of the parent method rather than replace it entirely.

Example

PYTHON
class Animal:
    def speak(self):
        return "Some generic sound"

class Dog(Animal):
    # Overriding the 'speak' method
    def speak(self):
        return "Bark!"

class Cat(Animal):
    def speak(self):
        # Calling the parent method, then adding to it
        original_sound = super().speak()
        return f"{original_sound} but specifically Meow!"

a = Animal()
d = Dog()
c = Cat()

print(a.speak()) # Output: Some generic sound
print(d.speak()) # Output: Bark!
print(c.speak()) # Output: Some generic sound but specifically Meow!


7. Data Hiding (Encapsulation)

Data hiding ensures that the internal implementation details of a class are hidden from the outside. In Python, this is achieved through naming conventions.

Levels of Access

  1. Public: Accessible from anywhere (default). Example: self.name.
  2. Protected: Indicated by a single underscore _. Conventionally implies "internal use only" but is technically accessible. Example: self._internal_id.
  3. Private: Indicated by double underscores __. Python performs "name mangling" to make it harder to access these from outside the class. Example: self.__password.

Example of Private Attributes

PYTHON
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner       # Public
        self.__balance = balance # Private

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"Added {amount}")

    def get_balance(self):
        return self.__balance

account = BankAccount("Alice", 1000)

print(account.owner)        # Allowed: Alice
print(account.get_balance()) # Allowed: 1000

# print(account.__balance)  # ERROR: AttributeError

Accessing Private Members (Name Mangling)

Although meant to be hidden, private variables can technically be accessed using the mangled name: _ClassName__variableName.

PYTHON
print(account._BankAccount__balance) # Output: 1000 (Not recommended)


8. Function Overloading

Important Note: Python does not support traditional function overloading (defining multiple methods with the same name but different parameters) like C++ or Java. If you define two methods with the same name, the second one simply overwrites the first.

However, we can simulate overloading using:

  1. Default arguments (None).
  2. Variable-length arguments (*args).
  3. Multiple dispatch libraries (external).

Simulating Overloading with Default Arguments

This is the most "Pythonic" way to handle methods that can accept different numbers of arguments.

PYTHON
class Calculator:
    # One method handling 1, 2, or 3 arguments
    def add(self, a, b=None, c=None):
        if b is not None and c is not None:
            return a + b + c
        elif b is not None:
            return a + b
        else:
            return a

calc = Calculator()

print(calc.add(5))       # Output: 5
print(calc.add(5, 10))   # Output: 15
print(calc.add(5, 10, 2))# Output: 17

Simulating Overloading with *args

Using *args allows passing any number of positional arguments.

PYTHON
class Printer:
    def print_data(self, *args):
        if len(args) == 1:
            print(f"Printing single item: {args[0]}")
        elif len(args) == 2:
            print(f"Printing pair: {args[0]} and {args[1]}")
        else:
            print("Invalid arguments")

p = Printer()
p.print_data("Hello")           # Output: Printing single item: Hello
p.print_data("Hello", "World")  # Output: Printing pair: Hello and World