4.1. Introduction to Classes and Objects#

Python provides us with the flexibility to define custom types, commonly known as programmer-defined types or classes. With classes, we can construct intricate data structures, encapsulate data, and define specific operations applicable to that data. As an illustration, consider the creation of a class named “Point” designed to represent a two-dimensional point in space [Downey, 2015, Python Software Foundation, 2023].

Definition - Classes

A class serves as a design or pattern used to generate objects within object-oriented programming (OOP). It outlines both the format and actions that instances belonging to that class will exhibit. Within a class, data attributes and operational functions (methods) are packaged together. Essentially, a class acts as a custom-made data category.

To put it in simpler words, consider a class as a cookie cutter that outlines the appearance and qualities of cookies you can create. It lays down the shared traits and actions that any object stemming from that class ought to possess [Lin et al., 2022].

Definition - Objects

An object represents a concrete manifestation of a class. By generating an object, you’re producing a distinct occurrence that adheres to the design and actions outlined by the class. Objects are the tangible components that engage with your program, capable of storing information and executing tasks in line with the methods specified in the class.

Drawing parallels with the cookie cutter analogy, objects are akin to the real cookies you craft using the cookie cutter. Each cookie shares the identical shape and attributes as established by the cookie cutter (class) [Lin et al., 2022].

Here’s a simple example to illustrate the concepts [Matthes, 2015]:

# Defining a class named "Car"
class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def drive(self):
        print(f"The {self.make} {self.model} is cruising down the road.")

Explanation: This code defines a class named Car. The class has a constructor (__init__) method that takes two parameters (make and model). Inside the constructor, the parameters are used to initialize two instance variables (self.make and self.model). The class also includes a method named drive which prints a message indicating that a specific car is cruising down the road.

# Creating objects (instances) of the Car class
car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Accord")
car3 = Car("Ford", "Mustang")

Explanation: In this part, three instances (objects) of the Car class are created. Each instance is created by calling the class name with the required parameters (make and model). car1 has a make “Toyota” and model “Camry”, car2 has a make “Honda” and model “Accord”, and car3 has a make “Ford” and model “Mustang”.

# Using the objects
car1.drive()  # Output: The Toyota Camry is cruising down the road.
car2.drive()  # Output: The Honda Accord is cruising down the road.
car3.drive()  # Output: The Ford Mustang is cruising down the road.
The Toyota Camry is cruising down the road.
The Honda Accord is cruising down the road.
The Ford Mustang is cruising down the road.

Explanation: Here, the objects (car1, car2, and car3) are utilized by calling the drive method on each of them. This results in the specified messages being printed, indicating that each car is cruising down the road. The messages are customized based on the make and model attributes that were set during object creation.

Benefits of Using Classes and Objects:

In object-oriented programming, the use of classes and objects provides several distinct advantages that contribute to the overall structure, organization, and efficiency of code. By encapsulating data and behavior into reusable units, classes and objects promote modularity and enhance code maintainability. Below are some key benefits of utilizing classes and objects in software development [Beck, 1997, Rini, 2023]:

  1. Modularity and Encapsulation: Classes encapsulate data and methods that operate on that data, allowing for a clear separation of concerns. This separation facilitates modular design, where different parts of a program can be developed and maintained independently. This modular structure simplifies code management, debugging, and troubleshooting.

  2. Code Reusability: Classes and objects promote code reuse through inheritance and composition. Inheritance allows new classes to inherit attributes and behaviors from existing classes, reducing the need to rewrite common functionality. Composition, on the other hand, enables the creation of complex objects by combining simpler, reusable components.

  3. Abstraction: Abstraction involves simplifying complex reality by modeling classes after real-world entities or concepts. This abstraction enables programmers to focus on essential characteristics and ignore unnecessary details. By modeling real-world objects as classes, developers can create a higher level of understanding and representation in the codebase.

  4. Maintainability and Scalability: Object-oriented programming supports the development of maintainable and scalable codebases. Changes or updates to one class are less likely to impact other parts of the program, reducing the risk of unintended consequences. This separation of concerns makes it easier to extend and modify software over time.

  5. Collaborative Development: Using classes and objects enhances collaborative development by allowing multiple programmers to work on different components simultaneously. With well-defined interfaces between classes, team members can develop components independently and integrate them seamlessly into the larger system.

  6. Data Integrity and Security: Encapsulation ensures that data is only accessible through well-defined methods, preventing unauthorized modification or access. This safeguards the integrity and security of sensitive data within the program.

  7. Readability and Maintainable Code: Classes and objects promote a more intuitive and organized structure in the code. Clear naming conventions for classes and methods enhance readability, making it easier for developers to understand the purpose and functionality of different parts of the program.

  8. Testing and Debugging: Object-oriented code is typically easier to test and debug due to its modular and encapsulated nature. Isolating specific units of functionality within classes and objects enables focused testing, making it simpler to identify and resolve issues.

