Object-oriented programming is not hard for beginners to understand, but it is hard to use well. Although we already summarized the three steps of object-oriented programming for everyone, defining classes, creating objects, and sending messages to objects, it is easy to say and hard to do. A lot of programming practice and reading good code are probably the two things that can help everyone the most at this stage. Next, we will still use classic examples to explain object-oriented programming, and at the same time connect the Python knowledge we learned before together.
Note: To keep things simple, our deck has only 52 cards, with no jokers. The game needs to deal all 52 cards to 4 players so that each player receives 13 cards. The cards in each player's hand should be arranged by suit in the order spades, hearts, clubs, diamonds, and by rank from low to high. No other game logic is implemented for now.
When using object-oriented programming, we first need to find the objects from the problem requirements and abstract the corresponding classes. In addition, we also need to find the attributes and behaviors of the objects. Of course, this is not especially difficult. We can find nouns and verbs from the requirement description. Nouns are usually objects or object attributes, and verbs are usually object behaviors. In a poker game, there should be at least three kinds of objects: cards, poker, and players. The three classes, card, poker, and player, are not isolated either. The relationship between classes can be roughly divided into an is-a relationship (inheritance), a has-a relationship (association), and a use-a relationship (dependency). Obviously, poker and cards are in a has-a relationship, because a deck of poker has 52 cards; between a player and cards, there is not only an association relationship but also a dependency relationship, because a player has cards and also uses cards.
The attributes of a card are easy to see. There are suit and face value. We can use the four numbers 0 to 3 to represent the four different suits, but code like this will be very hard to read, because we do not know the matching relationship between spades, hearts, clubs, diamonds and the numbers 0 to 3. If a variable has only a limited number of possible values, we can use an enumeration. Different from languages such as C and Java, Python does not have a keyword for declaring an enumeration type, but we can create an enumeration type by inheriting the Enum class in the enum module, as shown below.
from enum import Enum
class Suite(Enum):
"""Suit (enum)"""
SPADE, HEART, CLUB, DIAMOND = range(4)From the code above, we can see that defining an enumeration type is actually defining symbolic constants, such as SPADE and HEART. Every symbolic constant has its corresponding value. In this way, to represent spades we do not need to use the number 0, but can use Suite.SPADE; in the same way, to represent diamonds we do not need to use the number 3, but can use Suite.DIAMOND. Note that using symbolic constants is surely better than using literal constants, because if you can understand English, you can understand the meaning of symbolic constants, and the readability of the code will improve a lot. Enumeration types in Python are iterable types. Simply speaking, that means we can put an enumeration type into a for-in loop and take out every symbolic constant and its corresponding value one by one, 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 the suit and rank.We can test the Card class with the following code.
card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 13)
print(card1) # ♠5
print(card2) # ♥KNext, we define the Poker class.
import random
class Poker:
"""Deck of cards"""
def __init__(self):
self.cards = [Card(suite, face)
for suite in Suite
for face in range(1, 14)] # A list made up of 52 cards.
self.current = 0 # The current dealing position.
def shuffle(self):
"""Shuffle the deck"""
self.current = 0
random.shuffle(self.cards) # Randomize the order with random.shuffle.
def deal(self):
"""Deal one card"""
card = self.cards[self.current]
self.current += 1
return card
@property
def has_next(self):
"""Whether there are more cards to deal"""
return self.current < len(self.cards)We 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.Now define the Player class.
class Player:
"""Player"""
def __init__(self, name):
self.name = name
self.cards = [] # The cards in the player's hand.
def get_one(self, card):
"""Draw a card"""
self.cards.append(card)
def arrange(self):
"""Arrange the cards in hand"""
self.cards.sort()Create four players and deal the cards to them.
poker = Poker()
poker.shuffle()
players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]
# Deal cards to each player in turn, 13 cards per player.
for _ in range(13):
for player in players:
player.get_one(poker.deal())
# Let each player arrange their cards and print the name and hand.
for player in players:
player.arrange()
print(f'{player.name}: ', end='')
print(player.cards)Running the code above will raise an exception at player.arrange(), because the arrange method of Player uses the sort method of the list to sort the cards in the player's hand. Sorting needs to compare the size of two Card objects, but the < operator cannot be directly used on the Card type, so a TypeError exception appears. The exception message is: "<" not supported between instances of 'Card' and 'Card'.
To solve this problem, we can make a small change to the code of the Card class, so that two Card objects can be directly compared by using <. The technique used here is called operator overloading. In Python, to overload the < operator, we need to add a magic method named __lt__ in the class. It is easy to see that lt in the magic method __lt__ is the abbreviation of the English words "less than". In the same way, the magic method __gt__ corresponds to the > operator, the magic method __le__ corresponds to the <= operator, the magic method __ge__ corresponds to the >= operator, the magic method __eq__ corresponds to the == operator, and the magic method __ne__ corresponds to the != operator.
The modified Card class is shown below.
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 # If suits are the same, compare ranks.
return self.suite.value < other.suite.value # Otherwise compare suit values.Note: You can try writing a simple poker game on top of the code above, such as Blackjack. You can look up the rules online on your own.
Task: A company has three types of employees: department managers, programmers, and salespeople. We need to design a payroll system that calculates monthly salaries based on the employee information provided. A department manager has a fixed monthly salary of
15000; a programmer is paid200yuan per hour of work; a salesperson has a base salary of1800yuan plus a commission of5%of their sales.
Through the analysis of the requirement above, we can see that department managers, programmers, and salespeople are all employees, and they have the same attributes and behaviors. Then we can first design a parent class named Employee, and then derive the three child classes, department manager, programmer, and salesperson, from this parent class through inheritance. Obviously, the code later will not create objects of the Employee class, because what we need are concrete employee objects, so this class can be designed as an abstract class only used for inheritance. In Python there is no keyword for defining an abstract class, but we can define an abstract class through the metaclass ABCMeta in the abc module. We will not explain the concept of metaclass here. Of course, everyone does not need to get stuck on it, just follow and do it.
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"""
passIn the employee class above, there is a method named get_salary for calculating monthly salary. But because it is still not clear which type of employee it is, although calculating monthly salary is a common behavior of employees, there is no way to implement it here. For methods that cannot be implemented for the time being, we can use the abstractmethod decorator to declare them as abstract methods. A so-called abstract method is a method that only has declaration but no implementation, and declaring this method is to let child classes override this method. The code below shows how to derive the three child classes Manager, Programmer, and Salesman from the employee class, and how the child classes override the abstract method of the parent class.
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.05The three classes Manager, Programmer, and Salesman above all inherit from Employee, and the three classes each override the get_salary method. Overriding means a child class makes a new implementation for a method that already exists in the parent class. I believe everyone has already noticed that the get_salary methods in the three child classes are different, so this method will produce polymorphic behavior when the program runs. Simply speaking, polymorphism means calling the same method, but different child class objects do different things.
We can complete this salary settlement system through the code below. Because programmers and salespeople need to enter the working hours and sales amount for this month, in the code below we use Python's built-in isinstance function to judge the type of an employee object. The type function we talked about before can also recognize the type of an object, but the isinstance function is more powerful, because it can judge whether an object is a child type under some inheritance structure. You can simply understand it like this: the type function does exact matching on object type, while the isinstance function does fuzzy matching on object type.
emps = [Manager('刘备'), Programmer('诸葛亮'), Manager('曹操'),
Programmer('荀彧'), Salesman('张辽')]
for emp in emps:
if isinstance(emp, Programmer):
emp.working_hour = int(input(f'请输入{emp.name}本月工作时间: '))
elif isinstance(emp, Salesman):
emp.sales = float(input(f'请输入{emp.name}本月销售额: '))
print(f'{emp.name}本月工资为: ¥{emp.get_salary():.2f}元')The idea of object-oriented programming is very good, and it also matches normal human thinking habits. But if we want to use abstraction, encapsulation, inheritance, and polymorphism in object-oriented programming flexibly, it needs a long time of accumulation. This thing cannot be done in one step, because the accumulation of knowledge is itself a process of small drops becoming a river.