Object-Oriented Programming
Build classes and objects in Python, understanding constructors, methods, inheritance, and the use of super().
Classes and Objects
Classes in Python are blueprints for creating objects that bundle data and functionality together. The class keyword defines a new class, and calling the class like a function creates a new instance. Instance attributes are typically set in the __init__ method, which serves as the constructor. Python uses self as a reference to the current instance, passed explicitly as the first parameter to instance methods.
# Defining a class
class Dog:
species = "Canis familiaris" # Class attribute
def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age
def bark(self):
return f"{self.name} says Woof!"
def __str__(self):
return f"{self.name} ({self.age} years old)"
# Creating instances
buddy = Dog("Buddy", 5)
max_dog = Dog("Max", 3)
print(buddy.bark()) # "Buddy says Woof!"
print(str(max_dog)) # "Max (3 years old)"
print(Dog.species) # "Canis familiaris"
The __init__ Method and Encapsulation
The __init__ method is called automatically when a new instance is created and is used to initialize the object's state. Python does not have strict access modifiers like private or protected, but uses naming conventions to indicate intended access levels. A single underscore prefix indicates a protected attribute, while a double underscore prefix triggers name mangling to prevent accidental access. Properties provide a Pythonic way to implement getters and setters.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # Name-mangled attribute
@property
def balance(self):
return self.__balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.__balance += amount
return self.__balance
def withdraw(self, amount):
if amount > self.__balance:
raise ValueError("Insufficient funds")
self.__balance -= amount
return self.__balance
def __repr__(self):
return f"BankAccount('{self.owner}', {self.__balance})"
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print(account.balance) # 1300
print(account) # BankAccount('Alice', 1300)
Inheritance
Inheritance allows a class to derive attributes and methods from a parent class, promoting code reuse and establishing a hierarchy. The child class can override parent methods to provide specialized behavior while keeping the same interface. Python supports multiple inheritance, where a class can inherit from more than one parent. The Method Resolution Order (MRO) determines which method is called when multiple parents define the same method.
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}!"
def info(self):
return f"{self.name} is an animal"
class Cat(Animal):
def __init__(self, name, indoor=True):
super().__init__(name, "Meow")
self.indoor = indoor
def info(self):
location = "indoor" if self.indoor else "outdoor"
return f"{self.name} is an {location} cat"
class Kitten(Cat):
def __init__(self, name, indoor=True):
super().__init__(name, indoor)
def speak(self):
return f"{self.name} says Mew! (tiny meow)"
whiskers = Cat("Whiskers")
print(whiskers.speak()) # "Whiskers says Meow!"
print(whiskers.info()) # "Whiskers is an indoor cat"
tiny = Kitten("Tiny")
print(tiny.speak()) # "Tiny says Mew! (tiny meow)"
Using super() and Method Resolution
The super() function returns a proxy object that delegates method calls to a parent or sibling class in the MRO. It is essential for cooperative multiple inheritance, ensuring each class in the hierarchy is initialized properly. Using super() instead of directly calling the parent class makes your code more maintainable and compatible with complex inheritance trees. The MRO can be inspected using the __mro__ attribute or the mro() method on any class.
class Shape:
def __init__(self, color):
self.color = color
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Rectangle(Shape):
def __init__(self, color, width, height):
super().__init__(color)
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Square(Rectangle):
def __init__(self, color, side):
super().__init__(color, side, side)
# Usage
rect = Rectangle("blue", 5, 3)
print(f"Area: {rect.area()}, Color: {rect.color}") # Area: 15, Color: blue
sq = Square("red", 4)
print(f"Area: {sq.area()}, Color: {sq.color}") # Area: 16, Color: red
# Inspect Method Resolution Order
print(Square.__mro__)
# (<class 'Square'>, <class 'Rectangle'>, <class 'Shape'>, <class 'object'>)