d

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.

15 St Margarets, NY 10033
(+381) 11 123 4567
ouroffice@aware.com

 

KMF

Create Your Own Abstract Base Class in Python

We have two general paths to creating classes that are similar: we can leverage duck typing or we can define common abstractions. When we leverage duck typing, we can formalize the related types by creating a type hint using a protocol definition to enumerate the common methods or Union[] to enumerate the common types. There are many influencing factors that suggest one or the other approach. While duck typing offers the most flexibility, we may sacrifice the ability to use mypy. An abstract base class definition can be wordy and potentially confusing. 

This article is¬†an excerpt from my book¬†Python Object-Oriented Programming, Fourth Edition. I wrote this book along with¬†Dusty Phillips as the Co-Author. This book describes the object-oriented approach to creating programs in Python with examples. This excerpt is taken from the chapter that covers Abstract Base Classes and Operator Overloading.‚ÄĮ¬†

We will learn to create an abstract base class with an example. We will build a simulation of games that involve polyhedral dice. 

These are the dice including four, six, eight, twelve, and twenty sides. The six-sided dice are conventional cubes. Some sets of dice include 10-sided dice, which are cool, but aren’t ‚Äď technically ‚Äď a regular polyhedron; they’re two sets of five “kite-shaped” faces. One question that comes up is how best to simulate rolls of these differently shaped dice. There are three readily available sources of random data in Python: ‚ÄĮ¬†

  • the¬†random¬†module,¬†
  • the¬†os¬†module, and¬†
  • the¬†secrets¬†module.¬†

If we turn to third-party modules, we can add cryptographic libraries like¬†pynacl, which offer yet more random number capabilities. ‚ÄĮ¬†

Rather than bake the choice of random number generator into a class, we can define an abstract class that has the general features of a die. A concrete subclass can supply the missing randomization capability. The random module has a very flexible generator. The¬†os¬†module’s capability is limited but involves using an entropy collector to increase randomness. Flexibility and high entropy are generally combined by cryptographic generators. ‚ÄĮ¬†

To create our dice-rolling abstraction, we’ll need the¬†abc¬†module. This is distinct from the¬†module. The¬†abc¬†module has the foundational definitions for abstract classes:‚ÄĮ¬†

import abc  
class Die (abc.ABC):
  
  	def __init__(self) -> None:
      
		self.face: int  
          
		self.roll()  
        
	@abc.abstractmethod   
    
	def roll(self) -> None: 
      
		...      
        
	def __repr__(self) -> str:  
      
	return f"{self.face}"  

We’ve defined a class that inherits from the¬†abc. ABC¬†class. Using ABC as the parent class assures us that any attempt to create an instance of the Die class directly will raise a TypeError exception. This is a runtime exception; it’s also checked by¬†mypy.¬†We’ve marked a method,¬†roll()¬†as abstract with the¬†@abc.abstract¬†decorator. This isn’t a very complex method, but any subclass should match this abstract definition. This is only checked by¬†mypy. Of course, if we make a mess of the concrete implementation, things are likely to break at runtime. Consider this mess of code: ‚ÄĮ¬†

>>>classBad(Die):   

...       defroll(self, a: int, b: int) -> float:   

...             return (a+b)/2  

This will raise a TypeError exception at runtime. The problem is caused by the base class __init__()¬†not providing the a and b parameters to this strange-looking¬†roll()¬†method. This is a valid Python code, but it doesn’t make sense in this context. The method will also generate¬†mypy¬†errors, providing ample warning that the method definition doesn’t match the abstraction. Here’s what two proper extensions to the¬†Die¬†class look like: ‚ÄĮ¬†

class D4(Die): 

    def roll(self) -> None: 

        self.face = random.choice((1, 2, 3, 4)) 

class D6(Die): 

    def roll(self) -> None: 

        self.face = random.randint(1, 6) 

We’ve provided methods that provide a suitable definition for the abstract placeholder in the¬†Die¬†class. They use vastly different approaches to selecting a random value. The four-sided die uses¬†random.choice(). The six-sided die ‚Äď the common cube most people know ‚Äď uses¬†random.randint(). ‚ÄĮ¬†

Let’s go a step further and create another abstract class. This one will represent a handful of dice. Again, we have¬†a number of¬†candidate solutions, and we can use an abstract class to defer the final design choices. The interesting part of this design is the differences in the rules¬†for games with handfuls of dice. In some games, the rules require the player to roll all the dice. ‚ÄĮ¬†