4.1.1. Class Constructors and Initialization#

4.1.1.1. Defining and Creating Classes#

In the Python programming language, a class is established using the class keyword, proceeded by the class name (typically formatted in CamelCase). A class operates as a template that outlines the structure for generating objects, complete with designated attributes and behaviors. By encapsulating both data and functionality, it offers a means to systematically structure and control code organization [Lin et al., 2022, Matthes, 2015].

class Car:
    '''Creating objects (instances) of the Car class'''

The code snippet defines a class named Car. The text within the triple single-quotes '''Creating objects (instances) of the Car class''' is a docstring, which provides a concise description indicating that the class is intended for generating objects (instances) related to cars.

In object-oriented programming, class constructors play a crucial role in initializing object attributes and preparing instances for usage. The __init__ method, often referred to as the constructor, is a special method that is automatically invoked when an object of the class is instantiated. This method initializes the object’s initial state by accepting parameters and assigning values to its attributes. Let’s delve into the intricacies of class constructors and initialization [Downey, 2015, Lin et al., 2022]:

4.1.1.2. The __init__ Method as a Constructor#

The __init__ method serves as a constructor in Python classes. Its primary function is to initialize the attributes of an object when it’s created. This method is called with the newly created object and any additional parameters that you provide when creating the object. It is defined within the class just like any other method, but it has the special name __init__.

4.1.1.3. Initializing Object Attributes#

When creating a class, you can define its attributes within the constructor. These attributes will represent the data associated with each object of the class. By passing values to the constructor parameters, you can initialize these attributes to specific values during object creation.

4.1.1.4. The self Parameter#

The self parameter is a crucial aspect of the __init__ method and other methods within a class. It refers to the instance of the object that is being created or manipulated. Using self, you can access and modify the object’s attributes and methods. It acts as a reference to the instance itself, allowing you to interact with its internal components.

Example - Defining a Car Class with Constructor:

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def start_engine(self):
        print(f"The {self.make} {self.model}'s engine is running!")

This code defines a class named Car. Inside the class:

  • The __init__ method is created, which takes three parameters: self, make, and model. self refers to the instance being created, while make and model are attributes used to store the make and model of the car.

  • Inside the __init__ method, self.make = make assigns the value of the make parameter to the make attribute of the instance (object).

  • Similarly, self.model = model assigns the value of the model parameter to the model attribute of the instance.

  • The start_engine method is defined, which when called, prints a message indicating that the engine of the specified car (using its make and model attributes) is running.

Generating objects, known as instances, from a class involves invoking the class name followed by parentheses. This action triggers the creation of a new instance. By providing arguments within these parentheses, you’re able to initialize the object’s attributes with specific initial values [Lin et al., 2022, Matthes, 2015].

Example - Creating Objects:

car1 = Car("Toyota", "Camry")
car2 = Car("Honda", "Civic")

print(car1.make)       # Output: Toyota
print(car2.model)      # Output: Civic
car1.start_engine()    # Output: The Toyota Camry's engine is running!
Toyota
Civic
The Toyota Camry's engine is running!

Here, car1 and car2 are instances of the Car class. The __init__ method initializes their attributes based on the provided arguments. The attributes can be accessed and modified using dot notation. The start_engine method is invoked to print a message about the engine starting.

Note

For the provided example:

class Car:
    def __init__(self, make, model):
        self.make = make
        self.model = model
    
    def start_engine(self):
        print(f"The {self.make} {self.model}'s engine is running!")

In the case of creating an object, such as car1 = Car("Toyota", "Camry"), there are two common ways to define the object: using positional arguments or by specifying keyword arguments.

  1. Using Positional Arguments: When you create an object using positional arguments, you pass values in the order in which the parameters (attributes) are defined in the constructor. In this case, “Toyota” is assigned to make, and “Camry” is assigned to model based on their positions.

    Example:

    car1 = Car("Toyota", "Camry")
    
  2. Using Keyword Arguments: Alternatively, you can create an object using keyword arguments, where you explicitly specify which value corresponds to which parameter by mentioning the parameter names. This approach can make your code more readable, especially when dealing with classes that have many attributes.

    Example:

    car1 = Car(make="Toyota", model="Camry")
    

