Week 15 - Inheritance
Object-Oriented Programming - Inheritance
Learning Objectives
- Understand the concept of inheritance and its role in object-oriented programming.
- Implement inheritance to extend the functionality of base classes.
- Learn how to override methods and call methods from parent classes.
- Explore real-world examples to deepen understanding.
Introduction to Inheritance
Inheritance allows a child class to acquire the properties and methods of its parent class (i.e. the class the child is inheriting from). It helps in code reuse and creates a hierarchical relationship between classes.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | # Parent class
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
# Child class
class Dog(Animal):
def speak(self):
return f"{self.name} barks."
# Demonstration
generic_animal = Animal("Generic Animal")
dog = Dog("Buddy")
print(generic_animal.speak()) # Output: Generic Animal makes a sound.
print(dog.speak()) # Output: Buddy barks.
|
Important Concepts about Inheritance
- Though not demonstrated in the code above, inheritance avoids duplicating code by allowing child classes to inherit already implmented methods from the parent..
- Demonstrated abovee, overriding methods allows specialization of behavior in child classes (i.e. the dog objects now bark instead of simply makeing sound)..
Demonstrating OOP Inheritance with Cards
To demonstrate OOP and inheritance, we first start off with a simple object blueprint for a Card
. There is no inheritance in the Card class, but you should see how we compose Card
s into a Deck
class in the UML and of course the code below.
As you can see from the UML and code, the Deck
class demonstrates inheritance by creating a hierarchy of card-related classes like Hand, PokerHand and BridgeHand (see UML below).
classDiagram
class Card {
- suit: int
- rank: int
+ suit_names: list
+ rank_names: list
+ __init__(suit: int, rank: int)
+ __str__() str
+ __eq__(other: Card) bool
+ __lt__(other: Card) bool
+ __le__(other: Card) bool
+ to_tuple() tuple
}
class Deck {
- cards: list
+ __init__(cards: list)
+ make_cards() list
+ __str__() str
+ take_card() Card
+ put_card(card: Card)
+ shuffle()
+ sort()
+ move_cards(other: Deck, num: int)
+ deal_hand(label: str, num_cards: int) Hand
}
class Hand {
- label: str
- cards: list
+ __init__(label: str)
+ add_card(card: Card)
+ remove_card(card: Card)
+ __str__() str
}
class PokerHand {
+ __init__(label: str)
+ evaluate_hand() str
}
class BridgeHand {
+ __init__(label: str)
+ evaluate_hand() dict
}
Deck <|-- Hand
Deck *-- Card
Hand <|-- PokerHand
Hand <|-- BridgeHand
Example
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84 | from random import shuffle
class Card:
suit_names = ["Clubs", "Diamonds", "Hearts", "Spades"]
rank_names = [None, "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King"]
def __init__(self, suit: int, rank: int):
self.suit = suit
self.rank = rank
def __str__(self):
return f"{self.rank_names[self.rank]} of {self.suit_names[self.suit]}"
def __eq__(self, other: 'Card') -> bool:
return self.suit == other.suit and self.rank == other.rank
def __lt__(self, other: 'Card') -> bool:
return self.to_tuple() < other.to_tuple()
def __le__(self, other: 'Card') -> bool: # Example of using tuples directly
return (self.suit, self.rank) <= (other.suit, other.rank)
def to_tuple(self) -> tuple:
return self.suit, self.rank
class Deck:
def __init__(self, cards=None):
self.cards = cards if cards else Deck.make_cards()
@staticmethod
def make_cards():
return [Card(suit, rank) for suit in range(4) for rank in range(1, 14)]
def __str__(self):
return "\n".join(str(card) for card in self.cards)
def take_card(self) -> Card:
return self.cards.pop() if self.cards else None
def put_card(self, card: Card):
self.cards.append(card)
def shuffle(self):
shuffle(self.cards)
def sort(self):
self.cards.sort()
def move_cards(self, other: 'Deck', num: int):
for _ in range(num):
other.put_card(self.take_card())
class Hand(Deck):
def __init__(self, label: str):
super().__init__()
self.label = label
self.cards = []
def add_card(self, card: Card):
self.put_card(card)
def remove_card(self, card: Card):
self.cards.remove(card)
def __str__(self):
return f"{self.label}: " + ", ".join(str(card) for card in self.cards)
class PokerHand(Hand):
def __init__(self, label: str):
super().__init__(label)
def evaluate_hand(self) -> str:
return "Evaluation of Poker Hand"
class BridgeHand(Hand):
def __init__(self, label: str):
super().__init__(label)
def evaluate_hand(self) -> dict:
return "Evaluation of Bridge Hand"
|
Expanding Functionality with Deck
Compostion: This was last week’s topic, but notice how the Deck
class extends it’s own functionality by managing a collection of Card
objects in the code above?
A few other things to notice
- Static methods for creating structured data (e.g. creating the cards).
- Delegating complex logic to helper methods.
Adding Hands with Inheritance
The Hand
class inherits from Deck
, specializing its behavior for card games.
Example Usage
| # Usage
deck = Deck()
hand = Hand("Player 1")
hand.add_card(deck.deal_card())
hand.add_card(deck.deal_card())
print(hand) # Output: Player 1's Hand: Random Card 1, Random Card 2
|
Notice a few things about the Hand code
super().__init__()
is used to call the parent constructor.
- The child doesn’t need to implement any methods the parent contains already, though the child class can override (i.e.
take_card
, etc.).
- We can also provide helper methods or provide more appropriate method names that utilize the inherited methods (i.e.
add_card
).
Comparing Cards with Custom Methods - Pythonic Coding Tip
Custom comparison methods should use tuples for consistent ordering of cards.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | class Card:
# Existing code...
def to_tuple(self):
return self.suit, self.rank
def __lt__(self, other):
return self.to_tuple() < other.to_tuple()
# Usage
card1 = Card(2, 12) # Queen of Hearts
card2 = Card(3, 6) # 6 of Spades
print(card1 < card2) # Output: True
|
In General
- Defining comparison behavior for sorting and compare logic when building classes and hierarchies is advised.
- Using tuple-based comparisons increase readability and correctness since tuples are immutable.
Advanced Applications
Inheritance enables specialized decks, such as a deck for a specific card game.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13 | class PokerDeck(Deck):
def __init__(self):
super().__init__()
self.shuffle() # Shuffle upon creation
def deal_hand(self, num_cards):
return [self.deal_card() for _ in range(num_cards)]
# Usage
poker_deck = PokerDeck()
hand = poker_deck.deal_hand(5)
for card in hand:
print(card)
|
Homework/Practice
- Challenge: Create a
BlackjackHand
class that extends Hand
, adding a method to calculate the total value of the cards.
- Reflection: Write a short paragraph on the advantages of using inheritance in the card-related examples.