Stay Classy: A Look at Class Abstraction

zmbailey

Zander Bailey

Posted on May 13, 2019

Stay Classy: A Look at Class Abstraction

Classes are an important part of object oriented programming, and they can be very powerful if you understand how to use them correctly. Almost any variable can be considered an object. It's easy to see this in a language like Java, where everything is written in terms of classes, but today we're going to discuss objects in Python. In some way Python seems to breeze past much of the intricacies of typing that makes abstraction so important in Java, but that doesn't mean it's not useful. Abstraction and inheritance can be helpful in many ways, often for giving the same methods or behavior to multiple classes, or for using the same basic structure in several similar classes. But before we get too in depth about abstraction, let's first take a moment to understand classes in general.

A class is like a template for an object, it defines the behavior and attributes for an object, and may contain class methods or attributes which may be used on their own. Once defined it can be used to create instances, which may be individually transformed to hold specific data. Class methods, also known as Static methods, may be called by referencing the class, but do not apply to any specific instance of the class. Class methods are something that come up more frequently in languages like Java, but do feature from time to time in Python.

Levels of Abstraction: Basic, Abstract, Interface

There are three basic levels of class abstraction: Classes, Abstract Classes, and Interfaces. Classes, or Concrete classes, are the most common, the types you will come across most frequently. Concrete classes are classes that have fully implemented methods, and can be instantiated. Abstract classes can have both abstract methods and normal, implemented methods, but cannot be instantiated. Abstract classes exist to be inherited from other classes, and are never intended to be instantiated on their own. Like abstract classes, Interfaces cannot be instantiated, but also have all abstract methods by default, and are generally not intended to have implemented methods. Although Interfaces are possible in python, they are so uncommon that it's not really worth it to discuss them here. Instead, we will focus on abstract classes. Abstract classes are still a little uncommon in python, and even require an external package to implement.

Example:

Here is an example structure of an abstract class hierarchy:

from abc import ABC, abstractmethod

class Animal(ABC):

    @abstractmethod
    def make_noise(self):
        pass

    @abstractmethod
    def move(self):
        pass
Enter fullscreen mode Exit fullscreen mode

Here we have an abstract class. Animal is a basic definition, with several methods that could apply to any animal. Note how the methods have the @abstractmethod keyword, and only use pass as the body. These methods are abstract, because although any animal could use them, each animal might perform them differently. Animal can be abstract, since it is contains abstract methods and is never intended to be instantiated on its own. You can imagine that there is no animal that is only ever referred to as Animal.

Bird and Feline are both types of animal, but still somewhat general terms. At this point we can start to define some behavior that is common to these groups of animals, like saying that birds fly(except for the edge cases of Penguins, Emus, and Ostriches), and that felines tends to stalk.

class Bird(Animal):
    def move(self):
        print("*Flying!*")

class Feline(Animal):
    def move(self):
        print("*Silently stalk*")
Enter fullscreen mode Exit fullscreen mode

But still we can be more specific, like saying a Lion instead of Feline, or BarredOwl instead of Bird. At this point we can define the other method, because specific animals may have different behavior, like a Lion roaring where a Calico might meow.

class BarredOwl(Bird):
    def make_noise(self):
        print("Hoo!")

class Mallard(Bird):
    def make_noise(self):
        print("Quack!")

class Lion(Feline):
    def make_noise(self):
        print("Roar!")

class Calico(Feline):
    def make_noise(self):
        print("Meow!")
Enter fullscreen mode Exit fullscreen mode

Now we can create a specific animal, and it will have all the behavior we have told it to inherit:

Lucky = Calico()

Lucky.make_noise()
Lucky.move()

Output:
Meow!
*Silently stalk*
Enter fullscreen mode Exit fullscreen mode

Something else we can do with abstract classes is to create factory methods.
Factory methods are methods used to create other objects with varying levels of specificity. First, we start with the generic, abstract level class, say a Show:

class Show(ABC):

    def __init__(self):
        self.cast = []
        self.add_performers()

    def add_performers(self):
        for _ in range(4):
            self.cast.append(self.get_performer())

    @abstractmethod
    def get_performer(self):
        pass
