Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
135: Composition Over Inheritance
Listen, by now you're probably comfortable with inheritance. You've built base classes, overridden methods, and you feel like you've got a handle on the "is-a" relationship. But here is a secret from the trenches: in professional production code, we actually try to avoid deep inheritance hierarchies whenever possible. We prefer composition.
Think of it like building a custom PC. If you wanted a "Gaming PC," you wouldn't create a blueprint for a "Basic PC" and then create a subclass called "Gaming PC" that inherits everything from the basic one. Why? Because that's too rigid. If you suddenly decide your Gaming PC needs a different power supply or a specific liquid cooling system, you're stuck fighting the structure of the parent class. Instead, you treat a PC as a container. You buy a case, then you plug in a GPU, a CPU, and some RAM. The PC "has-a" GPU; it isn't "a type of" GPU. If the GPU breaks or you want an upgrade, you just swap that one component without redesigning the entire machine.
The Trap of the "Is-A" Relationship
In Python, we often start with inheritance because it feels intuitive. "A Warrior is a Character, so I'll inherit from Character." That works great until you need a BattleMage. Now you're stuck. Do you inherit from both Warrior and Mage? That leads to the "Diamond Problem" and a nightmare of method resolution orders (MRO) that will make you want to quit coding for the week.
# The "Inheritance Trap" way
class Character:
def move(self):
print("Moving...")
class Warrior(Character):
def attack(self):
print("Swings a sword!")
class Mage(Character):
def attack(self):
print("Casts a fireball!")
# Now what? A BattleMage needs both. Multiple inheritance is messy.
class BattleMage(Warrior, Mage):
pass
The problem here is that we've tied the identity of the object (what it is) to its behavior (what it can do). That's a recipe for fragile code.
Building with LEGOs instead of Casts
Composition flips the script. Instead of saying a BattleMage is a Warrior and a Mage, we say a Character has a combat style. We move the behavior into its own separate class and plug it into our main object. This is the "has-a" relationship.
class SwordAttack:
def execute(self):
print("Swings a sword!")
class SpellAttack:
def execute(self):
print("Casts a fireball!")
class Character:
def __init__(self, name, attack_style):
self.name = name
# This is composition. We store an instance of another class.
self.attack_style = attack_style
def perform_attack(self):
print(f"{self.name} attacks:", end=" ")
self.attack_style.execute()
# Now we can create any combination we want without complex hierarchies
warrior = Character("Grog", SwordAttack())
mage = Character("Zelda", SpellAttack())
battle_mage = Character("Valerius", SpellAttack()) # Start as mage...
# And here is the magic: we can change behavior at runtime!
battle_mage.attack_style = SwordAttack() # ...then switch to sword!
By decoupling the attack logic from the Character class, I've made the code infinitely more flexible. I don't need a new class for every single combination of abilities; I just mix and match components.
When to actually stick with inheritance
I'm not telling you to delete every (BaseClass) in your code. Inheritance is still the right tool when you have a true, unchanging taxonomic relationship. If you're building a system for biological species, a Dog will always be a Mammal. That's not a "component" you swap out on a Tuesday afternoon.
But as a rule of thumb: if you find yourself creating subclasses just to reuse a few methods, or if you're struggling with "Multiple Inheritance," stop. Step back. Ask yourself: "Should this object be this thing, or should it just have this thing?" Nine times out of ten, composition is the cleaner path.
📋 Practical Task
Building a Modular Smart-Home Device
You are designing a smart-home system. Instead of creating a rigid inheritance tree (e.g., SmartLight, SmartThermostat), you need to build a SmartDevice class that uses composition to handle different functionalities.
Requirements:
- Create two "Component" classes:
LightingComponent(with a methodoperate()that prints "Adjusting brightness...") andClimateComponent(with a methodoperate()that prints "Adjusting temperature..."). - Create a
SmartDeviceclass. It should take anameand acomponentin its__init__method. - The
SmartDeviceshould have a method calledactivate()that calls theoperate()method of whatever component it currently holds. - In your main execution block:
- Create a device named "Living Room Hub" with a
LightingComponent. - Call
activate()on it. - Swap the component of the "Living Room Hub" to a
ClimateComponent. - Call
activate()again to prove the behavior has changed without creating a new object.
- Create a device named "Living Room Hub" with a
There are no comments for now.