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
Personwithin the class itself, adhering to the principles of object-oriented programming.Self Parameter. Similar to the
__init__method, thegreetingmethod includes a parameter namedself. This parameter represents the instance of the class on which the method is called. When you invokesomeone.greeting(), Python automatically passes the instancesomeoneas theselfparameter to thegreetingmethod. This allows the method to access and operate on the instance’s attributes.Method Invocation. When you call
someone.greeting(), Python looks up thegreetingmethod in thePersonclass and executes it with thesomeoneinstance asself. This means that the method can accesssomeone.first_nameandsomeone.last_name, generating a personalized greeting message.Encapsulation and Cohesion By making
greetinga method ofPerson, we encapsulate behavior related to thePersonclass, 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.