Enter fullscreen mode Exit fullscreen mode

With this setup, we can define two types of shows, each using different type of performers:

class Play(Show):

    def get_performer(self):
        return Actor()


class Concert(Show):

    def get_performer(self):
        return Musician()

Enter fullscreen mode Exit fullscreen mode

We have two specific types of shows, a play and a concert. Each one can create performers with get_performer(), but depending on the type of Show it creates a different type of performer. For clarification, here is the Performer class and its subclasses:

class Performer(ABC):

    def perform(self):
           pass

class Actor(Performer):

    def perform(self):
        print("The play's the thing!")

class Musician(Performer):

    def perform(self):
        print("Music!")
Enter fullscreen mode Exit fullscreen mode

Actor and Musician are both subtypes of Performer, so in this case the Factory method is get_performer(), which can always be relied on to return a Performer, which has a consistent set of methods and attributes, whether it is specifically an Actor or a Musician.

Abstract classes and methods can be interesting, and rewarding to work with once you understand them. Abstraction is very common in many object oriented languages, but Python's informal approach to typing eliminates some of the complexity that necessitates abstraction in other languages.

Practical Implementation

Now that we've seen how abstraction works, and we've learned about factory methods, let's take a look at a more practical example. Let's take the case of making plots. In order to make a plot there are any number of tweaks or different plot styles we might use, but suppose we have a few styles we want to be able to replicate on numerous occasions, but it's complex to set it up each time. We can start with a generic class, written here as CustomPlot.

class CustomPlot():

    def __init__(self,X,y):
        self.X = X
        self.y = y
        self.run_plots()

    def run_plots(self):
        pass

    def scatter(self):
        pass

    def reg(self):
        pass

    def kde(self):
        pass
Enter fullscreen mode Exit fullscreen mode

notice that the __init__ function sets up the x and y that will be used for the plot, and then runs the function to create the plot, but the behavior for that function does not exist yet. Next we will create specific versions of this class that will implement these, to create whatever type of plot or plots we decide.

class NormalPlots(CustomPlot):

    def run_plots(self):
        self.scatter()
        self.reg()
        self.kde()

    def scatter(self):
        sns.scatterplot(x=self.X,y=self.y)

    def reg(self):
        sns.regplot(x=self.X,y=self.y)

    def kde(self):
        sns.kdeplot(self.X,self.y)

class SpecialPlot(CustomPlot):

    def run_plots(self):
        g = sns.jointplot(x=self.X,y=self.y, kind="kde", color="m")
        g.plot_joint(plt.scatter, c="w", s=30, linewidth=1, marker="+")
        g.ax_joint.collections[0].set_alpha(0)   

class JointPlots(CustomPlot):

    def run_plots(self):
        self.scatter()
        self.reg()
        self.kde()

    def scatter(self):
        sns.jointplot(x=self.X, y=self.y, kind="scatter")

    def reg(self):
        sns.jointplot(x=self.X,y=self.y,kind='reg')

    def kde(self):
        sns.jointplot(x=self.X,y=self.y,kind="kde")
Enter fullscreen mode Exit fullscreen mode

This gives us three different types of plot object, that each print a different style of plot. NormalPlots prints a scatter plot, a regression plot, and a KDE plot, all on the same axes:

The SpecialPlot demonstrates a different logic for creating plots, and how the class will still function without defining all of its functions. It creates a KDE with a scatterplot overlaid in white to display the distribution in an interesting effect:

And JointPlots uses Seaborn's jointplots to plot 3 individual plots:



This are kept simple for ease of demonstration, but you could go further and add parameters for color, figure size, or other attributes, which would then be applied in the appropriate places, all without having to directly touch the plotting methods.

Overall python is very flexible when it comes to classes and types, so there may be ways around many of the obstacles that abstraction is intended to solve. Even so, it is still important to understand how these structures work, to make you a more versatile programmer.

The files containing all the code used in this tutorial can be found here.

💖 💪 🙅 🚩
zmbailey
Zander Bailey

Posted on May 13, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related