Object Oriented Programming

As we know from IC211, the four pillars of OOP are: Encapsemation, Data hiding, Inheritance, Polymorphism. These are organization goals, not really mechanisms. Different languages provide different mechanisms for achieving this. In the world of Java: "encapsulation" means classes, "data hiding" means private/protected/public annotations, Inheritance means extends some class or implements some interface, and polymorphism means overriding methods that are defined in super classes or implementing methods required by some interface. So what mechanisms does Python have to support the four pillars?
  1. Encapsulation - Python has a class construct for bringing together different pieces of data into a single package. Note: Python terminology is to use "attribute" for what would be called a field or method in Java.
  2. Data hiding - Python has a convention that attributes with names beginning with __ (that is a double underscore, BTW) shouldn't be used by code outside the class ... at least not unless you know what you know what you are doing.
  3. Inheritance - Python has inheritance. In fact, it even allows multiple inheritance. However, it doesn't play as important a role as in Java, as we will see later.
  4. Polymorphism - Python's dynamic "duck" typing basically mean that when you call a method you always get the method that belongs to the type of the object, not the type of the reference, since references don't have a type. So the concept of polymorphism as part of OOP doesn't really apply to Python.

Static fields and methods (not all that useful, but ...)

Recall from IC211: static fields and methods are fields and methods that belong to the whole class, rather than being specific to each instance of the class. Static fields and methods are not really that common - we use instance fields and instance methods more frequently. But they make a nice starting point since the syntax is so easy.

The example below shows a class Pos in Java with a static field sep and static method disp. In OOP generally, we often refer to these as "class variables" and "class methods".

Below we do the same thing in Python, i.e. we declare a class Pos with class variable sep and class method disp. Note that they Python syntax for a class is
class Foo:
    field/method 1	
    field/method 2
    ...      

Note: there are no access specifiers, i.e. no "public", "private" or "protected". Python does not have these!

Note: the proper Python terminology is not "field" but rather "attribute". So we are really talking about "class attributes" rather than the Java terminology "static fields".

Instances of class Pos - constructors, instance fields and instance methods

More typically, we have instance fields and instance methods. So let's actually make our Java Pos class represent positions on a 2D grid with our usual (row,col) approach. We will:
  1. add instance fields row and col
  2. add a constructor to allow us to create Pos objects with initialized row/col values
  3. add an instance method len() that we can call like p1.len()
  4. add a toString() instance method. Note: overriding toString has special significance, since Java (a) toString() is a method of Object, so it exits for every class object, and (b) Java uses that method automatically in, for example, calls to println.

Now we'll do the exact same thing in the Python version.

  1. add instance attributes row/col and constructor for Pos objects
    Important1: a constructor for a class is a method named __init__(self,...).
    Important2: instance attributes (aka "fields") are created by assigning a value to a variable self.varname inside a constructor. There is no "declaration" of fields as there is in Java.
  2. add an instance method len() that we can call like p1.len()
    Important1: An instance method in Python is a method whose first argument is self, which is why we define len as: def len(self):
  3. Add a special method so when variable p1 is a Pos, calling print(p1) works like you would expect. Similar to Java, we need to add an instance method with a special name, in this case the name is __str__(self)
Important! The syntax for creating a new instance of a class is just like Java without the keyword new. So we create a new Pos object like p1 = Pos(3,4)

TODO
  1. Create a copy of pos.py in your VM and run it like the example above.
  2. You can see that Python does special things with methods named like __foo__. So far we have __init__(self,...) for constructors, and __str__(self) for when the language wants to convert an object to a string. However from the line
    >>> p1
    <__main__.Pos object at 0x7334ad7d2650>
    we see that the interpreter does not use __str__ to display values of type Pos in the interpreter. Instead the interpreter relies on an instance method named __repr__(self) for that. So add a definition of __repr__(self) and verify that evaluating an object like p1 in the interpreter displays the Pos object in a nice way, like (3,4). For example:
    >>> p1 = Pos(3,4)
    >>> p1
    (3,4) 
  3. Add an instance method step(dir) that changes the Pos's row/col coordinates according to dir, which is one of: "N", "S", "E", "W". For example:
    >>> p1 = Pos(5,3) # row 5, col 3
    >>> p1.step("S")  # to row 6, col 3
    >>> p1.step("W")  # to row 6, col 2
    >>> p1
    (6:2) 
    Note: This can be done in a really nice data-driven kind of way by defining a map in which the keys are the four directions, and the values are tuples defining the changes in row and col.
  4. Right now, == doesn't know how to check to Pos objects for equality:
    >>> p1 = Pos(3,4)
    >>> p2 = Pos(3,4)
    >>> p1 == p2
    False
    Once again, we can tell Python how to "do" == for Pos objects by overloading an underscore-named method: in this case the method __eq__(self,other), where "other" is a second Pos object. Overload this so == takes two Pos's to be equal if their row/col positions are equal. [BTW: the name "other" isn't special ... it's just suggestive.] With this addition, the following should work:
    $ python3 -i pos.py 
    >>> p1 = Pos(3,4)
    >>> p2 = Pos(3,4)
    >>> p1.__eq__(p2)
    True
    >>> p1 == p2
    True 

Inheritance

In Java, we used inheritance for three reasons: 1) adding functionality / modifying behavior, 2) expressing commonality and 3) polymorphism. Let's review what this means and see how it applies to Python: The moral of the story is ... we just use inheritance to add / modify functionality. So, let's use inheritance to create a "labeled position" class:

TODO
  1. Add a getLabel instance method that returns the instance's label:
  2. Add static method labMerge(A) that takes a list of LabPos objects and returns the string formed by concatenating together all of the lables (in order).
    $ python3 -i pos.py 
    >>> T = [ LabPos(3,4,"A"), LabPos(9,2,"H"), LabPos(1,1,"D") ]
    >>> LabPos.labMerge(T)
    'AHD' 
    This is really easy with list comprehensions and joins (e.g. "".join(["x","y","z"]) ).
  3. Add a static method dist that takes two LabPos objects and returns the "distance", which is the absolute value of the difference in their rows plus the absolute value of the difference in their cols.
    >>> p1 = LabPos(11,2,"J")
    >>> p2 = LabPos(5,6,"K")
    >>> LabPos.dist(p1,p2)
    10 
  4. Continuing with the above, explain why the following works:
    >>> p1 = Pos(12,5)
    >>> p2 = Pos(9,4)
    >>> LabPos.dist(p1,p2)
    4 

Christopher W Brown