# CMU 15-112: Fundamentals of Programming and Computer Science Class Notes: Object-Oriented Programming (OOP), Part 3 Special Methods + Inheritance + More

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

2. Special Methods
1. Equality Testing (__eq__)
The problem: Shouldn't `a1 == a2`?
class A(object): def __init__(self, x): self.x = x a1 = A(5) a2 = A(5) print(a1 == a2) # False!

The partial solution: __eq__
The __eq__ method tells Python how to evalute the equality of two objects.
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:
Here we don't crash on unexpected types of `other`:
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:
Just like with ==, Python doesn't really know how to print our objects...
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__
The `__str__` method tells Python how to convert the object to a string, but it is not used in some cases (such as when the object is in a list):
class A(object): def __init__(self, x): self.x = x def __str__(self): return f'A(x={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__
The `__repr__` method is used inside lists (and other places):
# 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 f'A(x={self.x})' a = A(5) print(a) # prints A(x=5) (better) print([a]) # [A(x=5)]

3. Class-Level Features
1. Class Attributes
Class Attributes are values specified in a class that are shared by all instances of that class! We can access class attributes from any instance of that class, but changing those values anywhere changes them for every instance.
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
Static Methods in a class can be called directly without necessarily making and/or referencing a specific object.
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 (except __hash__ method)
You are not responsible for the __hash__ method in the following example:
# 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): number = PlayingCard.numberNames[self.number] suit = PlayingCard.suitNames[self.suit] return f'<{number} of {suit}>' def getHashables(self): return (self.number, self.suit) # return a tuple of hashables def __hash__(self): # you are not responsible for this method 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 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)

4. Inheritance
A subclass inherits all the methods from its superclass, and then can add or modify methods.
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
We can change a method's behavior in a subclass by overriding it.
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 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 class NecroMonster(Monster): def attack(self): # NecroMonsters can attack even when 'killed' return self.strength