Classes and subclasses

Python classes are used to give greater structure and object orientated facilities to many projects, so it's important to have a firm grasp on class behaviors and syntax. Even if you've used object orientated design in other language like Java or PHP, Python has some particularities that you need to understand. Listing A-15 shows a simple Python class.

Listing A-15. Python class syntax and behavior

class Drink():
      """ Drink class """
      def __init__(self,size):
            self.size = size
      # Used to display object instance 
      def __str__(self):
            return 'Drink: size %s' % (self.size)
      # Helper method for size in ounces
      def sizeinoz(self):
            if self.size == "small":
                  return "8 oz"
            elif self.size == "medium":
                  return "12 oz"
            elif self.size == "large":
                  return "24 oz"
                  return "Unknown"

thedrink = Drink("small")
print("thedrink is %s " % thedrink.sizeinoz())

The first thing to notice about listing A-15 is it uses the Python class keyword to declare the start of a Python class. Next, you can see various class methods which are declared with the same def keyword as standard Python methods. Notice all the class methods use the self argument to gain access to the class/object instance, a construct that's prevalent in almost all Python classes, since Python doesn't grant access to the instance transparently in class methods like other object oriented languages (e.g. in Java you can just reference this inside a method without it being a method argument).

Now let's move on the some calls made on the Drink class in listing A-15. The first call in listing A-15 Drink("small") creates an instance of the Drink class. When this call is invoked, Python first triggers the __init__ method of the class to initialize the instance. Notice the two arguments __init__(self,size). The self variable represents the instance of the object itself, while the size variable represents an input variable provided by the instance creator, which in this case is assigned the small value. Inside the __init__ method, the self.size instance variable is created and assigned the size variable value.

The second call in listing A-15 print(thedrink) outputs the thedrink class instance which prints Drink: size small. What's interesting about the output is that it's generated by the class method __str__, which as you can see in listing A-15 returns a string with the value of the size instance variable. If the class didn't have a __str__ method definition, the call to print(thedrink) would output something like <__main__.Drink object at 0xcfb410>, which is the rather worthless/unfriendly in-memory representation of the instance. This is the purpose of the __str__ method in classes, to output a friendly instance value.

The third call in listing A-15 print("thedrink is %s " % thedrink.sizeinoz()) invokes the sizeinoz() class method on the instance and outputs a string based on the size instance variable created by __init__. Notice the sizeinoz(self) method declares self -- just like __init__ and __str__ -- to be able to access the instance value and perform its logic.

Now that you have a basic understanding of Python classes, let's explore Python subclasses. Listing A-16 illustrates a subclass created from the Drink class in listing A-15.

Listing A-16. Python subclass syntax and behavior

class Coffee(Drink):
      """ Coffee class """
      beans = "arabica"
      def __init__(self,*args,**kwargs):
            self.temperature = kwargs['temperature']
      # Used to display object instance 
      def __str__(self):
            return 'Coffee: beans %s, size %s, temperature %s' % (self.beans,self.size,self.temperature)

thecoffee = Coffee("large",temperature="cold")
print("thecoffee is %s " % thecoffee.sizeinoz())

Notice the class Coffee(Drink) syntax in listing A-16, which is Python's inheritance syntax (i.e. Coffee is a subclass or inherits its behavior from Drink). In addition, notice that besides the subclass having its own __init__ and __str__ methods, it also has the beans class field.

Now let's creates some calls on the Coffee subclass in listing A-16. The first call in listing A-16 Coffee("large",temperature="cold") creates a Coffee instance which is a subclass of the Drink instance. Notice the Coffee instance uses the arguments "large",temperature="cold" and in accordance with this pattern, the __init__ method definition is def __init__(self,*args,**kwargs).

Next, is the initialization of the parent class with Drink.__init__(self,*args), the *args value in this case is large and matches the __init__ method of the parent Drink class. Followed is the creation of the self.temperature instance variable which is assigned the value from kwargs['temperature'] (e.g. the value that corresponds to the key temperature).

The second call in listing A-16 print(thecoffee) outputs the thecoffee class instance which prints Coffee: beans arabica, size large, temperature cold. Because the Coffee class has its own __str__ method, it's used to output the object instance -- overriding the same method from the parent class -- if there were no such method definition, then Python would look for a __str__ method in the parent class (i.e. Drink) and use that, and if no __str__ method were found, then Python would output the in-memory representation of the instance.

The third and final call in listing A-16 print("thecoffee is %s " % thecoffee.sizeinoz()) invokes the sizeinoz() class method on the instance and outputs a string based on the size instance variable. The interesting bit about this call is it demonstrates object orientated polymorphism, the Coffee instance calls a method in the parent Drink class and works just like if it were a Drink instance.