Object-Oriented Programming
Introduction
As code complexity grows, managing and understanding functionality becomes increasingly challenging. Object-Oriented Programming (OOP) addresses this by breaking down large tasks into smaller, modular components.
Example: Self-Driving Car
Imagine developing software for a self-driving car. Instead of coding everything in a single block, you can organize the project into modules like camera systems, lane detection, navigation, and battery management. Each module is easier to develop, test, and maintain. Moreover, these modules can be reused in other applications, such as drone control, if similar functionalities are required.
How OOP Helps
OOP enhances modularity by allowing each module (or class) to operate independently. Different team members can work on separate classes, improving scalability and collaboration. Think of it like running a restaurant: instead of handling every task yourself, roles like chef, waiter, and cleaner are delegated to specialized staff, making operations efficient and scalable.
Core Concepts of OOP
Object-Oriented Programming (OOP) is based on the concept of objects, which are instances of classes (modules).
Classes and Objects:
- Class: A blueprint defining attributes (properties) and methods (actions) for objects.
- Object: An instance of a class, created using the class blueprint.
Objects combine attributes (what they "have") and methods (what they "do").
Attributes and Methods:
- Attributes: Properties specific to the object (e.g., resolution, lens type).
- Methods: Functions defining object behavior (e.g., capturing images, detecting objects).
For instance, a Camera
class in a self-driving car may define attributes like resolution and lens type, along with methods for capturing images or detecting objects. Multiple objects (e.g., front and rear cameras) can be created from this class, each with unique attribute values.
OOP makes it easy to reuse and extend code, simplifying the development of complex systems like self-driving cars.
Class Definition
Classes define the structure of objects, specifying their attributes and methods. Use the syntax:
class ClassName:
Just like with functions, the code inside the class is indented. Classes can be defined in the same script or in a separate script file, which can then be included using import
.
Changes to the class definition only apply to new objects, meaning that all objects from the old definition must be removed from the workspace.
A class for a Camera
module in a self-driving car could be defined as follows:
Info
pass
is a placeholder that indicates that no action is executed. It is used to define an empty code block.
You can create Camera objects — instances of the class — by assigning them using Camera()
.
class Camera:
pass
# Creating instances of the Camera class
front_camera = Camera()
rear_camera = Camera()
print(type(front_camera))
print(type(rear_camera))
front_camera
and rear_camera
are instances of the Camera
class, even though the class currently has no defined attributes or methods.
Variables or attributes can be defined within a class. As a result, all objects of the class will have this variable with the specified value. The attribute can be accessed using a dot operator.
class Camera:
# Defining a class attribute
lens_type = "wide-angle"
# Creating an instance of the Camera class
front_camera = Camera()
# Accessing the attribute using the dot operator
print(front_camera.lens_type)
lens_type
attribute is a class attribute, meaning it is shared by all instances of the class. Changing the value of the class attribute (
Camera.lens_type
) affects all objects created from the class, as they share the same attribute.
class Camera:
# Defining a class attribute
lens_type = "wide-angle"
# Creating two instances of the Camera class
front_camera = Camera()
rear_camera = Camera()
# Accessing the shared attribute
print(f"Front camera lens type: {front_camera.lens_type}")
print(f"Rear camera lens type: {rear_camera.lens_type}")
# Changing the class attribute
Camera.lens_type = "telephoto"
# Both instances reflect the updated value
print(f"Front camera lens type: {front_camera.lens_type}")
print(f"Rear camera lens type: {rear_camera.lens_type}")
Initialization Method
The __init__(self, property)
method is called each time a new object is instantiated.
Attributes are characteristics that describe an object (e.g., camera_type, lens_type). Within the __init__
method, the term self
refers to the object being created, and additional attributes can be added to it. This initialization method ensures that the Camera object is set up with specific values (e.g., camera_type and lens_type) right when it is created. An error message occurs if these specific values are missing.
class Camera:
# Setting the attributes camera_type and lens_tpye
def __init__(self, camera_type, lens_type):
self.camera_type = camera_type
self.lens_type = lens_type
# Creating an instance of the Camera class
front_camera = Camera("front","wide-angle")
print(f"{front_camera.camera_type} and {front_camera.lens_type}")
# Creating another instance of the Camera class
rear_camera = Camera()
Initialization Method
Why does this code generate an error message? Identify the cause and modify the code to ensure it runs without errors.
Initialization parameters allow optional customization when creating an object. If no values are provided, default values will be used.
class Camera:
# Setting the attributes camera_type, lens_tpye and resolution
def __init__(self, camera_type, lens_type, resolution=1080):
self.camera_type = camera_type
self.lens_type = lens_type
self.resolution = resolution
# Creating an instance of the Camera class w/o defining the resolution
front_camera = Camera("front","wide-angle")
print(
f"{front_camera.camera_type}, {front_camera.lens_type} "
f"and {front_camera.resolution}"
)
# Creating an instance of the Camera class with defining the resolution
front_camera = Camera("front","wide-angle",720)
print(
f"{front_camera.camera_type}, {front_camera.lens_type} "
f"and {front_camera.resolution}"
)
Encapsulation
Encapsulation separates what a class shows (public properties and methods) from its hidden internal details (private implementation). If data is public, it can be directly accessed and changed using the dot operator.
class Camera:
# Setting the attributes camera_type and lens_tpye
def __init__(self, camera_type, lens_type):
self.camera_type = camera_type
self.lens_type = lens_type
# Creating an instance of the Camera class
front_camera = Camera("front","wide-angle")
print(front_camera.camera_type)
# Changing the attribute camera_type
front_camera.camera_type = "rear"
print(front_camera.camera_type)
__name
), restricts access to the private attribute.
class Camera:
# Setting the private attributes camera_type and lens_tpye
def __init__(self, camera_type, lens_type):
self.__camera_type = camera_type
self.__lens_type = lens_type
# Creating an instance of the Camera class
front_camera = Camera("front","wide-angle")
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[8], line 2
1 # Incorrect usage: Accessing the attribute camera_type
----> 2 print(front_camera.camera_type)
AttributeError: 'Camera' object has no attribute 'camera_type'
By defining appropriate methods, interface functions can be provided to allow the user to modify and read private attributes (e.g., change_camera_type, display_data). The dot operator is used when calling the function.
class Camera:
# Setting the private attributes camera_type and lens_tpye
def __init__(self, camera_type, lens_type):
self.__camera_type = camera_type
self.__lens_type = lens_type
# Creating the method change_camera_type
def change_camera_type(self, camera_type_new):
self.__camera_type = camera_type_new
# Creating the method display
def disp(self):
print(f"Object of Camera Class:\n")
print(f"Camera Type: {self.__camera_type}")
print(f"Lens Type: {self.__lens_type}")
Encapsulation
Create a new instance of the Camera
class (e.g., front_camera
), then update its camera_type
attribute.
Definition of the Data Structure
The structure of data — such as data types and dimensions — can still be freely chosen by the user, which may lead to undesired behavior. For example, the attribute camera_type might be assigned a list (list) instead of a string (str), or resolution might be given a string (str) instead of an integer (int) without any warning about the incorrect input. Also a method could receive an attribute with the correct type but an invalid value (e.g., a negative number where only positives make sense).
Causing errors for others
Up until now, you have encountered various different errors.
For example, we encountered a NameError
when misspelling a
variable name, a TypeError
when using an incorrect data type,
or a IndentationError
when the code was not properly indented.
Now it's your time to raise an error (or often called exception) yourself, which can be a helpful and informative way to guide the user in case of incorrect use. Here is a comprehensive list of all built-in exceptions in Python.
To prevent this, data structures can be validated within the class definition. If the input is incorrect, a general error message can be raised using:
raise ValueError("Error message")
To specifically check if an attribute is of an incorrect type (e.g., passing a string when a number is expected), you can raise a TypeError
with a descriptive message:
raise TypeError("Error message")
You can check the data type with the command:
isinstance(variable, data_type)
Possible attributes of a category can also be defined in a list, for example:
__camera_types = ["front", "rear", "left", "right", "top", "not stated"]
orientation = ["horizontal", "vertical", "not stated"]
By validating the data types during object creation, you can ensure the object behaves as expected and avoid unexpected errors later in the program.
class Camera:
__camera_types = ["front","rear","left","right","top","not stated"]
# Setting the attributes with restrictions
def __init__(
self, camera_type, lens_type, resolution, orientation="not stated"
):
if camera_type not in self.__camera_types:
# Check if camera_type is one attribute from list.
raise ValueError(
f"Camera type must be one of {self.__camera_types}."
)
if not isinstance(lens_type, str):
# Check if lens_type is a string.
raise TypeError("Lens type must be a string.")
if not isinstance(resolution, int):
# Check if resolution is an integer.
raise TypeError("Resolution must be an integer.")
if orientation not in ["horizontal","vertical","not stated"]:
# Check if orientation is one attribute from list.
raise ValueError(
"Orientation must be either 'horizontal', "
"'vertical' or 'not stated'."
)
self.__camera_type = camera_type
self.__lens_type = lens_type
self.resolution = resolution
self.orientation = orientation
# Creating the method change_camera_type with data type restrictions
def change_camera_type(self, camera_type_new):
if camera_type_new not in self.__camera_types:
raise ValueError(
f"Camera type must be one of {self.__camera_types}."
)
self.__camera_type = camera_type_new
# Creating the method set_orientation with data type restrictions
def set_orientation(self, orientation):
if orientation not in ["horizontal","vertical","not stated"]:
raise ValueError(
"Orientation must be either 'horizontal', "
"'vertical' or 'not stated'."
)
self.orientation = orientation
# Creating the method display
def disp(self):
print(f"Object of Camera Class\n:")
print(f"Camera Type: {self.__camera_type}")
print(f"Lens Type: {self.__lens_type}")
print(f"Resolution: {self.resolution}p")
print(f"Orientation: {self.orientation}")
Data Structure
Create two instances of the Camera
class. Ensure one is created correctly, and intentionally cause an error with the other.