CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Object-Oriented Programming (OOP) (part2)
- Optional Reading
- Type Testing (type, isinstance)
- Equality Testing (__eq__)
- Converting to Strings (__str__ and __repr__)
- Using in Sets (__hash__ and __eq__)
- Using in Dictionaries (__hash__ and __eq__)
- Inheritance
- Class Attributes
- Static Methods
- Direct access to __dict__ (for instances and classes)
- Worked Examples Using OOP
- Optional Reading
For more on these topics, and many additional OOP-related topics, check here:
https://docs.python.org/3/reference/datamodel.html - 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 - 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!) - 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)] - Using in Sets (__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
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!)
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!) - Using in Dictionaries (__hash__ and __eq__)
The problem (same as sets):class A(object): def __init__(self, x): self.x = x d = dict() d[A(5)] = 42 print(d[A(5)]) # crashes
The solution (same as sets):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)) d = dict() d[A(5)] = 42 print(d[A(5)]) # works! - Inheritance
- 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)- 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)- 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- Multiple Inheritance
Note: Python supports multiple inheritance, where a class may have more than one superclass. We will not cover that in 15-112.- 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)- 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)- Direct access to __dict__ (for instances and classes)
class A(object): x = 42 def __init__(self, y): self.y = y a = A(99) print(a.__dict__) # {'y': 99} print(A.__dict__) # {'x': 42, ... } a.__dict__['y'] = 100 print(a.y) # 100- Worked Examples Using OOP
- oopy-dots-demo
# oopyDotsDemo.py # starts with betterDotsDemo and adds: # * a dotCounter that counts all the instances of Dot or its subclasses # * a MovingDot subclass of Dot that scrolls horizontally # * a FlashingMovingDot subclass of MovingDot that flashes and moves import random from tkinter import * class Dot(object): dotCount = 0 def __init__(self, x, y): Dot.dotCount += 1 self.x = x self.y = y self.r = random.randint(20,50) self.fill = random.choice(["pink","orange","yellow","green", "cyan","purple"]) self.clickCount = 0 def containsPoint(self, x, y): d = ((self.x - x)**2 + (self.y - y)**2)**0.5 return (d <= self.r) def draw(self, canvas): canvas.create_oval(self.x-self.r, self.y-self.r, self.x+self.r, self.y+self.r, fill=self.fill) canvas.create_text(self.x, self.y, text=str(self.clickCount)) def onTimerFired(self, data): pass class MovingDot(Dot): def __init__(self, x, y): super().__init__(x, y) self.speed = 5 # default initial speed def onTimerFired(self, data): self.x += self.speed if (self.x > data.width): self.x = 0 class FlashingMovingDot(MovingDot): def __init__(self, x, y): super().__init__(x, y) self.flashCounter = 0 self.showFlash = True def onTimerFired(self, data): super().onTimerFired(data) self.flashCounter += 1 if (self.flashCounter == 5): self.flashCounter = 0 self.showFlash = not self.showFlash def draw(self, canvas): if (self.showFlash): canvas.create_rectangle(self.x-self.r, self.y-self.r, self.x+self.r, self.y+self.r, fill="lightGray") super().draw(canvas) def init(data): data.dots = [ ] def mousePressed(event, data): for dot in reversed(data.dots): if (dot.containsPoint(event.x, event.y)): dot.clickCount += 1 return dotType = (len(data.dots) % 3) if (dotType == 0): data.dots.append(Dot(event.x, event.y)) elif (dotType == 1): data.dots.append(MovingDot(event.x, event.y)) else: data.dots.append(FlashingMovingDot(event.x, event.y)) def redrawAll(canvas, data): for dot in data.dots: dot.draw(canvas) canvas.create_text(data.width/2, 10, text="%d Dots" % Dot.dotCount) def keyPressed(event, data): pass def timerFired(data): for dot in data.dots: dot.onTimerFired(data) #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds init(data) # create the root and the canvas root = Tk() canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(400, 200) - oopy-playing-cards-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)
- Overriding methods
- Specifying a Superclass