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
selfrepresents the instance of the class.- By using the
selfkeyword, we can access the attributes and methods of the class in Python. - It binds the attributes with the given arguments.
- Note:
selfis not a keyword; you could call itthisorme, butselfis the strict convention.
Syntax and Example
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.
# 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
print(dog1.name) # Output: Buddy
print(dog2.age) # Output: 9
print(dog1.species) # Output: Canis familiaris
Calling Methods
print(dog1.description())
# Output: Buddy is 4 years old.
Modifying Attributes
You can modify the value of attributes directly:
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
class ParentClass:
# Parent body
pass
class ChildClass(ParentClass):
# Child body
pass
Example: Single Inheritance
# 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
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
- Public: Accessible from anywhere (default). Example:
self.name. - Protected: Indicated by a single underscore
_. Conventionally implies "internal use only" but is technically accessible. Example:self._internal_id. - 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
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.
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:
- Default arguments (
None). - Variable-length arguments (
*args). - 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.
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.
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