The rules for a lot of games with two dice require the player to roll both dice. In other games, the rules allow players to save dice, and re-roll selected dice. In some games, like Yacht, the players are allowed at most two re-rolls. In other games, like Zilch, they are allowed to re-roll until they elect to save their score or roll something invalid and lose all their points, scoring zilch (hence the name). These are dramatically different rules that apply to a simple list of¬†Die¬†instances. Here’s a class that leaves the roll implementation as an abstraction: ‚ÄĮ¬†

class Dice(abc.ABC): 

    def __init__(self, n: int, die_class: Type[Die]) -> None: 

    self.dice = [die_class() for _ in range(n)] 

 

    @abc.abstractmethod 

    def roll(self) -> None: 

        ... 

    @property 

    def total(self) -> int: 

        return sum(d.face for d in self.dice) 

The¬†__init__()¬†method expects an integer, n, and the class used to create Die instances, named¬†die_class. The type hint is¬†Type[Die], telling¬†mypy¬†to be on the lookout for any subclass of the abstract base class¬†Die. ‚ÄĮ¬†

We don’t expect an instance of any of the Die subclasses; we expect the class object itself. We’d expect to see¬†SomeDice(6, D6)¬†to create a list of six instances of the¬†D6¬†class. ‚ÄĮWe’ve defined the collection of¬†Die¬†instances as a list because that seems simple. Some games will identify dice by their position when saving some dice and rerolling the remainder of the dice, and the integer list indices seem useful for that. This subclass implements the roll-all-the-dice rule: ‚ÄĮ¬†

class SimpleDice(Dice): 

    def roll(self) -> None: 

        for d in self.dice: 

            d.roll() 

Each time the application evaluates¬†roll(), all the dice are updated. It looks like this: ‚ÄĮ¬†

>>> sd = SimpleDice(6, D6)   

>>> sd.roll()   

>>> sd.total   

23   

The object,¬†sd, is an instance of the¬†concrete¬†class,¬†SimpleDice, built from the abstract class,¬†Dice. The instance of¬†SimpleDice¬†contains six instances of the¬†D6¬†class. This, too, is a concrete class built from the abstract class¬†Die. Here’s another subclass that provides a dramatically different set of methods. Some of these, fill in the spaces left by abstract methods. Others, however, are unique to the subclass: ‚ÄĮ¬†

 

class YachtDice(Dice): 

def __init__(self) -> None: 

super().__init__(5, D6) 

self.saved: Set[int] = set() 

      def saving(self, positions: Iterable[int]) -> 



"YachtDice": 

if not all(0 <= n < 6 for n in positions): 

raise ValueError("Invalid position") 

    self.saved = set(positions)  

return self      

 

def roll(self) -> None:  

for n, d in enumerate(self.dice):  

if n not in self.saved:                  

d.roll()          

self.saved = set() 

We’ve created a set of saved positions. This is initially empty. We can use the¬†saving()¬†method to provide an iterable collection of integers as positions to save. It works like this: ‚ÄĮ¬†

>>> sd = YachtDice()   

>>> sd.roll()>>> sd.dice   

[2, 2, 2, 6, 1]   

>>> sd.saving([0, 1, 2]).roll()   

>>> sd.dice   

[2, 2, 2, 6, 6]   

We improved the hand from three of a kind to a full house. ‚ÄĮ¬†

In both cases, the¬†Die¬†class and the¬†Dice¬†class, it’s not clear that the¬†abc.ABC¬†base class and the presence of¬†@abc.abstractmethod¬†decoration is dramatically better than providing a concrete base class with a common set of default definitions. ‚ÄĮ¬†

In some languages, the abstraction-based definition is required. In Python, because of duck typing, abstraction is optional. In cases where it clarifies the design intent, use it. In cases where it seems fussy and little more than overhead, set it aside. Because it’s used to define the collections, we’ll often use the¬†collection.abc¬†names in type hints to describe the protocols objects must follow. In less common cases, we’ll leverage the¬†collections.abc¬†abstractions to create our own unique collections.‚ÄĮ¬†

Hope this helps you!

Credit: Source link

Previous Next
Close
Test Caption
Test Description goes like this