Object-Oriented Programming Applications in Python
Object-oriented programming is not difficult for beginners to understand but is difficult to apply. Although we’ve summarized the three-step method for object-oriented programming (define classes, create objects, send messages to objects), it’s easier said than done. A large amount of programming practice and reading high-quality code are probably the two things that can help everyone the most at this stage. Next, we’ll continue to analyze object-oriented programming knowledge through classic cases, and also connect all the Python knowledge we’ve learned before through these cases.
Example 1: Poker Game
Note: For simplicity, our poker deck only has 52 cards (no jokers). The game needs to deal 52 cards to 4 players, with each player having 13 cards. Cards are arranged in order of spades, hearts, clubs, diamonds and by rank from smallest to largest. We won’t implement other functions for now.
Using object-oriented programming methods, we first need to find objects from the problem requirements and abstract corresponding classes. We also need to find object attributes and behaviors. Of course, this is not particularly difficult. We can find nouns and verbs from the requirement description. Nouns are usually objects or object attributes, while verbs are usually object behaviors. In the poker game, there should be at least three types of objects: cards, poker deck, and players. The card, poker, and player classes are not isolated. Relationships between classes can be roughly divided into is-a relationships (inheritance), has-a relationships (association), and use-a relationships (dependency). Obviously, poker and cards have a has-a relationship because a poker deck has (has-a) 52 cards; players and cards have both association and dependency relationships because players have (has-a) cards and players use (use-a) cards.
The attributes of cards are obvious: suit and rank. We can use four numbers from 0 to 3 to represent four different suits, but this would make the code very unreadable because we don’t know the correspondence between spades, hearts, clubs, diamonds and the numbers 0 to 3. If a variable can only take a limited number of options, we can use enumerations. Unlike languages like C and Java, Python doesn’t have a keyword for declaring enumeration types, but we can create enumeration types by inheriting the Enum class from the enum module, as shown in the following code.
from enum import Enum
class Suite(Enum):
"""Suit (enumeration)"""
SPADE, HEART, CLUB, DIAMOND = range(4)
From the code above, you can see that defining an enumeration type is actually defining symbolic constants like SPADE, HEART, etc. Each symbolic constant has a corresponding value. This way, representing spades doesn’t need to use the number 0, but can use Suite.SPADE; similarly, representing diamonds doesn’t need to use the number 3, but can use Suite.DIAMOND. Note that using symbolic constants is definitely better than using literal constants because anyone who can read English can understand the meaning of symbolic constants, greatly improving code readability. Enumeration types in Python are iterable types. Simply put, enumeration types can be placed in for-in loops to sequentially extract each symbolic constant and its corresponding value, as shown below.
for suite in Suite:
print(f'{suite}: {suite.value}')
Next, we can define the card class.
class Card:
"""Card"""
def __init__(self, suite, face):
self.suite = suite
self.face = face
def __repr__(self):
suites = '♠♥♣♦'
faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
return f'{suites[self.suite.value]}{faces[self.face]}' # Return card suit and rank
You can test the Card class with the following code.
card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 13)
print(card1) # ♠5
print(card2) # ♥K
Next, we define the poker class.
import random
class Poker:
"""Poker"""
def __init__(self):
self.cards = [Card(suite, face)
for suite in Suite
for face in range(1, 14)] # List of 52 cards
self.current = 0 # Attribute to record dealing position
def shuffle(self):
"""Shuffle"""
self.current = 0
random.shuffle(self.cards) # Implement random shuffle through random module's shuffle function
def deal(self):
"""Deal"""
card = self.cards[self.current]
self.current += 1
return card
@property
def has_next(self):
"""Are there still cards to deal"""
return self.current < len(self.cards)
You can test the Poker class with the following code.
poker = Poker()
print(poker.cards) # Cards before shuffling
poker.shuffle()
print(poker.cards) # Cards after shuffling
Define the player class.
class Player:
"""Player"""
def __init__(self, name):
self.name = name
self.cards = [] # Cards in player's hand
def get_one(self, card):
"""Draw card"""
self.cards.append(card)
def arrange(self):
"""Arrange cards in hand"""
self.cards.sort()
Create four players and deal cards to their hands.
poker = Poker()
poker.shuffle()
players = [Player('East Evil'), Player('West Poison'), Player('South Emperor'), Player('North Beggar')]
# Deal cards to each player in turn, 13 cards per person
for _ in range(13):
for player in players:
player.get_one(poker.deal())
# Players arrange their cards and output name and hand
for player in players:
player.arrange()
print(f'{player.name}: ', end='')
print(player.cards)
Executing the code above will cause an exception at player.arrange() because the arrange method of the Player class uses the list’s sort method to sort the cards in the player’s hand. Sorting requires comparing the size of two Card objects, but the < operator cannot be directly applied to the Card type, so a TypeError exception occurs with the message: '<' not supported between instances of 'Card' and 'Card'.
To solve this problem, we can slightly modify the Card class code to allow two Card objects to be directly compared using <. The technique used here is called operator overloading. To implement overloading of the < operator in Python, we need to add a magic method named __lt__ to the class. Obviously, lt in the magic method __lt__ is an abbreviation of the English words “less than”. By analogy, the magic method __gt__ corresponds to the > operator, the magic method __le__ corresponds to the <= operator, __ge__ corresponds to the >= operator, __eq__ corresponds to the == operator, and __ne__ corresponds to the != operator.
The modified Card class code is as follows.
class Card:
"""Card"""
def __init__(self, suite, face):
self.suite = suite
self.face = face
def __repr__(self):
suites = '♠♥♣♦'
faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
return f'{suites[self.suite.value]}{faces[self.face]}'
def __lt__(self, other):
if self.suite == other.suite:
return self.face < other.face # Compare rank size when suits are the same
return self.suite.value < other.suite.value # Compare suit values when suits are different
Note: You can try writing a simple poker game based on the code above, such as 21 (Black Jack). You can find the game rules online yourself.
Example 2: Salary Settlement System
Requirement: A company has three types of employees: department managers, programmers, and salespeople. A salary settlement system needs to be designed to calculate employee monthly salaries based on provided employee information. Department managers have a fixed monthly salary of 15,000 yuan; programmers are paid monthly based on working hours (in hours), 200 yuan per hour; salespeople’s monthly salary consists of a base salary of 1,800 yuan plus 5% commission on sales.
Through analysis of the above requirements, we can see that department managers, programmers, and salespeople are all employees with the same attributes and behaviors. So we can first design a parent class named Employee, then derive department manager, programmer, and salesperson subclasses from this parent class through inheritance. Obviously, subsequent code won’t create Employee class objects because we need specific employee objects, so this class can be designed as an abstract class specifically for inheritance. Python doesn’t have a keyword for defining abstract classes, but we can define abstract classes through a metaclass named ABCMeta in the abc module. We won’t expand on the concept of metaclasses here. Of course, you don’t need to worry about it - just follow along.
from abc import ABCMeta, abstractmethod
class Employee(metaclass=ABCMeta):
"""Employee"""
def __init__(self, name):
self.name = name
@abstractmethod
def get_salary(self):
"""Calculate monthly salary"""
pass
In the employee class above, there’s a method named get_salary for calculating monthly salary, but since we haven’t determined which type of employee it is, calculating monthly salary is a common behavior of employees but cannot be implemented here. For methods that cannot be implemented temporarily, we can use the abstractmethod decorator to declare them as abstract methods. So-called abstract methods are methods with only declarations but no implementations. Declaring this method is to let subclasses override this method. The following code shows how to derive department manager, programmer, and salesperson subclasses from the employee class and how subclasses override the parent class’s abstract method.
class Manager(Employee):
"""Department manager"""
def get_salary(self):
return 15000.0
class Programmer(Employee):
"""Programmer"""
def __init__(self, name, working_hour=0):
super().__init__(name)
self.working_hour = working_hour
def get_salary(self):
return 200 * self.working_hour
class Salesman(Employee):
"""Salesperson"""
def __init__(self, name, sales=0):
super().__init__(name)
self.sales = sales
def get_salary(self):
return 1800 + self.sales * 0.05
The three classes Manager, Programmer, and Salesman above all inherit from Employee, and all three classes override the get_salary method. Overriding means a subclass re-implements an existing method of the parent class. I believe everyone has noticed that the get_salary methods in the three subclasses are all different, so this method will produce polymorphic behavior at runtime. Polymorphism simply means calling the same method, different subclass objects do different things.
We’ll complete this salary settlement system through the following code. Since programmers and salespeople need to input their monthly working hours and sales respectively, we use Python’s built-in isinstance function to determine the type of employee object in the code below. We talked about the type function before, which can also identify object types, but the isinstance function is more powerful because it can determine whether an object is a subtype in an inheritance structure. You can simply understand that the type function is a precise match of object types, while the isinstance function is a fuzzy match of object types.
emps = [Manager('Liu Bei'), Programmer('Zhuge Liang'), Manager('Cao Cao'), Programmer('Xun Yu'), Salesman('Zhang Liao')]
for emp in emps:
if isinstance(emp, Programmer):
emp.working_hour = int(input(f'Please enter {emp.name}\'s working hours this month: '))
elif isinstance(emp, Salesman):
emp.sales = float(input(f'Please enter {emp.name}\'s sales this month: '))
print(f'{emp.name}\'s salary this month: ¥{emp.get_salary():.2f}')
Summary
Object-oriented programming thinking is very good and conforms to human normal thinking habits, but to flexibly apply abstraction, encapsulation, inheritance, and polymorphism in object-oriented programming requires long-term accumulation and precipitation. This cannot be achieved overnight because knowledge accumulation is inherently a process of gradual progress.