- 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
- 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
- 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)
- 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 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.
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!
- 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()
- 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)
- 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)
- 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.
- 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
- Additional Reading
For more on these topics, and many additional OOP-related topics, check here:
https://docs.python.org/3/reference/datamodel.html