Scientific Programming II

Programming in Python

OOP in Python


The easiest way to explore the differences between Java and Python is to look at how we would implement a data structure in Python. In this example, we will build a simple linked list, though do note that we would never actually use a list like this in Python!

# This line is automatically added by the PyCharm IDE. It simply
# sets the author meta-variable for this module. If you are running
# your Python programs via the command line like a true wizard, it
# is not required.
__author__ = 'Mr. Davis'

# We define a class in Python with the class keyword. If we were to
# define a superclass for our Node, we would include a list of
# superclasses in parentheses following the class name:
#   class Node(SuperClass1, SuperClass2):
class Node:
    # By giving the argument 'next' a default parameter, it will
    # automatically be set to None if there is nothing passed as
    # a parameter. Therefore, the following two statements will
    # be equivalent:
    #   x = Node(2, None)
    #   x = Node(2)
    # We can, however, pass 'next' a parameter if we so choose:
    #   x = Node(2, other_node)
    def __init__(self, data, next = None):
        self.data = data
        self.next = next

    # __str__() returns a human-readable string representation
    # of the class. If for some reason you needed a machine-
    # readable representation, you should define a __repr__()
    # method. However, we won't need to do this in this course.
    def __str__(self):
        # This is an example of setting a variable based on a
        # condition. If our next Node is None, then we will 
        # append an empty string. Otherwise, we will append
        # an arrow indicating that we have a Node following
        # us.
        suffix = '' if self.next is None else ' -> '
        return "(" + str(self.data) + ")" + suffix

    # __cmp__() is a 'magic method' that will allow us to use
    # the comparison operators with our Nodes. In addition, there
    # are six other magic comparison methods, corresponding to 
    # each comparison operator:
    #   __eq__() -> ==
    #   __ne__() -> !=
    #   __lt__() -> <
    #   __gt__() -> >
    #   __le__() -> <=
    #   __ge__() -> >=
    # Other magic methods exist for other operators and useful
    # builtins in Python. In Python 3 (which you may see in 
    # university), you must define all six methods, as __cmp__()
    # has been removed:
    # def __eq__(self, other):
    # 
    # def __ne__(self, other):
    # 
    # def __lt__(self, other):
    # 
    # def __gt__(self, other):
    # 
    # def __le__(self, other):
    # 
    # def __ge__(self, other):
    def __cmp__(self, other):
        if isinstance(other, Node):
            if self.data < other.data:
                return -1
            elif self.data > other.data:
                return 1
            return 0
        else:
            # This is equivalent to raising an exception in Java.
            # TypeErrors should be used when the error is due to
            # some argument or variable being the incorrect type,
            # while ValueErrors should be used for errors due
            # to obtaining an incorrect or error value.
            raise TypeError("Operand of invalid type")

class List:
    # Our List class will only store the location of the first
    # node. We could of course keep track of the tail as well,
    # but for this implementation we won't. In order to easily
    # create empty lists, we give the head argument a default
    # parameter of None.
    def __init__(self, head = None):
        self.head = head

    # Defining __add__() will allow us to append items to our
    # list like so:
    #   x = List()
    #   x + 2       # (2)
    #   x + Node(3) # (2) -> (3)
    # We will be able to add integers or Nodes.
    def __add__(self, other):
        # Like Scala, functions are first-class values in Python,
        # so we can have inner functions to make our code neater,
        # and to practice the DRY principle (Don't Repeat Yourself)
        def add_helper(other):
            # None is a special kind of object called a singleton,
            # meaning there is only ever one copy of it in memory
            # when Python is running. The safest way of comparing to
            # a singleton is using the 'is' keyword, and not '=='.
            if self.head is None:
                self.head = other
            else:
                tmp = self.head
                while tmp.next:
                    tmp = tmp.next
                tmp.next = other

        # isinstance() allows us to check the type of a variable.
        if isinstance(other, Node):
            add_helper(other)
        elif isinstance(other, int):
            add_helper(Node(other))
        else:
            raise TypeError("Operand of invalid type")

    # __radd__() will allow us to prepend items to our list like so,
    # assuming 'x' is the same list:
    #   1 + x       # (1) -> (2) -> (3)
    #   Node(0) + x # (0) -> (1) -> (2) -> (3)
    # Normally, those statements would try to call the __add__()
    # method on an integer, or Node, which either don't exist or
    # don't handle our custom class. However, we can define this
    # method to give us that ability. So, given the following
    # pattern of statements:
    #   operand1 op operand2
    # Python will first try:
    #   operand1.__op__(operand2)
    # If that fails, it will try:
    #   operand2.__rop__(operand1) # Notice the added r
    # If that also fails, the program will crash.
    def __radd__(self, other):
        def radd_helper(other):
            if self.head is None:
                self.head = other
            else:
                other.next = self.head
                self.head = other

        if isinstance(other, Node):
            radd_helper(other)
        elif isinstance(other, int):
            radd_helper(Node(other))
        else:
            raise TypeError("Operand of invalid type")

    # __sub__() will allow us to remove items from our list
    # by searching for the integer or Node, then removing it.
    # Only statements like the following make sense algebraically,
    # so we only define __sub__(), not __rsub__():
    #   x - 2 # (0) -> (1) -> (3)
    def __sub__(self, other):
        def sub_helper(other):
            if self.head is None:
                # Having an empty list means we have essentially
                # an error value, so we raise a ValueError
                raise ValueError("Empty list")
            elif self.search(other) is None:
                # Having a list without the node we're deleting
                # is also a problem...
                raise ValueError("Operand not found in list")
            elif self.head == other:
                self.head = self.head.next
            else:
                val = self.search(other)
                tmp = self.head
                while tmp.next != val:
                    tmp = tmp.next
                tmp.next = val.next

        if isinstance(other, Node):
            sub_helper(other)
        elif isinstance(other, int):
            sub_helper(Node(other))
        else:
            raise TypeError("Operand of invalid type")

    # __str__() is implemented in a functional style, to show
    # that Python is multi-paradigm just like Scala. We build
    # our string by recursively building our string and passing
    # it back to the recursive call until we reach the end of
    # the list.
    def __str__(self):
        if self.head is None: return "Empty list"
        else:
            def str_helper(list, list_str):
                if list.head is None: return list_str
                else:
                    tmp = list.head
                    return str_helper(List(tmp.next), list_str + str(tmp))
            return str_helper(self, "")

    # search() is implemented in a functional style, to show
    # that Python is multi-paradigm just like Scala. We search
    # the list by recursively checking the head of smaller 
    # sub-lists.
    def search(self, val):
        def search_helper(list, val):
            # We cannot combine our boolean expressions here,
            # since Python would be unable to tell if list.head 
            # is supposed to be a Node or None. If you don't
            # understand, try this if-statement instead and
            # explore the error you receive:
            # 
            # if list.head == val or list.head is None:
            #   return list.head
            # else:
            #   return search_helper(List(list.head.next), val)
            if list.head is None:
                return None
            elif list.head == val:
                return list.head
            else:
                return search_helper(List(list.head.next), val)

        if isinstance(val, Node):
            return search_helper(self, val)
        elif isinstance(val, int):
            return search_helper(self, Node(val))
        else:
            raise TypeError("Searching for incorrect type")

if __name__ == "__main__":
    q = List()
    print q
    q + 2
    print q
    q + Node(3)
    print q
    1 + q
    print q
    q - 2
    print q