Introduction
The mountains fade into the plains, and the river flows into the wilderness.
Overview
- Basics of Classes
- Inheritance
- Polymorphism
- Encapsulation
- Multiple Inheritance
- Using the super() Function
- Class Methods and Static Methods
<span>Property Decorators</span><span>Abstract Base Classes</span>
Basics of Classes
What is a Class?
- A class is a template that describes the behavior and state of a type of object. An object is an instance of a class. A class contains attributes and methods.
- For example, we can create a class called “Dog” that has attributes like name, age, and gender, and methods like bark, run, and bite.
How to Define a Class?
- Use the
<span>class</span>keyword to define a class.
class Dog:
# Class attribute (shared by all dogs)
species = "Canis familiaris"
# Initialization method (constructor)
def __init__(self, name, age):
# Instance attributes (unique to each instance)
self.name = name
self.age = age
# Instance method
def bark(self):
return f"{self.name} says woof!"
def run(self, speed):
return f"{self.name} runs at {speed} km/h."
Creating Objects (Instantiation)
- Use the class name followed by parentheses to create an object, passing the required parameters for the initialization method (excluding
<span>self</span>).
my_dog = Dog("Buddy", 3)
print(my_dog.name) # Output: Buddy
print(my_dog.age) # Output: 3
print(my_dog.bark()) # Output: Buddy says woof!
Inheritance
What is Inheritance?
- Inheritance is a feature of object-oriented programming that allows us to define a class (subclass) that inherits attributes and methods from another class (superclass).
- The subclass can override or extend the functionality of the superclass.
# Superclass (Base class)
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
raise NotImplementedError("Subclass must implement this method")
# Subclass (Derived class)
class Cat(Animal): # Inherits from Animal
def __init__(self, name, age, color):
super().__init__(name, age) # Call the superclass's initialization method
self.color = color
# Override superclass method
def speak(self):
return f"{self.name} says meow!"
# Method unique to the subclass
def describe(self):
return f"{self.name} is {self.age} years old and has {self.color} fur."
Using Subclasses
my_cat = Cat("Whiskers", 2, "black")
print(my_cat.speak()) # Output: Whiskers says meow!
print(my_cat.describe()) # Output: Whiskers is 2 years old and has black fur.
Polymorphism
- Polymorphism refers to the ability of different class objects to respond differently to the same message (method).
- For example, we can have a function that accepts any animal object and calls its
<span>speak</span>method.
def animal_sound(animal):
return animal.speak()
# Create different animal objects
my_dog = Dog("Buddy", 3)
my_cat = Cat("Whiskers", 2, "black")
print(animal_sound(my_dog)) # Output: Buddy says woof!
print(animal_sound(my_cat)) # Output: Whiskers says meow!
Encapsulation
- Encapsulation is the bundling of data (attributes) and methods that operate on that data, while hiding the internal implementation details. In Python, we use naming conventions to indicate the access level of attributes and methods.
- Public: No leading underscore, e.g.,
<span>name</span>. - Protected: One leading underscore, e.g.,
<span>_age</span>(indicates that it should not be accessed directly from outside, but it can still be accessed). - Private: Two leading underscores, e.g.,
<span>__secret</span>(name mangled, cannot be accessed directly).
class BankAccount:
def __init__(self, account_holder, balance):
self.account_holder = account_holder # Public attribute
self._balance = balance # Protected attribute (indicates not to access directly)
self.__account_number = 123456 # Private attribute
# Public method
def get_balance(self):
return self._balance
def deposit(self, amount):
self._balance += amount
return self._balance
def __update_database(self): # Private method
print("Database updated")
account = BankAccount("Alice", 1000)
print(account.account_holder) # Can access
print(account.get_balance()) # Access protected attribute through public method
# print(account.__account_number) # Will raise an error, as private attribute cannot be accessed directly
# account.__update_database() # Similarly, private method cannot be accessed directly
Method Overriding
- Method overriding allows a subclass to redefine a method from its superclass to change its behavior.
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
def start_engine(self):
return "Engine started"
def info(self):
return f"{self.brand} {self.model}"
class Car(Vehicle):
def __init__(self, brand, model, doors):
super().__init__(brand, model)
self.doors = doors
# Override superclass method
def start_engine(self):
return f"Car engine of {self.brand} {self.model} started with a key"
# Extend superclass method
def info(self):
return f"{super().info()} with {self.doors} doors"
class ElectricCar(Car):
def __init__(self, brand, model, doors, battery_capacity):
super().__init__(brand, model, doors)
self.battery_capacity = battery_capacity
# Completely override superclass method
def start_engine(self):
return f"Electric car {self.brand} {self.model} powered on silently"
# Testing
car = Car("Toyota", "Camry", 4)
electric_car = ElectricCar("Tesla", "Model S", 4, "100kWh")
print(car.start_engine()) # Output: Car engine of Toyota Camry started with a key
print(electric_car.start_engine()) # Output: Electric car Tesla Model S powered on silently
print(car.info()) # Output: Toyota Camry with 4 doors
Multiple Inheritance
- A class can inherit from multiple parent classes.
# Example 1
class A:
def method_a(self):
return "Method A"
class B:
def method_b(self):
return "Method B"
class C(A, B): # Multiple inheritance
def method_c(self):
return "Method C"
obj = C()
print(obj.method_a()) # Output: Method A
print(obj.method_b()) # Output: Method B
# Example 2
class Flyable:
def fly(self):
return "I can fly!"
class Swimmable:
def swim(self):
return "I can swim!"
class Duck(Flyable, Swimmable): # Multiple inheritance
def __init__(self, name):
self.name = name
def quack(self):
return f"{self.name} says quack!"
donald = Duck("Donald")
print(donald.quack()) # Output: Donald says quack!
print(donald.fly()) # Output: I can fly!
print(donald.swim()) # Output: I can swim!
Method Resolution Order
- When using multiple inheritance, Python uses the C3 algorithm to determine the method resolution order.
class A:
def method(self):
return "Method A"
class B(A):
def method(self):
return "Method B"
class C(A):
def method(self):
return "Method C"
class D(B, C):
pass
# Check method resolution order
print(D.__mro__)
# Output: (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
d = D()
print(d.method()) # Output: Method B (because B comes before C)
Using the super() Function
<span>super()</span>function is used to call methods from the superclass, especially when overriding methods, we often need to call the superclass’s implementation first.
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side) # Call superclass's __init__, setting both length and width to side
# A square is a type of rectangle, so we can inherit this way
square = Square(5)
print(square.area()) # Output: 25
Why Call the Superclass’s __init__ Method?
- When creating a subclass, we want the subclass to have both the attributes of the superclass and its own unique attributes.
class Cat(Animal):
def __init__(self, name, age, color):
# ❌ Repeating code already present in the superclass
self.name = name # Repetition!
self.age = age # Repetition!
self.color = color # New attribute
my_cat = Cat("Whiskers", 2, "black")
print(my_cat.name) # Output: Whiskers
class Cat(Animal):
def __init__(self, name, age, color):
# ✅ Correct approach: let the superclass handle what it does best
super().__init__(name, age) # Superclass sets name and age
self.color = color # Subclass sets its unique attribute
What is super()?
<span>super()</span>is a built-in function that returns a proxy object representing the superclass. Through<span>super()</span>, we can call methods from the superclass.- In the subclass’s
<span>__init__</span>method, we typically use<span>super().init(...)</span>to call the superclass’s initialization method, so we don’t have to repeat the initialization code already present in the superclass.
Other Uses of super()
<span>super()</span>can be used not only for<span>__init__</span>, but also to call other methods from the superclass:
class Animal:
def make_sound(self):
return "Some generic animal sound"
def describe(self):
return "I am an animal"
class Dog(Animal):
def make_sound(self):
return "Woof! Woof!"
def describe(self):
# First call the superclass's describe method, then add a dog-specific description
parent_description = super().describe()
return f"{parent_description}, specifically a dog"
class LoudDog(Dog):
def make_sound(self):
# First call the superclass's make_sound, then make it louder
parent_sound = super().make_sound()
return parent_sound.upper() + "!!!"
def describe(self):
parent_description = super().describe()
return f"{parent_description} that is very LOUD"
# Testing
animal = Animal()
dog = Dog()
loud_dog = LoudDog()
print("Animal:", animal.describe()) # Output: I am an animal
print("Dog:", dog.describe()) # Output: I am an animal, specifically a dog
print("LoudDog:", loud_dog.describe()) # Output: I am an animal, specifically a dog that is very LOUD
print("\nSounds:")
print("Animal:", animal.make_sound()) # Output: Some generic animal sound
print("Dog:", dog.make_sound()) # Output: Woof! Woof!
print("LoudDog:", loud_dog.make_sound()) # Output: WOOF! WOOF!!!
Class Methods and Static Methods
- Instance methods: The first parameter is
<span>self</span>, which refers to the instance itself. - Class methods: Use the
<span>@classmethod</span>decorator, the first parameter is<span>cls</span>, which refers to the class itself. Class methods can access class attributes but cannot access instance attributes. - Static methods: Use the
<span>@staticmethod</span>decorator, which does not require a default parameter. It cannot access instance attributes or class attributes, acting like a regular function but logically belonging to the class.

