CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Object-Oriented Programming (OOP)


  1. Objects and Classes
  2. Type Testing (type, isinstance)
  3. Constructor (__init__)
  4. Equality Testing (__eq__)
  5. Converting to Strings (__str__ and __repr__)
  6. Fraction Example
  7. Inheritance
    1. Specifying a Superclass
    2. Overriding methods
    3. isinstance vs type in inherited classes
    4. Multiple Inheritance
  8. Monster Demo
  9. Optional Topics
    1. Class Attributes
    2. Static Methods
    3. Playing Card Demo
    4. Using in Sets and Dictionaries (__hash__ and __eq__)
  10. Additional Reading

  1. Objects and Classes
    In programming, an object is a data structure that has user-defined properties and methods associated with it. Its properties are features of the object; for example, a property of a Dog object might be its breed. These are implemented as variables. Its methods are things the object can do; for example, a method of a Dog might be a function called speak() which prints out "Bark!". These are implemented as functions.

    Objects are defined using a class, which can be thought of as a template for a generic object. Once the class has been written, individual objects can be created using this template. These individual objects are called instances.

    class Dog(object): # define properties and methods of a generic dog here pass fido = Dog() # fido is now a specific instance of the class

  2. Type Testing (type, isinstance)
    class A(object): pass a = A() print(type(a)) # A (technically, < class '__main__.A' >) print(type(a) == A) # True print(isinstance(a, A)) # True

  3. Constructor (__init__)
    class A(object): def __init__(self, color, isHappy): self.color = color self.isHappy = isHappy a1 = A('yellow', True) a2 = A('blue', False) print(a1.color, a1.isHappy) print(a2.color, a2.isHappy)

  4. Equality Testing (__eq__)
    The problem:
    class A(object): def __init__(self, x): self.x = x a1 = A(5) a2 = A(5) print(a1 == a2) # False!

    The partial solution: __eq__
    class A(object): def __init__(self, x): self.x = x def __eq__(self, other): return (self.x == other.x) a1 = A(5) a2 = A(5) print(a1 == a2) # True print(a1 == 99) # crash (darn!)

    A better solution:
    class A(object): def __init__(self, x): self.x = x def __eq__(self, other): return (isinstance(other, A) and (self.x == other.x)) a1 = A(5) a2 = A(5) print(a1 == a2) # True print(a1 == 99) # False (huzzah!)

  5. Converting to Strings (__str__ and __repr__)
    The problem:
    class A(object): def __init__(self, x): self.x = x a = A(5) print(a) # prints <__main__.A object at 0x102916128> (yuck!)

    The partial solution: __str__
    class A(object): def __init__(self, x): self.x = x def __str__(self): return "A(x=%d)" % self.x a = A(5) print(a) # prints A(x=5) (better) print([a]) # prints [<__main__.A object at 0x102136278>] (yuck!)

    The better solution: __repr__
    # Note: repr should be a computer-readable form so that # (eval(repr(obj)) == obj), but we are not using it that way. # So this is a simplified use of repr. class A(object): def __init__(self, x): self.x = x def __repr__(self): return "A(x=%d)" % self.x a = A(5) print(a) # prints A(x=5) (better) print([a]) # [A(x=5)]


  6. Fraction Example
    # Very simple, far-from-fully implemented Fraction class # to demonstrate the OOP ideas from above. # Note that Python actually has a full Fraction class that # you would use instead (from fractions import Fraction), # so this is purely for demonstrational purposes. def gcd(x, y): if (y == 0): return x else: return gcd(y, x%y) class Fraction(object): def __init__(self, num, den): # Partial implementation -- does not deal with 0 or negatives, etc g = gcd(num, den) self.num = num // g self.den = den // g def __repr__(self): return '%d/%d' % (self.num, self.den) def __eq__(self, other): return (isinstance(other, Fraction) and ((self.num == other.num) and (self.den == other.den))) def times(self, other): if (isinstance(other, int)): return Fraction(self.num * other, self.den) else: return Fraction(self.num * other.num, self.den * other.den) def testFractionClass(): print('Testing Fraction class...', end='') assert(str(Fraction(2, 3)) == '2/3') assert(str([Fraction(2, 3)]) == '[2/3]') assert(Fraction(2,3) == Fraction(2,3)) assert(Fraction(2,3) != Fraction(2,5)) assert(Fraction(2,3) != "Don't crash here!") assert(Fraction(2,3).times(Fraction(3,4)) == Fraction(1,2)) assert(Fraction(2,3).times(5) == Fraction(10,3)) print('Passed.') if (__name__ == '__main__'): testFractionClass()

  7. Inheritance
    1. Specifying a Superclass
      class A(object): def __init__(self, x): self.x = x def f(self): return 10*self.x class B(A): def g(self): return 1000*self.x print(A(5).f()) # 50 print(B(7).g()) # 7000 print(B(7).f()) # 70 (class B inherits the method f from class A) print(A(5).g()) # crashes (class A does not have a method g)

    2. Overriding methods
      class A(object): def __init__(self, x): self.x = x def f(self): return 10*self.x def g(self): return 100*self.x class B(A): def __init__(self, x=42, y=99): super().__init__(x) # call overridden init! self.y = y def f(self): return 1000*self.x def g(self): return (super().g(), self.y) a = A(5) b = B(7) print(a.f()) # 50 print(a.g()) # 500 print(b.f()) # 7000 print(b.g()) # (700, 99)

    3. isinstance vs type in inherited classes
      class A(object): pass class B(A): pass a = A() b = B() print(type(a) == A) # True print(type(b) == A) # False print(type(a) == B) # False print(type(b) == B) # True print() print(isinstance(a, A)) # True print(isinstance(b, A)) # True (surprised?) print(isinstance(a, B)) # False print(isinstance(b, B)) # True

    4. Multiple Inheritance
      Note: Python supports multiple inheritance, where a class may have more than one superclass. We will not cover that in 15-112.

  8. Monster Demo
    # This is our base class class Monster(object): def __init__(self, strength, defense): self.strength = strength self.defense = defense self.health = 10 def attack(self): # returns damage to be dealt if self.health > 0: return self.strength def defend(self, damage): # does damage to self self.health -= damage # In this class, we'll partially overwrite the init method, and make a new, class-specific method class MagicMonster(Monster): def __init__(self, strength, defense): super().__init__(strength, defense) # most properties are the same self.health = 5 # but they start out weaker def heal(self): # only magic monsters can heal themselves! if 0 < self.health < 5: self.health += 1 # In this class, we'll overwrite a specific method class NecroMonster(Monster): def attack(self): # NecroMonsters can attack even when 'killed' return self.strength

  9. Optional Topics

    1. Class Attributes
      class A(object): dirs = ["up", "down", "left", "right"] # typically access class attributes directly via the class (no instance!) print(A.dirs) # ['up', 'down', 'left', 'right'] # can also access via an instance: a = A() print(a.dirs) # but there is only one shared value across all instances: a1 = A() a1.dirs.pop() # not a good idea a2 = A() print(a2.dirs) # ['up', 'down', 'left'] ('right' is gone from A.dirs)

    2. Static Methods
      class A(object): @staticmethod def f(x): return 10*x print(A.f(42)) # 420 (called A.f without creating an instance of A)

    3. Playing Card Demo
      # oopy-playing-cards-demo.py # Demos class attributes, static methods, repr, eq, hash import random class PlayingCard(object): numberNames = [None, "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King"] suitNames = ["Clubs", "Diamonds", "Hearts", "Spades"] CLUBS = 0 DIAMONDS = 1 HEARTS = 2 SPADES = 3 @staticmethod def getDeck(shuffled=True): deck = [ ] for number in range(1, 14): for suit in range(4): deck.append(PlayingCard(number, suit)) if (shuffled): random.shuffle(deck) return deck def __init__(self, number, suit): # number is 1 for Ace, 2...10, # 11 for Jack, 12 for Queen, 13 for King # suit is 0 for Clubs, 1 for Diamonds, # 2 for Hearts, 3 for Spades self.number = number self.suit = suit def __repr__(self): return ("<%s of %s>" % (PlayingCard.numberNames[self.number], PlayingCard.suitNames[self.suit])) def __eq__(self, other): return (isinstance(other, PlayingCard) and (self.number == other.number) and (self.suit == other.suit)) # Show this code in action print("Demo of PlayingCard will keep creating new decks, and") print("drawing the first card, until we see the same card twice.") print() cardsSeen = [] diamondsCount = 0 # Now keep drawing cards until we get a duplicate while True: deck = PlayingCard.getDeck() drawnCard = deck[0] if (drawnCard.suit == PlayingCard.DIAMONDS): diamondsCount += 1 print(" drawnCard:", drawnCard) if (drawnCard in cardsSeen): break cardsSeen.append(drawnCard) # And then report how many cards we drew print("Total cards drawn:", 1+len(cardsSeen)) print("Total diamonds drawn:", diamondsCount)

    4. Using in Sets and Dictionaries (__hash__ and __eq__) (Optional!)
      The problem:
      class A(object): def __init__(self, x): self.x = x s = set() s.add(A(5)) print(A(5) in s) # False d = dict() d[A(5)] = 42 print(d[A(5)]) # crashes

      The solution: __hash__ and __eq__
      class A(object): def __init__(self, x): self.x = x def __hash__(self): return hash(self.x) def __eq__(self, other): return (isinstance(other, A) and (self.x == other.x)) s = set() s.add(A(5)) print(A(5) in s) # True (whew!) d = dict() d[A(5)] = 42 print(d[A(5)]) # works!

      A better (more generalizable) solution
      # Your getHashables method should return the values upon which # your hash method depends, that is, the values that your __eq__ # method requires to test for equality. class A(object): def __init__(self, x): self.x = x def getHashables(self): return (self.x, ) # return a tuple of hashables def __hash__(self): return hash(self.getHashables()) def __eq__(self, other): return (isinstance(other, A) and (self.x == other.x)) s = set() s.add(A(5)) print(A(5) in s) # True (still works!) d = dict() d[A(5)] = 42 print(d[A(5)]) # works!

  10. Additional Reading
    For more on these topics, and many additional OOP-related topics, check here:
          https://docs.python.org/3/reference/datamodel.html