Object-Oriented Programming in Practice

by Pyrastra Team
Object-Oriented Programming in Practice

Object-oriented programming is not difficult for beginners to understand but is difficult to apply. Although we’ve summarized a 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, each player has 13 cards, arranged in the order of spades, hearts, clubs, diamonds and by rank from small to large. 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 the attributes and behaviors of objects. 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 three classes - card, poker, and player - are not isolated. The 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 have both association and dependency relationships with cards because players have (has-a) cards in their hands 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’s value only has 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 code below.

from enum import Enum


class Suite(Enum):
    """Suit (enumeration)"""
    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, 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 being able to read English means understanding the meaning of symbolic constants, which greatly improves 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's 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)  # ♥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):
        """Check if 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

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 the players’ hands.

poker = Poker()
poker.shuffle()
players = [Player('Eastern Heretic'), Player('Western Poison'), Player('Southern Emperor'), Player('Northern 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 Player uses the list’s sort 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 make two Card objects directly comparable 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 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   # If suits are different, compare suit values

Note: You can try writing a simple poker game based on the code above, such as Blackjack. You can find the game rules online yourself.

Example 2: Payroll System

Requirement: A company has three types of employees: department managers, programmers, and salespeople. We need to design a payroll system to calculate employee monthly salaries based on provided employee information. Department managers have a fixed monthly salary of 15,000 yuan; programmers are paid by 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, and then derive department manager, programmer, and salesperson subclasses from this parent class through inheritance. Obviously, subsequent code won’t create objects of the Employee class 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 use a metaclass named ABCMeta from the abc module to define abstract classes. 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. However, 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 that only have 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 that a subclass re-implements a method that already exists in the parent class. I believe everyone has noticed that the get_salary in the three subclasses is different, so this method will produce polymorphic behavior at runtime. Polymorphism simply means calling the same method, different subclass objects do different things.

We complete this payroll system through the code below. Since programmers and salespeople need to input their monthly working hours and sales respectively, in the code below we use Python’s built-in isinstance function to determine the type of employee object. The type function we discussed earlier 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'Enter {emp.name}\'s working hours this month: '))
    elif isinstance(emp, Salesman):
        emp.sales = float(input(f'Enter {emp.name}\'s sales this month: '))
    print(f'{emp.name}\'s monthly salary: ¥{emp.get_salary():.2f}')

Summary

Object-oriented programming thinking is very good and conforms to human normal thinking habits. However, to flexibly use 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.