class Student:
school_name = "Python High School" # Class attribute
student_count = 0 # Class attribute to count students
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student.student_count += 1 # Modify class attribute
# Instance method - can access instance and class attributes
def info(self):
return f"{self.name} is in grade {self.grade} at {self.school_name}"
# Class method - can access class attributes, cannot access instance attributes
@classmethod
def get_school_info(cls):
return f"School: {cls.school_name}, Total students: {cls.student_count}"
# Static method - cannot access class or instance attributes
@staticmethod
def is_passing_grade(grade):
return grade >= 60
# Class method as an alternative constructor
@classmethod
def from_string(cls, student_str):
name, grade = student_str.split(",")
return cls(name, int(grade))
# Usage
student1 = Student("Alice", 85)
student2 = Student("Bob", 92)
print(student1.info()) # Output: Alice is in grade 85 at Python High School
print(Student.get_school_info()) # Output: School: Python High School, Total students: 2
print(Student.is_passing_grade(75)) # Output: True
# Using alternative constructor
student3 = Student.from_string("Charlie,88")
print(student3.info()) # Output: Charlie is in grade 88 at Python High School
Property Decorators
- Property decorators (
<span>@property</span>) can turn methods into properties, allowing us to hide implementation details and add logic when getting or setting properties.
Why Use @property?
- Background: Encapsulation and data validation.
- Without
<span>@property</span>, if we want to add validation logic when setting a property, we would need to do it like this:
class PersonWithoutProperty:
def __init__(self, first_name, last_name):
self._first_name = first_name
self._last_name = last_name
def get_full_name(self):
return f"{self._first_name} {self._last_name}"
def set_full_name(self, name):
if not isinstance(name, str) or len(name.split()) < 2:
raise ValueError("Full name must contain at least first and last name")
self._first_name, self._last_name = name.split(" ", 1)
# Using getter and setter methods
person = PersonWithoutProperty("John", "Doe")
print(person.get_full_name()) # Output: John Doe
person.set_full_name("Jane Smith")
- Problem: The syntax is not intuitive, requiring method calls instead of direct property access.
Basic Principle of @property
<span>@property</span>turns a method into a “read-only property”.
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
"""Turn full_name method into a read-only property"""
return f"{self.first_name} {self.last_name}"
# Usage
person = Person("John", "Doe")
# Now we can call full_name like accessing a property
print(person.full_name) # Output: John Doe
# Note: No parentheses! It looks like a property, but it's actually a method call
# person.full_name = "Jane Smith" # This will raise an error, as it is now a read-only property
Details of the Setter Method
- Add setting functionality to the property.
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
"""Getter method - called when accessing the property"""
return f"{self.first_name} {self.last_name}"
@full_name.setter
def full_name(self, name):
"""Setter method - called when setting the property"""
# Add validation logic
if not isinstance(name, str):
raise ValueError("Name must be a string")
parts = name.split()
if len(parts) < 2:
raise ValueError("Full name must contain at least first and last name")
self.first_name = parts[0]
self.last_name = " ".join(parts[1:]) # Handle middle names
print(f"Name updated to: {self.first_name} {self.last_name}")
# Usage
person = Person("John", "Doe")
print("Original name:", person.full_name) # Output: Original name: John Doe
# Now we can set full_name like a property
person.full_name = "Jane Marie Smith" # This will call the setter method
print("New name:", person.full_name) # Output: New name: Jane Marie Smith
# Test validation logic
try:
person.full_name = "SingleName" # This will trigger an error
except ValueError as e:
print(f"Error: {e}")
The Magic Behind @property: Descriptor Protocol
<span>@property</span>is implemented based on Python’s descriptor protocol. Here’s how to manually implement similar functionality:
# Manually implement property functionality (simplified version)
class ManualProperty:
def __init__(self, fget=None, fset=None):
self.fget = fget # Getter function
self.fset = fset # Setter function
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def setter(self, fset):
self.fset = fset
return self
class PersonWithManualProperty:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@ManualProperty
def full_name(self):
return f"{self.first_name} {self.last_name}"
@full_name.setter
def full_name(self, name):
self.first_name, self.last_name = name.split(" ", 1)
- Usage example:
class BankAccount:
def __init__(self, account_holder, initial_balance=0):
self._account_holder = account_holder # Protected attribute
self.__balance = initial_balance # Private attribute
self._transaction_history = []
# Property decorator - call method like accessing a property
@property
def balance(self):
return self.__balance
@property
def account_holder(self):
return self._account_holder
@account_holder.setter
def account_holder(self, new_name):
if len(new_name) > 0:
self._account_holder = new_name
self._transaction_history.append(f"Account holder changed to {new_name}")
def deposit(self, amount):
if amount > 0:
self.__balance += amount
self._transaction_history.append(f"Deposited: ${amount}")
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
self._transaction_history.append(f"Withdrew: ${amount}")
return True
return False
def get_transaction_history(self):
return self._transaction_history.copy() # Return a copy to protect original data
# Usage
account = BankAccount("John Doe", 1000)
print(account.balance) # Output: 1000
print(account.account_holder) # Output: John Doe
account.account_holder = "Jane Doe" # Using setter
account.deposit(500)
account.withdraw(200)
print(f"New balance: ${account.balance}") # Output: New balance: $1300
print("Transaction history:", account.get_transaction_history())
The Complete @property Ecosystem
- In addition to
<span>getter</span>and<span>setter</span>, there is also<span>deleter</span>:
class Config:
def __init__(self):
self._settings = {"theme": "dark", "language": "en"}
@property
def theme(self):
return self._settings.get("theme")
@theme.setter
def theme(self, value):
if value not in ["light", "dark"]:
raise ValueError("Theme must be 'light' or 'dark'")
self._settings["theme"] = value
@theme.deleter
def theme(self):
print("Deleting theme setting, restoring default")
self._settings["theme"] = "light"
config = Config()
print(f"Current theme: {config.theme}") # Output: Current theme: dark
config.theme = "light"
print(f"New theme: {config.theme}") # Output: New theme: light
del config.theme # Output: Deleting theme setting, restoring default
print(f"Default theme: {config.theme}") # Output: Default theme: light
- Syntax pattern:
class MyClass:
def __init__(self, value):
self._value = value
@property
def value(self):
# Getter logic
return self._value
@value.setter
def value(self, new_value):
# Setter logic (including validation)
if validation_condition:
self._value = new_value
else:
raise ValueError("Error message")
@value.deleter
def value(self):
# Deleter logic
del self._value
Abstract Base Classes
- Abstract Base Classes (ABCs) are used to define interfaces, specifying methods that subclasses must implement. We cannot directly instantiate an abstract base class.
Why Do We Need Abstract Base Classes?
- Standardizing Interfaces: When multiple subclasses need to implement the same interface, an abstract base class ensures that each subclass implements the necessary methods.
- For example, in a graphics library, each shape should have methods for calculating area and perimeter, so we can define an abstract base class Shape that requires each subclass to implement area and perimeter methods.
- Avoiding Omissions: If a subclass does not implement all abstract methods in the abstract base class, the subclass cannot be instantiated, which helps catch errors during development rather than at runtime.
- Polymorphism: Abstract base classes allow us to handle different subclass objects in a uniform way.
- For example, we can have a list of Shape types containing various shapes (circles, rectangles, etc.), and then iterate through the list to call each shape’s area method without worrying about the specific type of shape.
- Documentation Purpose: Abstract base classes clearly specify the methods that subclasses should have, providing a clear explanation for readers of the code.
The Logic Behind Abstract Base Classes
- Abstract base classes are implemented through the
<span>abc</span>module, where<span>ABC</span>is a helper class for abstract base classes, and<span>abstractmethod</span>is a decorator used to declare abstract methods. - An abstract base class cannot be instantiated because its abstract methods are not implemented. Attempting to instantiate an abstract base class will raise an error.
- Subclasses must implement all abstract methods to be instantiated; otherwise, the subclass will also be considered an abstract class.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
def perimeter(self):
return 2 * 3.14 * self.radius
# Cannot instantiate Shape class, must instantiate its subclass, and the subclass must implement abstract methods
circle = Circle(5)
print(circle.area()) # Output: 78.5
- If we create a subclass but do not implement all abstract methods, that subclass also cannot be instantiated:
class IncompleteCircle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
# Missing perimeter method
# incomplete_circle = IncompleteCircle(5) # TypeError: Can't instantiate abstract class IncompleteCircle with abstract method
Underlying Implementation Principles
- The implementation of abstract base classes relies on Python’s metaclass mechanism. When we use
<span>ABC</span>as a base class, it is actually managed by the metaclass<span>ABCMeta</span>. - Registering abstract methods: When using the
<span>@abstractmethod</span>decorator, that method is added to the class’s<span>__abstractmethods__</span>set. - Instantiation check: When we attempt to instantiate a class, Python checks whether the class’s
<span>__abstractmethods__</span>set is empty. If it is not empty, it raises a<span>TypeError</span>, indicating that there are still abstract methods that have not been implemented.
# Manually implement the functionality of abstract base classes (simplified version)
class ManualABC:
def __init__(self):
# Collect all abstract methods
self._abstract_methods = set()
# Iterate through the class's methods to find those marked as abstract
for attr_name in dir(self):
attr = getattr(self, attr_name)
if hasattr(attr, '_is_abstract') and attr._is_abstract:
self._abstract_methods.add(attr_name)
def __new__(cls, *args, **kwargs):
# Check if there are any unimplemented abstract methods
instance = super().__new__(cls)
if hasattr(instance, '_abstract_methods') and instance._abstract_methods:
raise TypeError(f"Cannot instantiate abstract class {cls.__name__},"
f" unimplemented methods: {instance._abstract_methods}")
return instance
def manual_abstractmethod(func):
"""Manually implemented @abstractmethod decorator"""
func._is_abstract = True
return func
# Using manually implemented abstract base class
class ManualShape(ManualABC):
@manual_abstractmethod
def area(self):
pass
@manual_abstractmethod
def perimeter(self):
pass
class ManualCircle(ManualShape):
def __init__(self, radius):
self.radius = radius
def area(self): # Must implement
return 3.14 * self.radius ** 2
# Intentionally not implementing perimeter method
# Testing
try:
# circle = ManualCircle(5) # This will raise an error because perimeter method is not implemented
pass
except TypeError as e:
print(f"Error: {e}")
Conclusion
In the maze-like city of Chang’an, seeking the happiness each deems fit.