In the previous post, we learned about objects and classes that are rooted in the object-oriented programming paradigm. 👇👇👇
In Python, these classes have magic "Dunder" methods. "Dunder" is a colloquial term used in programming to refer to "double underscore". These methods have names that start and end with double underscores, like __init__
what we already know. They are also known as "special methods" because they have special meaning in the Python language. Dunder methods allow us to define how objects of a class behave in various contexts and interact with built-in Python functions or operators.
💡 Dunder is short for double underscore and refers to magic methods in Python (like __init__ method).
__init__ method
The __init__
method is a special method in Python, often referred to as a constructor. The primary purpose of the method is to set up the initial state of an object. When we create a new instance of a class, Python automatically calls __init__
with the newly created object as the first argument (self
) along with any additional arguments that we provide.
self
is a convention (not a keyword) used to refer to the instance of the object being created. It allows us to access and modify the object's attributes within the __init__
method. We can name it differently, but it's a widely accepted convention to use self
for clarity.
Inside the __init__
method, we typically define and initialize instance attributes (variables) that store the object's data. These attributes can have different data types, including numbers, strings, lists, or even other objects. These attributes define the object's properties.
The __init__
method can accept arguments other than self
. These arguments allow us to pass initial values or configuration parameters to the object when we create it. We can use these arguments to customize the object's initial state.
Let’s take a look at the example of __init__
method from the previous post.
class Car:
def __init__(self, make: str, speed: int):
self.__make: str = make
self.__speed: int = speed
def drive(self):
print(f'Driving {self.__make} at {self.__speed} km/h')
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
car2 = Car(f'Audi', 140)
car1.drive()
car2.drive()
The __init__
method in the provided implementation of the Car
class is responsible for initializing the attributes of each Car
object when it's created. Let's break down the __init__
method and how it works:
def __init__(self, make: str, speed: int):
self.__make: str = make
self.__speed: int = speed
__init__(self, make: str, speed: int)
: This is the constructor method, denoted by __init__
. It takes three parameters:
self
: It represents the instance of the class, which is automatically passed when a new object is created. It's used to access and manipulate the object's attributes.make
: This parameter is a string that represents the make or brand of the car.speed
: This parameter is an integer that represents the speed of the car in kilometers per hour (km/h).
Inside the __init__
method, self.__make
and self.__speed
are instance attributes. The __
prefix indicates that these attributes are private and should not be accessed directly from outside the class. They are set to the values provided as arguments during object creation. These attributes store the make and speed of each car object.
Now, let's see how this __init__
method is used in the __main__
block:
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
car2 = Car(f'Audi', 140)
In the __main__
block, two instances of the Car
class are created:
car1
represents a Fiat car with a speed of 50 km/h.car2
represents an Audi car with a speed of 140 km/h.
These objects are created by calling the Car
constructor (__init__
) with the specified make and speed values.
Finally, the drive
method is called on each car object to print out a message about driving.
car1.drive()
car2.drive()
The drive
method prints a message indicating the make and speed of each car. For example, if we call car1.drive()
, it will print ”Driving Fiat at 50 km/h”, and if we call car2.drive()
, it will print ”Driving Audi at 140 km/h”.
__str__ method
The __str__
method is a special method used to define a human-readable string representation of an object. It is part of Python's data model and allows us to provide a custom string representation for instances of our classes. This method is automatically called when we use the str()
function on an object or when we try to print the object using print()
.
The __str__
method should have the following signature.
def __str__(self):
# Return the string representation here
Inside the __str__
method, we construct and return a string that represents the object. This string can contain any relevant information about the object's state, attributes, or purpose. The __str__
method is often used for custom classes where we want to improve the clarity of object representations. Common use cases include:
providing a descriptive summary of an object's attributes,
creating formatted strings that include key information,
or hiding complex or internal details from users.
Let’s see how we can use the __str__
method in Car class.
class Car:
def __init__(self, make: str, speed: int):
self.__make: str = make
self.__speed: int = speed
def drive(self):
print(f'Driving {self.__make} at {self.__speed} km/h')
def __str__(self) -> str:
return f'Make: {self.__make}, speed: {self.__speed} km/h'
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
car2 = Car(f'Audi', 140)
print(car1)
print(car2)
In the above example, the __str__
method of the Car
class returns a string that includes the car's make (or brand) and speed, providing a clear and informative representation of the object. Let’s print it!
❯ python3 main.py
Make: Fiat, speed: 50 km/h
Make: Audi, speed: 140 km/h
If we don't define a __str__
method in our custom class, Python falls back to using the default __str__
implementation, which typically returns a string containing the object's memory address. Here we can see an example.
❯ python3 main.py
<__main__.Car object at 0x10fa9ace0>
<__main__.Car object at 0x10fa9ac80>
__repr__ method
The __repr__
method is used to define an unambiguous and formal string representation of an object. It is intended to be a representation that, when passed to the eval()
function, would recreate an object with the same state. The primary purpose of __repr__
is to aid in debugging and development, providing a detailed and precise way to represent an object as a string.
The __repr__
method should have the following signature.
def __repr__(self):
# Return the formal string representation here
Inside the __repr__
method, we construct and return a string that represents the object. This string should ideally be a valid Python expression that, when executed using eval()
, would create an object with the same state as the original. The __repr__
method is often used for custom classes to formally represent objects. Common use cases include:
displaying detailed information about an object's attributes and state,
representing complex or custom objects in a way that can be recreated programmatically,
or ensuring that the string representation is unambiguous and unique for each object.
Let’s see how we can use the __repr__
method in Car class.
class Car:
def __init__(self, make: str, speed: int):
self.__make: str = make
self.__speed: int = speed
def drive(self):
print(f'Driving {self.__make} at {self.__speed} km/h')
def __str__(self) -> str:
return f'Make: {self.__make}, speed: {self.__speed} km/h'
def __repr__(self) -> str:
return f'Car(make="{self.__make}", speed={self.__speed})'
In the above example, the __repr__
method of the Car
class returns a string that includes the Car
class name along with make and speed properties, providing a formal and unambiguous string representation of the Car
object.
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
car2 = Car(f'Audi', 140)
print(car1)
print(car2)
car3 = eval(repr(car2))
print(car3)
Our example is capable of recreating a Car
object using eval()
and repr()
functions.
❯ python3 main.py
Make: Fiat, speed: 50 km/h
Make: Audi, speed: 140 km/h
Make: Audi, speed: 140 km/h
If we don't define a __repr__
method in our custom class, Python falls back to using the default __repr__
implementation, which typically returns a string containing the object's memory address similar to the default __str__
implementation.
__len__ method
The __len__
method used to define the length of an object. It allows us to customize how the built-in len()
function behaves when called on instances of our custom classes. By implementing __len__
, we can make our objects iterable and take advantage of sequence-like behaviors.
The __len__
method should have the following signature.
def __len__(self):
# Return the length of the object here
Inside the __len__
method, we calculate and return an integer that represents the object's length. This integer can be zero or a positive number, depending on the object's content and how you define its length. Common use cases are:
sequences: Implementing
__len__
is common for classes that represent sequences, such as lists, tuples, and custom collection classes. It allows us to use the object infor
loops and functions that expect sequences,or custom objects: We can use
__len__
in custom classes to define the length of objects based on specific criteria. For example, if we have a class representing a database table, we might define the length as the number of rows.
Having Car
classes in place, let's define the ParkingLot
class for them. We can assume that our parking space will have a limited number of spots and we want to know how many.
class ParkingLot:
def __init__(self, spots: int):
self.__spots: list[Car] = list()
for _ in range(spots):
self.__spots.append(None)
def __len__(self) -> int:
return len(self.__spots)
The primary purpose of the __len__
method is to provide a custom implementation for determining the length or size of a ParkingLot
object. When you use the len()
function on an object, it returns the number of parking spots in the parking lot. Inside the __len__
method, the code returns the length of the self.__spots list, which represents the number of parking spots in the parking lot.
Let's create a parking lot with 10 spots.
if __name__ == '__main__':
parkingLot = ParkingLot(10)
print(len(parkingLot))
Printing len()
gives the following output.
❯ python3 main.py
10
If we don't define a __len__
method in our custom class, instances of the class will not have a defined length, and attempting to use the len()
function on them will raise a TypeError
.
Let's define the __str__
method to print parking occupancy.
class ParkingLot:
def __init__(self, spots: int):
self.__spots: list[Car] = list()
for _ in range(spots):
self.__spots.append(None)
def __len__(self) -> int:
return len(self.__spots)
def __str__(self) -> str:
occupancy = f'Space occupancy: \n'
for index, spot in enumerate(self.__spots):
occupancy += f'Spot {index} is '
if spot:
occupancy += f'BUSY, parked car: {spot}\n'
else:
occupancy += f'FREE\n'
return occupancy
if __name__ == '__main__':
parkingLot = ParkingLot(10)
print(parkingLot)
Now we can print and see which spots are busy and which are free.
❯ python3 main.py
Space occupancy:
Spot 0 is FREE
Spot 1 is FREE
Spot 2 is FREE
Spot 3 is FREE
Spot 4 is FREE
Spot 5 is FREE
Spot 6 is FREE
Spot 7 is FREE
Spot 8 is FREE
Spot 9 is FREE
__setitem__ method
The __setitem__
method is a special method used to define how objects of a class should respond when elements or values are assigned to specific positions using square brackets ([]
). It allows us to create objects that support item assignment, making them mutable and subscriptable. By implementing the __setitem__
method, we can customize how our objects are modified using square brackets.
The __setitem__
method should have the following signature.
def __setitem__(self, key, value):
# Define how to assign a value to the item based on the key
The self parameter is the instance of the object and is automatically passed to the method. The key parameter represents the index or key where the assignment is taking place. It can be of any data type, depending on how we want to use it. The value parameter represents the value that is being assigned to the specified position or key. Inside the method, we define the logic to assign the value to the item specified by the key. This logic can involve updating the object’s internal state, changing attributes, or any other custom behavior. Common use cases are:
custom mutable sequences: Implementing
__setitem__
is common for classes that aim to mimic the behavior of mutable sequences like lists or arrays. It allows us to define how elements can be modified or replaced at specific indices,or custom mutable mappings: We can use
__setitem__
to create custom mapping classes, similar to dictionaries, where elements can be assigned or updated using keys.
Now we can go back to our ParkingLot
class and implement the __setitem__
method to gain the ability to assign cars to spots.
class ParkingLot:
def __init__(self, spots: int):
self.__spots: list[Car] = list()
for _ in range(spots):
self.__spots.append(None)
def __len__(self) -> int:
return len(self.__spots)
def __str__(self) -> str:
occupancy = f'Space occupancy: \n'
for index, spot in enumerate(self.__spots):
occupancy += f'Spot {index} is '
if spot:
occupancy += f'BUSY, parked car: {spot}\n'
else:
occupancy += f'FREE\n'
return occupancy
def __setitem__(self, index: int, car: Car):
self.__spots[index] = car
The __setitem__
method is defined within the ParkingLot
class, and it takes three parameters. The self parameter represents the instance of the ParkingLot
object and is automatically passed to the method. The index parameter represents the position or spot in the parking lot where the car should be parked. The car parameter represents the Car
object that is being parked in the specified spot. Inside the __setitem__
method, the code assigns the car object to the self.__spots list at the specified index. This effectively parks the car in the parking spot specified by the index. The parking spot is updated with the car object, replacing any previous value that might have been in that spot.
Now let's park a car.
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
print(car1)
parkingLot = ParkingLot(10)
parkingLot[3] = car1
print(parkingLot)
When executing the above code, we get the following output.
❯ python3 main.py
Make: Fiat, speed: 50 km/h
Space occupancy:
Spot 0 is FREE
Spot 1 is FREE
Spot 2 is FREE
Spot 3 is BUSY, parked car: Make: Fiat, speed: 50 km/h
Spot 4 is FREE
Spot 5 is FREE
Spot 6 is FREE
Spot 7 is FREE
Spot 8 is FREE
Spot 9 is FREE
So we have parked a car!
__delitem__ method
The __delitem__
method in Python is a special method used to define how objects of a class should respond when elements or values are deleted from specific positions using the del
statement and square brackets ([]
). It allows us to create objects that support item deletion, making them mutable and subscriptable. By implementing __delitem__
, we can customize how our objects are modified or cleaned up when specific items are removed.
The __delitem__
method should have the following signature.
def __delitem__(self, key):
# Define how to delete the item based on the key
The self parameter is the instance of the object and is automatically passed to the method. The key parameter represents the index or key from which the item is being deleted. It can be of any data type, depending on how we want to use it. Inside the __delitem__
method, we define the logic to remove the item specified by the key. This logic can involve updating the object's internal state, removing attributes, or any other custom behavior required for item deletion. Common use cases are:
custom mutable sequences: Implementing
__delitem__
is common for classes that aim to mimic the behavior of mutable sequences like lists or arrays. It allows us to define how elements can be deleted at specific indices,or custom mutable mappings: We can use
__delitem__
to create custom mapping classes, similar to dictionaries, where elements can be deleted using keys.
Let's go back to our ParkingLot
class one more time and implement the __delitem__
method to gain the ability to free parking spots.
class ParkingLot:
def __init__(self, spots: int):
self.__spots: list[Car] = list()
for _ in range(spots):
self.__spots.append(None)
def __len__(self) -> int:
return len(self.__spots)
def __str__(self) -> str:
occupancy = f'Space occupancy: \n'
for index, spot in enumerate(self.__spots):
occupancy += f'Spot {index} is '
if spot:
occupancy += f'BUSY, parked car: {spot}\n'
else:
occupancy += f'FREE\n'
return occupancy
def __setitem__(self, index: int, car: Car):
self.__spots[index] = car
def __delitem__(self, index: int):
self.__spots[index] = None
The __delitem__
method defined within the ParkingLot
class takes two parameters. The self parameter represents the instance of the ParkingLot
object and is automatically passed to the method. The index parameter represents the position or spot in the parking lot where the car should be removed. Inside the __delitem__
method, the code sets the parking spot at the specified index
to None
. This action effectively removes or "unparks" any car that might have been parked in that spot. By setting the parking spot to None
, it indicates that the spot is now empty and available for parking another car.
Now let's free some parking spots.
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
print(car1)
parkingLot = ParkingLot(10)
parkingLot[3] = car1
print(parkingLot)
del parkingLot[3]
print(parkingLot)
Let's run the script and see the output.
❯ python3 main.py
Make: Fiat, speed: 50 km/h
Space occupancy:
Spot 0 is FREE
Spot 1 is FREE
Spot 2 is FREE
Spot 3 is BUSY, parked car: Make: Fiat, speed: 50 km/h
Spot 4 is FREE
Spot 5 is FREE
Spot 6 is FREE
Spot 7 is FREE
Spot 8 is FREE
Spot 9 is FREE
Space occupancy:
Spot 0 is FREE
Spot 1 is FREE
Spot 2 is FREE
Spot 3 is FREE
Spot 4 is FREE
Spot 5 is FREE
Spot 6 is FREE
Spot 7 is FREE
Spot 8 is FREE
Spot 9 is FREE
The parking lot is fully free again!
__getitem__ method
The __getitem__
method in Python is a special method used to define how objects of a class should respond when they are accessed using square brackets ([]
). It allows us to create objects that behave like sequences or mappings, making them iterable and subscriptable. By implementing __getitem__
, we can customize how our objects are indexed and accessed.
The __getitem__
method should have the following signature.
def __getitem__(self, key):
# Define how to retrieve the item based on the key
The self parameter is the instance of the object and is automatically passed to the method. The key parameter represents the index or key used to access an element within the object. It can be of any data type, depending on how you want to use it. Inside the __getitem__
method, we define the logic to retrieve and return the item corresponding to the given key. The return value can be any Python object, such as an integer, string, list, or even another custom object. Common use cases are:
custom sequences: Implementing
__getitem__
is common for classes that aim to mimic the behavior of sequences like lists or tuples. It allows us to define how elements are accessed based on their positions or indices,or custom mappings: We can use
__getitem__
to create custom mapping classes, similar to dictionaries, where elements are accessed using keys.
Now we can go back to our ParkingLot
class and implement the __getitem__
method to gain the ability to retrieve a car from a parking spot.
class ParkingLot:
def __init__(self, spots: int):
self.__spots: list[Car] = list()
for _ in range(spots):
self.__spots.append(None)
def __len__(self) -> int:
return len(self.__spots)
def __str__(self) -> str:
occupancy = f'Space occupancy: \n'
for index, spot in enumerate(self.__spots):
occupancy += f'Spot {index} is '
if spot:
occupancy += f'BUSY, parked car: {spot}\n'
else:
occupancy += f'FREE\n'
return occupancy
def __setitem__(self, index: int, car: Car):
self.__spots[index] = car
def __delitem__(self, index: int):
self.__spots[index] = None
def __getitem__(self, index: int) -> Car:
return self.__spots[index]
The __getitem__
method defined within the ParkingLot
class takes two parameters. The self parameter represents the instance of the ParkingLot
object and is automatically passed to the method. The index parameter represents the position or spot in the parking lot from which a car should be retrieved. Inside the __getitem__
method, the code retrieves the car object from the parking spot at the specified index
and returns it. If the parking spot is empty (i.e., no car is parked in that spot), None
is returned.
if __name__ == '__main__':
car1 = Car(f'Fiat', 50)
print(car1)
parkingLot = ParkingLot(10)
parkingLot[3] = car1
print(parkingLot)
print(parkingLot[2])
print(parkingLot[3])
Now let's see the output.
❯ python3 main.py
Make: Fiat, speed: 50 km/h
Space occupancy:
Spot 0 is FREE
Spot 1 is FREE
Spot 2 is FREE
Spot 3 is BUSY, parked car: Make: Fiat, speed: 50 km/h
Spot 4 is FREE
Spot 5 is FREE
Spot 6 is FREE
Spot 7 is FREE
Spot 8 is FREE
Spot 9 is FREE
None
Make: Fiat, speed: 50 km/h
Summary
Described methods play essential roles in defining the behavior and representation of custom classes. The __init__
method initializes object attributes upon creation. __str__
and __repr__
methods customize string representations for users and developers, respectively. The __len__
method allows objects to have a length, making them iterable. __getitem__
, __setitem__
, and __delitem__
enable subscriptable and mutable behavior, defining how elements are accessed, modified, and deleted within custom objects, such as sequences and mappings. These methods collectively empower developers to tailor the functionality and presentation of their classes to meet specific programming needs.