In actual development, inheritance is a double-edged sword. When used well, it enhances code reusability; when used poorly, it becomes a maintenance nightmare.
1. The Essence of Inheritance: When Should Inheritance Be Used?
Many developers have misconceptions about inheritance, believing it to be a universal key for code reuse. In fact, inheritance should serve two core objectives:
1. A True “Is-A” Relationship
When you say “Dog is an Animal”, this inheritance relationship is reasonable. However, if it becomes “Employee is a DatabaseConnection”, then it needs to be reconsidered.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ✅ Good Inheritance Example
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "Woof!"
# ❌ Bad Inheritance: A square is not a rectangle
class Rectangle:
def set_size(self, width, height):
self.width = width
self.height = height
class Square(Rectangle): # Violates Liskov Substitution Principle
def set_size(self, width, height):
if width != height:
raise ValueError("Square width and height must be equal")
super().set_size(width, height)
2. The Need for Polymorphism
When a unified interface is needed to handle different objects, inheritance is a good choice:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class PaymentProcessor:
def pay(self, amount):
raise NotImplementedError
class AlipayProcessor(PaymentProcessor):
def pay(self, amount):
print(f"Alipay payment of {amount} yuan")
class WechatProcessor(PaymentProcessor):
def pay(self, amount):
print(f"WeChat payment of {amount} yuan")
# Unified payment processing
def process_payment(processor: PaymentProcessor, amount):
processor.pay(amount)
2. Inheritance vs Composition: How to Choose?
| Scenario | Choose Inheritance | Choose Composition |
|---|---|---|
| Clear “Is-A” Relationship | ✅ | ❌ |
| Need for Polymorphic Support | ✅ | ❌ |
| Function Composition | ❌ | ✅ |
| Runtime Dynamic Changes | ❌ | ✅ |
Practical Application of Composition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Using Composition Instead of Inheritance
class Engine:
def start(self):
print("Engine started")
class MusicPlayer:
def play(self):
print("Playing music")
class Car:
def __init__(self):
self.engine = Engine() # Composition: Car has an engine
self.music_player = MusicPlayer() # Composition: Car has a music player
def drive(self):
self.engine.start()
print("Car is driving")
# Usage
my_car = Car()
my_car.drive()
my_car.music_player.play()
3. Essential Inheritance Design Principles
3.1 Liskov Substitution Principle: Subclasses Should Seamlessly Replace Superclasses
Core Idea: Subclasses should be able to completely replace superclasses without breaking the correctness of the program. This means that subclasses should not change the behavioral contracts of the superclass.
Violation Example Analysis:
1
2
3
4
5
6
7
class Bird:
def fly(self):
print("Flying")
class Ostrich(Bird): # Ostriches cannot fly
def fly(self):
raise Exception("Ostriches cannot fly")
The problem here is: if the code expects to handle a Bird type but receives an Ostrich, it will throw an exception, breaking the expected behavior of the program.
Correct Approach:
1
2
3
4
5
6
7
8
9
10
class Bird:
pass
class FlyingBird(Bird):
def fly(self):
print("Flying")
class Ostrich(Bird): # A bird that cannot fly
def run(self):
print("Running fast")
3.2 Single Responsibility Principle: A Class Should Do One Thing
Core Idea: A class should have only one reason to change. If a class is found to have too many responsibilities, it should be split.
Violation Example Analysis:
1
2
3
4
5
class Employee:
def calculate_salary(self): pass # Calculate salary
def save_to_database(self): pass # Database operations
def generate_report(self): pass # Generate report
def send_notification(self): pass # Send notification
This Employee class takes on too many unrelated responsibilities, and any change in one area will affect the entire class.
Correct Approach:
1
2
3
4
5
6
7
8
9
10
11
class Employee: # Core business logic
def calculate_salary(self): pass
class EmployeeRepository: # Data persistence
def save(self, employee): pass
class ReportGenerator: # Report generation
def generate(self, employee): pass
class NotificationService: # Notification service
def send(self, employee, message): pass
3.3 Open/Closed Principle: Open for Extension, Closed for Modification
Core Idea: Software entities should be open for extension but closed for modification. This means that functionality should be extended by adding new code rather than modifying existing code.
Violation Example Analysis:
1
2
3
4
5
6
7
8
9
class PaymentProcessor:
def process(self, payment_type, amount):
if payment_type == "alipay":
# Alipay processing logic
pass
elif payment_type == "wechat":
# WeChat payment processing logic
pass
# Every time a new payment method is added, this class must be modified
Correct Approach:
1
2
3
4
5
6
7
8
9
10
11
12
13
class PaymentProcessor:
def process(self, amount):
raise NotImplementedError
class AlipayProcessor(PaymentProcessor):
def process(self, amount):
# Alipay specific logic
pass
class WechatProcessor(PaymentProcessor):
def process(self, amount):
# WeChat payment specific logic
pass
4. Practical Tips: Mixin Pattern
A Mixin is an elegant way to implement multiple inheritance in Python, providing specific functionality without instantiating separately.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class JsonSerializableMixin:
"""JSON serialization functionality"""
def to_json(self):
import json
return json.dumps(self.__dict__)
class LoggableMixin:
"""Logging functionality"""
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class User(JsonSerializableMixin, LoggableMixin):
def __init__(self, name, email):
self.name = name
self.email = email
self.log(f"User {name} has been created")
# Usage
user = User("Zhang San", "[email protected]")
print(user.to_json()) # Using Mixin provided functionality
5. Real Case Study: E-commerce System Design
Let’s look at how to apply these principles through the discount functionality of an e-commerce system:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from abc import ABC, abstractmethod
# Abstract base class defining the interface
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, price: float) -> float:
pass
# Concrete strategy implementations
class PercentageDiscount(DiscountStrategy):
def __init__(self, percent):
self.percent = percent
def apply(self, price):
return price * (1 - self.percent / 100)
class FixedAmountDiscount(DiscountStrategy):
def __init__(self, amount):
self.amount = amount
def apply(self, price):
return max(0, price - self.amount)
# Product class using composition
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
self.discount_strategy = None
def set_discount(self, strategy: DiscountStrategy):
self.discount_strategy = strategy
@property
def final_price(self):
if self.discount_strategy:
return self.discount_strategy.apply(self.price)
return self.price
# Usage example
iphone = Product("iPhone 15", 5999)
iphone.set_discount(PercentageDiscount(10)) # 10% off
book = Product("Python Programming", 99)
book.set_discount(FixedAmountDiscount(20)) # 20 yuan off
print(f"iPhone final price: {iphone.final_price}")
print(f"Book final price: {book.final_price}")
6. Pitfall Guide
1. Avoid Over-Inheritance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ❌ Over-Inheritance: Too deep hierarchy
class Animal: pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Poodle(Dog): pass
class ToyPoodle(Poodle): pass # Difficult to maintain!
# ✅ Use attributes instead of deep inheritance
class Dog:
def __init__(self, breed, size="medium"):
self.breed = breed
self.size = size
toy_poodle = Dog("poodle", size="small")
2. Correctly Use super()
1
2
3
4
5
6
7
8
9
10
class Base:
def __init__(self):
print("Base class initialized")
self.base_data = {}
class Child(Base):
def __init__(self, name):
super().__init__() # Call parent class initialization first
self.name = name
print(f"Child class initialized: {name}")
7. Conclusion
Inheritance is an important feature of object-oriented programming, but it needs to be used with caution:
- 1. Prefer Composition, unless there is a clear “Is-A” relationship
- 2. Follow Design Principles, especially Liskov Substitution and Single Responsibility
- 3. Utilize Mixins to achieve functional composition
- 4. Keep Inheritance Hierarchies Flat, avoiding over-design
- 5. Use Abstract Base Classes to define clear interfaces
Remember: Inheritance represents an eternal relationship, not a temporary code reuse need. Before using inheritance, always ask yourself: Will this relationship change in the future?
Discussion Topic: What pitfalls related to inheritance have you encountered in actual projects? Feel free to share and discuss in the comments!
Related Reading:
– Python super() function: Unlocking the Hidden Power of Object-Oriented Programming
– Python Object-Oriented Inheritance – Method Overriding: From Basics to Advanced Practice
– Understanding Object-Oriented Terminology in Python: Classes, Derived Classes, Parent Classes, Subclasses, Classic Classes, and New-Style Classes
These are my personal insights; if you have other views, let’s discuss in the comments!!!
✨Follow me, to get more Python learning resources, practical projects, and industry trends! Reply “python learning” in the public account backend to get Python learning e-books!