Both methods achieve the same result, but using keyword arguments can be more explicit and easier to understand, particularly when the class constructor has multiple parameters.

4.1.1.5. Accessing Attributes and Methods#

After object creation, you gain the ability to interact with their attributes and methods through dot notation. This mechanism enables you to retrieve and alter the object’s stored information and execute actions linked with the methods [Lin et al., 2022, Matthes, 2015].

print(car1.make)  # Output: Toyota
print(car2.model) # Output: Civic
car1.start_engine()  # Output: The Toyota Camry's engine is running!
Toyota
Civic
The Toyota Camry's engine is running!

In this code snippet:

  • print(car1.make): This line prints the value of the make attribute of the car1 object, which is "Toyota". So, the output will be: Toyota.

  • print(car2.model): This line prints the value of the model attribute of the car2 object, which is "Civic". The output will be: Civic.

  • car1.start_engine(): This line calls the start_engine method of the car1 object. Inside the method, a message is printed indicating that the engine of the specified car ("Toyota Camry") is running. So, the output will be: The Toyota Camry's engine is running!.

4.1.1.6. Updating Attributes#

Objects can have their attributes updated after creation by using dot notation. For instance:

car1.make = "Ford"
car1.model = "Mustang"
print(car1.make)
print(car1.model)
Ford
Mustang

4.1.1.7. Class Reusability#

A single class definition can be used to create multiple objects with the same structure and behaviors. This promotes code reusability and helps manage data consistently across instances [Lin et al., 2022, Matthes, 2015].

car3 = Car("Ford", "Focus")
car4 = Car("Tesla", "Model S")

4.1.1.8. Object Identity (Optional Content)#

Each object created from a class is a separate entity, even if they share the same class attributes. They have their own unique identity and memory allocation [Lin et al., 2022, Matthes, 2015]. If you want to compare these objects based on their attributes (make and model), you need to define the __eq__ method (the equality operator) in your Car class to specify how objects of this class should be compared. Here’s an example of how you can do this:

# Define a class called Car
class Car:
    def __init__(self, make, model):
        # Initialize the Car object with make and model attributes
        self.make = make
        self.model = model

    def start_engine(self):
        # Method to start the car's engine and print a message
        print(f"The {self.make} {self.model}'s engine is running!")

    def __eq__(self, other):
        # Override the equality operator to compare Car objects
        if isinstance(other, Car):
            # Check if the other object is an instance of Car
            return self.make == other.make and self.model == other.model
            # Compare make and model attributes to determine equality
        return False
        # Return False if the other object is not an instance of Car

# Create instances of the Car class
car1 = Car("Ford", "Mustang")
car2 = Car("Honda", "Civic")

# Compare car1 and car2 based on their attributes
print(car1 == car2)  # This will print False

car1 = Car("Ford", "Mustang")
car2 = Car("Ford", "Bronco")

# Compare car1 and car2 based on their attributes
print(car1 == car2)  # This will print False

car1 = Car("Ford", "Mustang")
car2 = Car("Ford", "Mustang")

# Compare car1 and car2 based on their attributes
print(car1 == car2)  # This will print True
False
False
True

Explanation:

  1. The Car class is defined with a constructor (__init__) that takes two parameters: make and model. These parameters are used to initialize the make and model attributes of the Car objects.

  2. The class also has a start_engine method, which prints a message indicating that the car’s engine is running. This method can be called on instances of the Car class.

  3. The __eq__ method is overridden to customize the equality comparison between Car objects. It checks if the other object being compared is an instance of Car and then compares the make and model attributes to determine if the two cars are equal.

  4. Three instances of the Car class (car1, car2) are created with different makes and models.

  5. The equality operator (==) is used to compare car1 and car2 instances based on their attributes, and the results are printed.

    • The first comparison (print(car1 == car2)) compares a Ford Mustang to a Honda Civic, which are not equal, so it prints False.

    • The second comparison (print(car1 == car2)) compares two Ford vehicles, a Mustang and a Bronco, which are also not equal, so it prints False.

    • The third comparison (print(car1 == car2)) compares two Ford Mustangs with the same make and model, so it prints True because they are considered equal based on their attributes.