Inheritance & Composition

Patterns for Code Reuse

Introduction

design patterns

Object-Oriented "Patterns"

  • Object-Oriented (OO) software design is a huge topic

  • Thinking about designing software offers an opportunity to apply craft in what we are building

  • Today will just look at two patterns as examples of OO design:

    • Inheritance

    • Composition

Inheritance

definition
finches
diagram

How Inheritance Works

  • Declare a parent-child relationship between two classes

  • The relationship is declared on the child class (the subclass)

  • Declare the name of the parent as an argument in the class definition

  • All features of the parent are automatically available on the child class

  • New features can be added or changed (overridden) on the child class

When Inheritance Makes Sense

  • Most useful when there is significant overlap between classes

  • Main benefit comes from avoiding duplicate code

  • Without substantial shared functionality, inheritance may not pay off

Subclassing in Action

Consider this example, what will be the result of lines 13-14?

class Post():
  def __init__(self, creator):
    self.creator = creator

class Tweet(Post):
  def retweet(self):
    print("retweeted by " + self.creator)

class Snap(Post):
  def expire(self):
    print("expired snap by " + self.creator)

snap = Snap("edsu")
snap.expire()
  • ANSWER: expired snap by edsu

Subclassing in Action

In this example, note how the initialization method can use a keyword argument to create an instance of the Tweet. What will be the result?

class Post():
  def __init__(self, creator):
    self.creator = creator

class Tweet(Post):
  def retweet(self):
    print("retweeted by " + self.creator)

class Snap(Post):
  def expire(self):
    print("expired snap by " + self.creator)

tweet = Tweet(creator="edsu")
tweet.retweet()
  • ANSWER: retweeted by edsu

Methods are not Shared Among Siblings

What will happen in this example?

class Post():
  def __init__(self, creator):
    self.creator = creator

class Tweet(Post):
  def retweet(self):
    print("retweeted by " + self.creator)

class Snap(Post):
  def expire(self):
    print("expired snap by " + self.creator)

tweet = Tweet(creator="edsu")
tweet.expire()
  • ANSWER: AttributeError: 'Tweet' object has no attribute 'expire'

Parent’s Methods are Shared

class Post():
  def __init__(self, creator):
    self.creator = creator
  def delete(self):
    print("post deleted by " + self.creator)

class Tweet(Post):
  def retweet(self):
    print("retweeted by " + self.creator)

class Snap(Post):
  def expire(self):
    print("expired snap by " + self.creator)

tweet = Tweet(creator="edsu")
tweet.delete()
  • post deleted by edsu

Overriding Methods

In some cases you may need/want to redefine a method or attribute from the parent class. This is known as overriding a method.

class Post():
  def __init__(self, creator):
    self.creator = creator
  def update(self):
    print("post updated by " + self.creator)

class Tweet(Post):
  def update(self):
    print("you can never update tweets!")

class Snap(Post):
  def expire(self):
    print("expired snap by " + self.creator)

tweet = Tweet(creator="edsu")
tweet.update()
  • you can never update tweets!

Composition

composition def

Things are Made from Other Things

pizza

Ingredients of a Pizza

  • crust

  • toppings

  • cheese

  • sauce

Composition in Action

Observe how the attributes work here.

class Topping():

    def __init__(self, name, num_pieces):
        self.name = name
        self.num_pieces = num_pieces

topping = Topping("pepperoni", 25)
print(topping)
  • <main.Topping object at 0x10c6374e0>

Composition in Action

Now let’s create a class Pizza that can contain Toppings:

class Pizza():

    def __init__(self):
        self.toppings = []

    def add_topping(self, topping):
        self.toppings.append(topping)

pizza = Pizza()
print(pizza)
  • <main.Pizza object at 0x10c6374e0>

Composition in Action

We can make the Pizza and Topping classes work together.

class Pizza():
    def __init__(self):
        self.toppings = []
    def add_topping(self, topping):
        self.toppings.append(topping)

class Topping():
    def __init__(self, name, num_pieces):
        self.name = name
        self.num_pieces = num_pieces

pizza = Pizza()
pizza.add_topping(Topping("pepperoni", 18))
pizza.add_topping(Topping("mushrooms", 12))
pizza.add_topping(Topping("green peppers", 15))
print(pizza.toppings)
  • [<main.Topping object at 0x10c9ae748>, <main.Topping object at 0x10c9aea58>, <main.Topping object at 0x10c9aea90>]

Composition in Action

Remember "magic" methods? A useful one is repr, which controls how things print.

class Topping():

    def __init__(self, name, num_pieces):
        self.name = name
        self.num_pieces = num_pieces

    def __repr__(self):
        return "{} pieces of {}".format(self.num_pieces, self.name))

pizza = Pizza()
pizza.add_topping(Topping("pepperoni", 18))
pizza.add_topping(Topping("mushrooms", 12))
pizza.add_topping(Topping("green peppers", 15))
print(pizza.toppings)
  • 18 pieces of pepperoni, 12 pieces of mushrooms, 15 pieces of green peppers

Composition in Action

Let’s define a method on Pizza that "reads" its toppings. What will be the result?

class Pizza():
    """ ... imagine init and add_topping are the same as before ... """

    def num_pieces(self):
        count = 0
        for topping in self.toppings:
            count += topping.num_pieces
        return count

pizza = Pizza()
pizza.add_topping(Topping("pepperoni", 18))
pizza.add_topping(Topping("mushrooms", 12))
pizza.add_topping(Topping("green peppers", 15))
print(pizza.num_toppings())
  • 45

Conclusion

Summary

  • Today we dipped a toe into the topic of OO Patterns.

  • We learned about Inheritance, which is useful for defining related classes with shared functionality.

  • We also learned about Composition, which is useful for defining objects that will be contained by other objects.

Final Takeaways

timeless
  • Composition is in many cases a much more useful OO pattern.

  • Inheritance should be used sparingly.

  • Composition is well suited to many data-science applications.

  • Always design your code to fit your requirements!