OOP Advanced
Multiple Inheritance
Many Parents
Your game character needs to be Movable, Damageable, and Renderable. Python lets a class inherit from multiple parents. The Method Resolution Order (MRO) determines which parent's method is used when names collide.
Basic multiple inheritance
Inherit from two parent classes.
# Basic Multiple Inheritance
class Swimmer:
"""Can swim."""
def swim(self):
return f"{self.__class__.__name__} is swimming"
def describe(self):
return "I can swim"
class Flyer:
"""Can fly."""
def fly(self):
return f"{self.__class__.__name__} is flying"
def describe(self):
return "I can fly"
class Walker:
"""Can walk."""
def walk(self):
return f"{self.__class__.__name__} is walking"
def describe(self):
return "I can walk"
# Single inheritance - one parent
class Fish(Swimmer):
pass
# Multiple inheritance - two parents
class Duck(Swimmer, Flyer, Walker):
"""Ducks can swim, fly, and walk!"""
pass
# Another multiple inheritance example
class Penguin(Swimmer, Walker):
"""Penguins can swim and walk, but not fly."""
pass
def main():
print("=== Basic Multiple Inheritance ===\n")
# Single inheritance
print("--- Single Inheritance (Fish) ---")
fish = Fish()
print(fish.swim())
print(f"describe(): {fish.describe()}")
# Multiple inheritance - Duck
print("\n--- Multiple Inheritance (Duck) ---")
duck = Duck()
print(duck.swim())
print(duck.fly())
print(duck.walk())
print(f"describe(): {duck.describe()}")
# Multiple inheritance - Penguin
print("\n--- Multiple Inheritance (Penguin) ---")
penguin = Penguin()
print(penguin.swim())
print(penguin.walk())
print(f"describe(): {penguin.describe()}")
# Check what methods are available
print("\n--- Available Methods ---")
print(f"Duck methods: swim(), fly(), walk(), describe()")
print(f"Penguin methods: swim(), walk(), describe()")
# Check inheritance
print("\n--- Inheritance Check (isinstance) ---")
print(f"duck is Swimmer: {isinstance(duck, Swimmer)}")
print(f"duck is Flyer: {isinstance(duck, Flyer)}")
print(f"duck is Walker: {isinstance(duck, Walker)}")
print(f"penguin is Swimmer: {isinstance(penguin, Swimmer)}")
print(f"penguin is Flyer: {isinstance(penguin, Flyer)}")
# Parent classes
print("\n--- Parent Classes (__bases__) ---")
print(f"Duck parents: {Duck.__bases__}")
print(f"Penguin parents: {Penguin.__bases__}")
print("\n=== Key Points ===")
print("""
1. class Child(Parent1, Parent2) inherits from both
2. Child has access to all parent methods
3. isinstance() checks all parent types
4. __bases__ shows direct parent classes
5. When methods conflict, leftmost parent wins
""")
main()
class Child(Parent1, Parent2): inherits from both. Child has all methods.
Method Resolution Order
How Python decides which method to call.
# Method Resolution Order (MRO)
class A:
def method(self):
return "A.method()"
def who_am_i(self):
return "I am A"
class B(A):
def method(self):
return "B.method()"
class C(A):
def method(self):
return "C.method()"
def who_am_i(self):
return "I am C"
class D(B, C):
pass # Inherits from both B and C
class E(C, B):
pass # Same parents, different order!
def main():
print("=== Method Resolution Order (MRO) ===\n")
# Display the inheritance structure
print("Inheritance Structure:")
print("""
A
/ \\
B C
\\ /
D
""")
# Check MRO for D
print("--- MRO for class D(B, C) ---")
print(f"D.__mro__ = {D.__mro__}")
print("\nMRO as list:")
for i, cls in enumerate(D.__mro__):
print(f" {i + 1}. {cls.__name__}")
# Method resolution demonstration
print("\n--- Method Resolution for D ---")
d = D()
print(f"d.method() = {d.method()}")
print(" → Found in B (first parent with method)")
print(f"d.who_am_i() = {d.who_am_i()}")
print(" → Not in D, not in B, found in C")
# Compare with E (different parent order)
print("\n--- MRO for class E(C, B) ---")
print(f"E.__mro__ = {E.__mro__}")
e = E()
print(f"\ne.method() = {e.method()}")
print(" → Found in C (first parent with method)")
print(f"e.who_am_i() = {e.who_am_i()}")
print(" → Found in C (first parent with method)")
# Using mro() method
print("\n--- Using mro() method ---")
print(f"D.mro() = {D.mro()}")
# MRO for simpler case
print("\n--- MRO for Simple Cases ---")
print(f"B.__mro__ = {B.__mro__}")
print(" B → A → object")
print(f"C.__mro__ = {C.__mro__}")
print(" C → A → object")
# Demonstrate search path
print("\n--- How Python Finds Methods ---")
print("When calling d.method():")
print(" 1. Look in D → Not found")
print(" 2. Look in B → FOUND! Stop here")
print(" 3. (Would look in C)")
print(" 4. (Would look in A)")
print(" 5. (Would look in object)")
print("\nWhen calling d.who_am_i():")
print(" 1. Look in D → Not found")
print(" 2. Look in B → Not found")
print(" 3. Look in C → FOUND! Stop here")
# Key points about MRO
print("\n=== C3 Linearization Rules ===")
print("""
1. Children come before parents
2. Left parents come before right parents
3. A class appears only once in MRO
4. Each class must appear before its parents
5. Follows C3 linearization algorithm
""")
main()
ClassName.__mro__ shows lookup order. Follows C3 linearization algorithm.
The diamond problem
When two parents share a grandparent.
# The Diamond Problem
# The "diamond" comes from the inheritance shape:
# A
# / \
# B C
# \ /
# D
class A:
def __init__(self):
print("A.__init__() called")
self.value = "from A"
def greet(self):
return "Hello from A"
def identify(self):
return f"A (value={self.value})"
class B(A):
def __init__(self):
print("B.__init__() called")
super().__init__() # Calls next in MRO
self.b_attr = "from B"
def greet(self):
return "Hello from B"
class C(A):
def __init__(self):
print("C.__init__() called")
super().__init__() # Calls next in MRO
self.c_attr = "from C"
def greet(self):
return "Hello from C"
class D(B, C):
def __init__(self):
print("D.__init__() called")
super().__init__() # Starts the chain
def main():
print("=== The Diamond Problem ===\n")
# Show the diamond structure
print("Diamond inheritance structure:")
print("""
A (base)
/ \\
B C
\\ /
D
""")
# The problem: A's __init__ called twice?
print("--- Without super() properly, A could be initialized twice ---")
print("With super() and MRO, A is initialized only ONCE!\n")
# Create D instance - watch the init order
print("Creating D():")
print("-" * 30)
d = D()
print("-" * 30)
# Show the MRO
print(f"\nD's MRO: {[cls.__name__ for cls in D.__mro__]}")
# Explain the init chain
print("\n--- Init Chain Explanation ---")
print("""
D.__init__() called
↓ super().__init__() → next in MRO is B
B.__init__() called
↓ super().__init__() → next in MRO is C (not A!)
C.__init__() called
↓ super().__init__() → next in MRO is A
A.__init__() called
✓ A is initialized only ONCE!
""")
# Check attributes
print("--- Attributes from all classes ---")
print(f"d.value = '{d.value}' (from A)")
print(f"d.b_attr = '{d.b_attr}' (from B)")
print(f"d.c_attr = '{d.c_attr}' (from C)")
# Method resolution in diamond
print("\n--- Method Resolution in Diamond ---")
print(f"d.greet() = '{d.greet()}'")
print(" → D has no greet(), check MRO")
print(" → B has greet() → 'Hello from B'")
print(f"\nd.identify() = '{d.identify()}'")
print(" → Only A has identify()")
# Bad approach: calling parent explicitly
print("\n--- Bad Approach (Don't Do This) ---")
print("""
# Instead of super(), calling parents directly:
class D(B, C):
def __init__(self):
B.__init__(self) # A initialized here
C.__init__(self) # A initialized AGAIN!
This would initialize A twice!
Always use super() in multiple inheritance.
""")
# Why super() works
print("=== Why super() Works ===")
print("""
1. super() follows MRO, not just parent
2. In D(B, C): super() in B calls C, not A
3. Each class is initialized exactly once
4. Order: D → B → C → A → object
5. This is called "cooperative multiple inheritance"
""")
main()
A → B, C → D. Python's MRO ensures each class is called once.
super() chain
Cooperative multiple inheritance.
# Using super() with Multiple Inheritance
class Base:
def __init__(self, **kwargs):
print(f"Base.__init__(kwargs={kwargs})")
# End of chain - absorb remaining kwargs
def process(self):
print("Base.process()")
return ["Base"]
class FeatureA(Base):
def __init__(self, a_param="default_a", **kwargs):
print(f"FeatureA.__init__(a_param={a_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.a_param = a_param
def process(self):
print("FeatureA.process()")
result = super().process() # Call next in chain
return ["FeatureA"] + result
class FeatureB(Base):
def __init__(self, b_param="default_b", **kwargs):
print(f"FeatureB.__init__(b_param={b_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.b_param = b_param
def process(self):
print("FeatureB.process()")
result = super().process() # Call next in chain
return ["FeatureB"] + result
class FeatureC(Base):
def __init__(self, c_param="default_c", **kwargs):
print(f"FeatureC.__init__(c_param={c_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.c_param = c_param
def process(self):
print("FeatureC.process()")
result = super().process() # Call next in chain
return ["FeatureC"] + result
class Combined(FeatureA, FeatureB, FeatureC):
def __init__(self, **kwargs):
print(f"Combined.__init__(kwargs={kwargs})")
super().__init__(**kwargs) # Pass all kwargs
def process(self):
print("Combined.process()")
result = super().process() # Start the chain
return ["Combined"] + result
def main():
print("=== super() Chain with **kwargs ===\n")
# Show MRO
print("MRO for Combined:")
print([cls.__name__ for cls in Combined.__mro__])
print()
# Create with all parameters
print("--- Creating Combined with all parameters ---")
obj =
print(f"\nAttributes:")
print(f" obj.a_param = '{obj.a_param}'")
print(f" obj.b_param = '{obj.b_param}'")
print(f" obj.c_param = '{obj.c_param}'")
# Process chain demonstration
print("\n--- Calling process() ---")
result = obj.process()
print(f"\nResult: {result}")
# Create with default parameters
print("\n" + "=" * 50)
print("--- Creating Combined with defaults ---")
obj2 = Combined()
print(f"\nAttributes (defaults):")
print(f" obj2.a_param = '{obj2.a_param}'")
print(f" obj2.b_param = '{obj2.b_param}'")
print(f" obj2.c_param = '{obj2.c_param}'")
# Explain the pattern
print("\n=== The **kwargs Pattern ===")
print("""
Each class:
1. Accepts its own named parameter
2. Accepts **kwargs for other classes
3. Uses super().__init__(**kwargs)
This lets each class extract its parameter
and pass the rest up the chain!
Example:
Combined(a_param="A", b_param="B", c_param="C")
→ Combined receives: {a_param="A", b_param="B", c_param="C"}
→ FeatureA takes a_param, passes: {b_param="B", c_param="C"}
→ FeatureB takes b_param, passes: {c_param="C"}
→ FeatureC takes c_param, passes: {}
→ Base receives: {} (empty)
""")
# super() with arguments
print("=== super() with Explicit Arguments ===")
print("""
# super() can take class and instance:
super(FeatureA, self).__init__(**kwargs)
# This is the same as:
super().__init__(**kwargs)
# In Python 3, super() automatically uses
# the enclosing class and first argument.
""")
main()
# Using super() with Multiple Inheritance
class Base:
def __init__(self, **kwargs):
print(f"Base.__init__(kwargs={kwargs})")
# End of chain - absorb remaining kwargs
def process(self):
print("Base.process()")
return ["Base"]
class FeatureA(Base):
def __init__(self, a_param="default_a", **kwargs):
print(f"FeatureA.__init__(a_param={a_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.a_param = a_param
def process(self):
print("FeatureA.process()")
result = super().process() # Call next in chain
return ["FeatureA"] + result
class FeatureB(Base):
def __init__(self, b_param="default_b", **kwargs):
print(f"FeatureB.__init__(b_param={b_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.b_param = b_param
def process(self):
print("FeatureB.process()")
result = super().process() # Call next in chain
return ["FeatureB"] + result
class FeatureC(Base):
def __init__(self, c_param="default_c", **kwargs):
print(f"FeatureC.__init__(c_param={c_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.c_param = c_param
def process(self):
print("FeatureC.process()")
result = super().process() # Call next in chain
return ["FeatureC"] + result
class Combined(FeatureA, FeatureB, FeatureC):
def __init__(self, **kwargs):
print(f"Combined.__init__(kwargs={kwargs})")
super().__init__(**kwargs) # Pass all kwargs
def process(self):
print("Combined.process()")
result = super().process() # Start the chain
return ["Combined"] + result
def main():
print("=== super() Chain with **kwargs ===\n")
# Show MRO
print("MRO for Combined:")
print([cls.__name__ for cls in Combined.__mro__])
print()
# Create with all parameters
print("--- Creating Combined with all parameters ---")
obj =
print(f"\nAttributes:")
print(f" obj.a_param = '{obj.a_param}'")
print(f" obj.b_param = '{obj.b_param}'")
print(f" obj.c_param = '{obj.c_param}'")
# Process chain demonstration
print("\n--- Calling process() ---")
result = obj.process()
print(f"\nResult: {result}")
# Create with default parameters
print("\n" + "=" * 50)
print("--- Creating Combined with defaults ---")
obj2 = Combined()
print(f"\nAttributes (defaults):")
print(f" obj2.a_param = '{obj2.a_param}'")
print(f" obj2.b_param = '{obj2.b_param}'")
print(f" obj2.c_param = '{obj2.c_param}'")
# Explain the pattern
print("\n=== The **kwargs Pattern ===")
print("""
Each class:
1. Accepts its own named parameter
2. Accepts **kwargs for other classes
3. Uses super().__init__(**kwargs)
This lets each class extract its parameter
and pass the rest up the chain!
Example:
Combined(a_param="A", b_param="B", c_param="C")
→ Combined receives: {a_param="A", b_param="B", c_param="C"}
→ FeatureA takes a_param, passes: {b_param="B", c_param="C"}
→ FeatureB takes b_param, passes: {c_param="C"}
→ FeatureC takes c_param, passes: {}
→ Base receives: {} (empty)
""")
# super() with arguments
print("=== super() with Explicit Arguments ===")
print("""
# super() can take class and instance:
super(FeatureA, self).__init__(**kwargs)
# This is the same as:
super().__init__(**kwargs)
# In Python 3, super() automatically uses
# the enclosing class and first argument.
""")
main()
# Using super() with Multiple Inheritance
class Base:
def __init__(self, **kwargs):
print(f"Base.__init__(kwargs={kwargs})")
# End of chain - absorb remaining kwargs
def process(self):
print("Base.process()")
return ["Base"]
class FeatureA(Base):
def __init__(self, a_param="default_a", **kwargs):
print(f"FeatureA.__init__(a_param={a_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.a_param = a_param
def process(self):
print("FeatureA.process()")
result = super().process() # Call next in chain
return ["FeatureA"] + result
class FeatureB(Base):
def __init__(self, b_param="default_b", **kwargs):
print(f"FeatureB.__init__(b_param={b_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.b_param = b_param
def process(self):
print("FeatureB.process()")
result = super().process() # Call next in chain
return ["FeatureB"] + result
class FeatureC(Base):
def __init__(self, c_param="default_c", **kwargs):
print(f"FeatureC.__init__(c_param={c_param}, kwargs={kwargs})")
super().__init__(**kwargs) # Pass remaining kwargs up
self.c_param = c_param
def process(self):
print("FeatureC.process()")
result = super().process() # Call next in chain
return ["FeatureC"] + result
class Combined(FeatureA, FeatureB, FeatureC):
def __init__(self, **kwargs):
print(f"Combined.__init__(kwargs={kwargs})")
super().__init__(**kwargs) # Pass all kwargs
def process(self):
print("Combined.process()")
result = super().process() # Start the chain
return ["Combined"] + result
def main():
print("=== super() Chain with **kwargs ===\n")
# Show MRO
print("MRO for Combined:")
print([cls.__name__ for cls in Combined.__mro__])
print()
# Create with all parameters
print("--- Creating Combined with all parameters ---")
obj =
print(f"\nAttributes:")
print(f" obj.a_param = '{obj.a_param}'")
print(f" obj.b_param = '{obj.b_param}'")
print(f" obj.c_param = '{obj.c_param}'")
# Process chain demonstration
print("\n--- Calling process() ---")
result = obj.process()
print(f"\nResult: {result}")
# Create with default parameters
print("\n" + "=" * 50)
print("--- Creating Combined with defaults ---")
obj2 = Combined()
print(f"\nAttributes (defaults):")
print(f" obj2.a_param = '{obj2.a_param}'")
print(f" obj2.b_param = '{obj2.b_param}'")
print(f" obj2.c_param = '{obj2.c_param}'")
# Explain the pattern
print("\n=== The **kwargs Pattern ===")
print("""
Each class:
1. Accepts its own named parameter
2. Accepts **kwargs for other classes
3. Uses super().__init__(**kwargs)
This lets each class extract its parameter
and pass the rest up the chain!
Example:
Combined(a_param="A", b_param="B", c_param="C")
→ Combined receives: {a_param="A", b_param="B", c_param="C"}
→ FeatureA takes a_param, passes: {b_param="B", c_param="C"}
→ FeatureB takes b_param, passes: {c_param="C"}
→ FeatureC takes c_param, passes: {}
→ Base receives: {} (empty)
""")
# super() with arguments
print("=== super() with Explicit Arguments ===")
print("""
# super() can take class and instance:
super(FeatureA, self).__init__(**kwargs)
# This is the same as:
super().__init__(**kwargs)
# In Python 3, super() automatically uses
# the enclosing class and first argument.
""")
main()
super() follows MRO, not direct parent. Essential for diamond pattern.
Conflict resolution
What happens when parents have same method.
# Handling Method Name Conflicts
class Logger:
def log(self, message):
return f"[LOG] {message}"
def get_info(self):
return "Logger: Provides logging capability"
class Serializer:
def serialize(self):
return f"<{self.__class__.__name__}/>"
def get_info(self):
return "Serializer: Provides serialization"
class Validator:
def validate(self, data):
return len(data) > 0
def get_info(self):
return "Validator: Provides validation"
# Method name conflict: all have get_info()
class DataProcessor(Logger, Serializer, Validator):
def process(self, data):
if self.validate(data):
self.log(f"Processing: {data}")
return self.serialize()
return None
# Solution 1: Override and choose
class ProcessorV1(Logger, Serializer, Validator):
def get_info(self):
# Explicitly choose Logger's version
return Logger.get_info(self)
# Solution 2: Override and combine
class ProcessorV2(Logger, Serializer, Validator):
def get_info(self):
# Combine all parent info
info_parts = [
Logger.get_info(self),
Serializer.get_info(self),
Validator.get_info(self),
]
return " | ".join(info_parts)
# Solution 3: Use super() chain
class LoggerChain:
def get_info(self):
info = "Logger: Provides logging"
parent_info = super().get_info() if hasattr(super(), 'get_info') else ""
return info + (" | " + parent_info if parent_info else "")
class SerializerChain:
def get_info(self):
info = "Serializer: Provides serialization"
parent_info = super().get_info() if hasattr(super(), 'get_info') else ""
return info + (" | " + parent_info if parent_info else "")
class ValidatorChain:
def get_info(self):
info = "Validator: Provides validation"
parent_info = super().get_info() if hasattr(super(), 'get_info') else ""
return info + (" | " + parent_info if parent_info else "")
class Base:
def get_info(self):
return "" # End of chain
class ProcessorV3(LoggerChain, SerializerChain, ValidatorChain, Base):
pass
def main():
print("=== Handling Method Name Conflicts ===\n")
# Show the conflict
print("--- The Conflict ---")
print("Logger, Serializer, and Validator all have get_info()")
print("When a class inherits from all three, which one wins?\n")
# Default behavior: leftmost wins
print("--- Default Behavior (Leftmost Wins) ---")
proc = DataProcessor()
print(f"MRO: {[c.__name__ for c in DataProcessor.__mro__]}")
print(f"proc.get_info() = '{proc.get_info()}'")
print(" → Logger's version (first in inheritance list)")
# Solution 1: Explicitly choose
print("\n--- Solution 1: Explicitly Choose ---")
v1 = ProcessorV1()
print(f"v1.get_info() = '{v1.get_info()}'")
print(" → Override calls Logger.get_info(self) directly")
# Solution 2: Combine all
print("\n--- Solution 2: Combine All ---")
v2 = ProcessorV2()
print(f"v2.get_info() = '{v2.get_info()}'")
print(" → Override calls all three parent methods")
# Solution 3: super() chain
print("\n--- Solution 3: super() Chain ---")
v3 = ProcessorV3()
print(f"MRO: {[c.__name__ for c in ProcessorV3.__mro__]}")
print(f"v3.get_info() = '{v3.get_info()}'")
print(" → Each class calls super().get_info() to chain")
# Accessing specific parent methods
print("\n--- Accessing Specific Parent Methods ---")
print(f"Logger.get_info(proc) = '{Logger.get_info(proc)}'")
print(f"Serializer.get_info(proc) = '{Serializer.get_info(proc)}'")
print(f"Validator.get_info(proc) = '{Validator.get_info(proc)}'")
print(" → Can always call parent method directly with instance")
# Best practices
print("\n=== Best Practices for Conflicts ===")
print("""
1. Avoid conflicts by using unique method names
2. If conflict unavoidable, override in child
3. Use ParentClass.method(self) for explicit calls
4. Design parent classes to work with super() chain
5. Document which parent's method is used
6. Consider composition over inheritance
""")
main()
Leftmost parent wins. Explicit override if you need different behavior.
Exercise: practical.py
Build a game character with multiple capability classes