Polymorphism

Polymorphism

Polymorphism (poly- multiple, morph- shape), is the ability to write code that works over multiple data types. It’s very easy to write polymorphic functions in Python–for instance, what type does the following function return?

def add(x, y):
  return x + y

Depending on the type of its arguments, it could return an integer, a float, or a string. This property can be confusing–if we only plan on calling this function with a specific type, we might want to add annotations:

def add(x: str, y: str) -> str:
  return x + y

Polymorphic code can also be useful, though, as we’ll see below.

Polymorphism at the library

Let’s revisit an example we saw in CSCI 0111: tracking items at a library. The library has both books and movies, and we can make classes for both of them:

class Book:
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author

class Movie:
    def __init__(self, title: str, director: str, actors: list):
        self.title = title
        self.director = director
        self.actors = actors

We can represent a library as a list of these items:

library = [Book("The Dispossessed", "Ursula K LeGuin"),
           Movie("Parasite", "Bong Joon-ho", ["Song Kang-ho", "Lee Sun-kyun"])]

An operation we might want on libraries is text search over library items. An advanced search might allow users to search over authors or directors, or search for particular item types (books or movies). In Pyret we implemented this by making books and movies constructors of the same datatype. We could do a similar thing in Python, but there’s a more elegant solution.

We’ll add a method to each class:

class Book:
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author

    def matches(self, query: str) -> bool:
        return search in self.title or search in self.author

class Movie:
    def __init__(self, title: str, director: str, actors: list):
        self.title = title
        self.director = director
        self.actors = actors

    def matches(self, query: str) -> bool:
        return search in self.title or search in self.director or 
          any([search in actor for actor in self.actors])

Then we can implement our search function:

def search(library: list, query: str) -> list:
    return [item for item in library if item.matches(query)]

Let’s work though what happens when we call search(library, "LeGuin"). See the lecture capture for details.

Inheritance

Right now, we’re considering each class to be totally separate, with no shared code or data. For instance, even though Book~s and ~Movie~s both implement a ~matches method, the two methods do are have completely different implementations. Inheritance gives us a way to share code between objects. Classes can inherit from other classes, like this:

class A:
    pass

class B(A):
    pass

When there’s an inheritance relationship like this between two classes, we sat that A is the superclass of B and B is a subclass of A.

Let’s see an example of inheritance in action. Let’s say our library wants to track which items have been checked out, and only return items in a search if they are actually available. First, we’ll implement a LibraryItem class, which we’ll use as the superclass for both books and movies:

class LibraryItem:
    def __init__(self):
        self.checked_out = False

    def checkout(self):
        self.checked_out = True

    def return(self):
        self.checked_out = True

The LibraryItem class only handles checking items out and back in–it doesn’t know anything about what the items actually are.

In order to use our new superclass, we’ll make some changes to the Book and Movie classes:

class Book(LibraryItem):
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author
        super().__init__(self)

class Movie(LibraryItem):
    def __init__(self, title: str, director: str, actors: list):
        self.title = title
        self.director = director
        self.actors = actors
        super().__init__(self)

Both Book and Movie objects will now have a checked_out field, as well as the checkout and return methods. We can use the field in each class’s matches method:

class Book(LibraryItem):

      def matches(self, query: str) -> bool:
          return (not self.checked_out) and (search in self.title or search in self.author)

class Movie(LibraryItem):

      def matches(self, query: str) -> bool:
          return (not self.checked_out) and
             search in self.title or search in self.director or 
             any([search in actor for actor in self.actors])