17. Basics of Objects and Classes#

In this chapter, we will begin our exploration of Object-Oriented Programming (OOP) in Python, a powerful programming paradigm that enables developers to model real-world entities as objects in their code. Mastering OOP in Python is essential for writing effective and efficient Python code, as Python is inherently an object-oriented language—everything in Python is an object.

17.1. Playing with Objects#

At its core, an object in programming is a collection of data, known as attributes, and functions, known as methods, that operate on this data. In OOP, objects are instances of classes, which can be thought of as blueprints or templates for creating objects. A class defines a set of attributes and methods that the objects created from it will possess. Essentially, a class represents a type, and objects are instances of that type, sharing the same properties and behaviors.

To define a class in Python, we use the following syntax:

class NameOfClass:
    pass

Here, NameOfClass is the name of the class, and the pass statement indicates that the class currently has no attributes or methods. This is a placeholder that allows us to define the structure of the class without implementing any functionality just yet.

Once a class is defined, we can create objects (instances) of that class by calling the class as if it were a function. This process is known as instantiation. After creating an object, we can inspect its type and retrieve its unique identifier (ID) using the type and id functions, respectively.

class Person:
    pass

someone = Person()
print(type(someone))  # Outputs: <class '__main__.Person'>
print(id(someone))    # Outputs: A unique identifier, e.g., 4569893136

At this stage, the object someone is not very useful because it doesn’t have any attributes or methods. However, we can dynamically add attributes to the object using dot notation:

someone.first_name = "John"
someone.last_name = "Doe"

These attributes can then be accessed and used within functions:

def greeting(p):
    print(f"Hello, my name is {p.first_name} {p.last_name}.")

greeting(someone)  # Outputs: Hello, my name is John Doe.

As demonstrated, objects are useful for grouping related data together. However, dynamically adding attributes to objects is not considered good practice. This approach can lead to inconsistencies and errors, especially when working with multiple objects of the same class.

>>> someother = Person()
>>> someother.name = "Jane"
>>> someother.last_name = "Doe"

>>> greeting(someother)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[23], line 5
      2 someother.name = "Jane"
      3 someother.last_name = "Doe"
----> 5 greeting(someother)

Cell In[20], line 5, in greeting(someother)
      4 def greeting(p):
----> 5     print(f"Hello, my name is {p.first_name} {p.last_name}.")

AttributeError: 'Person' object has no attribute 'first_name'

17.2. The __init__ method#

To ensure that all objects of a given class share the same attributes, we should avoid adding attributes on the fly. Instead, we define a special method within the class called __init__. The __init__ method is automatically called when a new object is instantiated, allowing us to initialize the object’s attributes in a consistent manner.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

Now, when we create a new Person object, we must provide values for first_name and last_name, ensuring consistency across all instances.

someone = Person("John", "Doe")
greeting(someone)  # Outputs: Hello, my name is John Doe.

someother = Person("Jane", "Doe")
greeting(someother)  # Outputs: Hello, my name is Jane Doe.

The __init__ method is a special method in Python known as the constructor. It is automatically called when a new object (instance) of a class is created. The primary purpose of the __init__ method is to initialize the attributes of the newly created object with the values provided. Let’s break down the process step by step using the example someone = Person("John", "Doe").

1. Memory Allocation. When the statement someone = Person("John", "Doe") is executed, Python first allocates memory for the new Person object. This involves reserving a block of memory to store the object’s attributes and any other necessary data.

2. Initialization with __init__. Python then calls the __init__ method of the Person class to initialize the new object. The newly created object is passed to the __init__ method as the first parameter, traditionally named self. This allows the __init__ method to configure the attributes of this specific object. Within the __init__ method, you can set up the initial state of the object.

3. Returning the Object. After the __init__ method is completed, it does not explicitly return anything. Instead, the Person method implicitly returns the newly created and initialized object. This object is then assigned to the variable someone.

By using the __init__ method, we establish a clear and reliable structure for our objects, making our code more predictable and easier to maintain.

17.3. Adding a first method#

The function greeting is intrinsically tied to the attributes of the Person class, as it operates directly on these attributes to generate its output. This close relationship suggests that greeting should logically be a method within the Person class rather than an external function. By refactoring the function into a method, we ensure that it becomes an integral part of the Person class, thus promoting better organization and encapsulation of code. Here is how you can refactor the function into a method within the Person class.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name
    
    def greeting(self):
        print(f"Hello, my name is {self.first_name} {self.last_name}.")

In this revised implementation, greeting is defined as a method of the Person class. This means that greeting is now a function that operates on instances of Person, utilizing the instance’s attributes (first_name and last_name).

Here’s a breakdown of the changes and their implications:

  • Method Definition. The greeting function is defined within the Person class. This change encapsulates the behavior associated with a Person within the class itself, adhering to the principles of object-oriented programming.

  • Self Parameter. Similar to the __init__ method, the greeting method includes a parameter named self. This parameter represents the instance of the class on which the method is called. When you invoke someone.greeting(), Python automatically passes the instance someone as the self parameter to the greeting method. This allows the method to access and operate on the instance’s attributes.

  • Method Invocation. When you call someone.greeting(), Python looks up the greeting method in the Person class and executes it with the someone instance as self. This means that the method can access someone.first_name and someone.last_name, generating a personalized greeting message.

  • Encapsulation and Cohesion By making greeting a method of Person, we encapsulate behavior related to the Person class, which enhances cohesion. Encapsulation ensures that related data and methods are grouped together, making the code more modular and easier to maintain.

Note

As possible we carefuly use the terms function and method. A method is a function that is defined within a class and is intended to operate on instances of that class. Methods are bound to objects and can access and modify the object’s state through self.