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


  1. Objects and Classes
  2. Constructor (__init__)
  3. Type Testing (type, isinstance)
  4. Generic Class Methods
    1. Equality Testing (__eq__)
    2. Converting to Strings (__str__ and __repr__)
    3. Using in Sets and Dictionaries (__hash__ and __eq__)
    4. Fraction Example
  5. Class-Level Features
    1. Class Attributes
    2. Static Methods
    3. Playing Card Demo
  6. Inheritance
    1. Specifying a Superclass
    2. Overriding methods
    3. isinstance vs type in inherited classes
    4. Monster Demo
  7. 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. Constructor (__init__)
    class A(object): def __init__(self, color, isHappy): self.color = color self.isHappy = isHappy def isBlue(self): return self.color == "blue" a1 = A('yellow', True) a2 = A('blue', False) print(a1.color, a1.isHappy, a1.isBlue()) print(a2.color, a2.isHappy, a2.isBlue())

  3. 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

  4. Generic Class Methods
    1. 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!)

    2. 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)]

    3. Using in Sets and Dictionaries (__hash__ and __eq__)
      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. # CAVEAT: a proper hash function should only test values that will not change! 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!

    4. 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 __hash__(self): return hash((self.num, self.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)) s = set() assert(Fraction(1, 2) not in s) s.add(Fraction(1, 2)) assert(Fraction(1, 2) in s) s.remove(Fraction(1, 2)) assert(Fraction(1, 2) not in s) print('Passed.') if (__name__ == '__main__'): testFractionClass()

  5. Class-Level Features
    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 getHashables(self): return (self.number, self.suit) # return a tuple of hashables def __hash__(self): return hash(self.getHashables()) 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 = set() 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.add(drawnCard) # And then report how many cards we drew print("Total cards drawn:", 1+len(cardsSeen)) print("Total diamonds drawn:", diamondsCount)

  6. 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. 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

  7. Additional Reading
    For more on these topics, and many additional OOP-related topics, check the following links:
         https://docs.python.org/3/tutorial/classes.html
         https://docs.python.org/3/reference/datamodel.html