CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Object-Oriented Programming (OOP), Part 1
Using Objects and Writing Classes


  1. Methods vs Functions
  2. Classes and Instances
  3. Objects and Object-Oriented Programming (OOP)
  4. Writing Classes
  5. Writing Constructors
  6. Writing Methods
  7. Advantages of Classes and Methods
  8. Objects and Aliases
  9. Example: Animations with OOP

  1. Methods vs Functions  
    We call methods using s.f() rather than f(s):
    s = 'This could be any string!'
    
    print(len(s))     # len is a function
    
    print(s.upper())  # upper is a string method, called using the . notation
                      # we say that we "call the method upper on the string s"
    
    print(s.replace('could', 'may')) # some methods take additional arguments

    See how we get different errors for improperly calling methods vs functions:
    n = 123
    print(len(n))    # TypeError: object of type 'int' has no len()
                     # This means that len() cannot work properly with int's
    
    n = 123
    print(n.upper()) # AttributeError: 'int' object has no attribute 'upper'
                     # This means that there is no method upper() for int's

  2. Classes and Instances  
    • Classes are also called "Types" in Python.
      • For example, these are classes: int, float, str, bool
    • Instances are values of a given class or type.
      • For example: 'abc' is a str instance (also called a string)

  3. Objects and Object-Oriented Programming (OOP)  
    • Every value in Python is an Object.
      • Every instance is an object, and its type is some class.
      • Every class is an object, too (its type is type!).
    • That is why we call this Object-Oriented Programming
      • We are using objects only a little bit now.
      • Soon we will write our own classes.
      • Then we will add some sophistication to how we write and use classes and objects.
      • Even so, because we are using objects now, we are already using Object-Oriented Programming (OOP).

  4. Writing Classes
    # Create our own class:
    class Dog:
        # a class must have a body, even if it does nothing, so we will
        # use 'pass' for now...
        pass
    
    # Create instances of our class:
    d1 = Dog()
    d2 = Dog()
    
    # Verify the type of these instances:
    print(type(d1))             # Dog (actually, class '__main__.Dog')
    print(isinstance(d2, Dog))  # True
    
    # Set and get properties (aka 'fields' or 'attributes') of these instances:
    d1.name = 'Dot'
    d1.age = 4
    d2.name = 'Elf'
    d2.age = 3
    print(d1.name, d1.age) # Dot 4
    print(d2.name, d2.age) # Elf 3

  5. Writing Constructors
    • Constructors let us pre-load our new instances with properties.
    • This lets us write code like so:
      d1 = Dog('fred', 4) # now d1 is a Dog instance with name 'fred' and age 4
      
    • We would like to write our constructor like this:
      def constructor(dog, name, age):
          # pre-load the dog instance with the given name and age:
          dog.name = name
          dog.age = age
      
    • Unfortunately, Python does not use 'constructor' as the constructor name. Instead, it uses '__init__' (sorry about that), like so:
      def __init__(dog, name, age):
          # pre-load the dog instance with the given name and age:
          dog.name = name
          dog.age = age
      
    • Also, unfortunately, while we could name the instance 'dog' like we did, standard convention requires that we name it 'self' (sorry again), like so:
      def __init__(self, name, age):
          # pre-load the dog instance with the given name and age:
          self.name = name
          self.age = age
      
    • Finally, we place this method inside the class and we have a constructor that we can use, like so:
      class Dog:
          def __init__(self, name, age):
              # pre-load the dog instance with the given name and age:
              self.name = name
              self.age = age
       
      # Create instances of our class, using our new constructor
      d1 = Dog('Dot', 4)
      d2 = Dog('Elf', 3)
      
      print(d1.name, d1.age) # Dot 4
      print(d2.name, d2.age) # Elf 3

  6. Writing Methods
    • Start with a function:
      class Dog:
          def __init__(self, name, age):
              self.name = name
              self.age = age
      
      # Here is a function we will turn into a method:
      def sayHi(dog):
          print(f'Hi, my name is {dog.name} and I am {dog.age} years old!')
      
      d1 = Dog('Dot', 4)
      d2 = Dog('Elf', 3)
      
      sayHi(d1) # Hi, my name is Dot and I am 4 years old!
      sayHi(d2) # Hi, my name is Elf and I am 3 years old!

    • Turn the function into a method, and the function call into a method call, like this:
      class Dog:
          def __init__(self, name, age):
              self.name = name
              self.age = age
      
          # Now it is a method (simply by indenting it inside the class!)
          def sayHi(dog):
              print(f'Hi, my name is {dog.name} and I am {dog.age} years old!')
      
      d1 = Dog('Dot', 4)
      d2 = Dog('Elf', 3)
      
      # Notice how we change the function calls into method calls:
      
      d1.sayHi() # Hi, my name is Dot and I am 4 years old!
      d2.sayHi() # Hi, my name is Elf and I am 3 years old!

    • Finally, use self, as convention requires:
      class Dog:
          def __init__(self, name, age):
              self.name = name
              self.age = age
      
          # Now we are using self, as convention requires:
          def sayHi(self):
              print(f'Hi, my name is {self.name} and I am {self.age} years old!')
      
      d1 = Dog('Dot', 4)
      d2 = Dog('Elf', 3)
      
      # Notice how we change the function calls into method calls:
      
      d1.sayHi() # Hi, my name is Dot and I am 4 years old!
      d2.sayHi() # Hi, my name is Elf and I am 3 years old!

    • Methods can take additional parameters, like so:
      class Dog:
          def __init__(self, name, age):
              self.name = name
              self.age = age
      
          # This method takes a second parameter -- times
          def bark(self, times):
              print(f'{self.name} says: {"woof!" * times}')
      
      d = Dog('Dot', 4)
      
      d.bark(1) # Dot says: woof!
      d.bark(4) # Dot says: woof!woof!woof!woof!

    • Methods can also set properties, like so:
      class Dog:
          def __init__(self, name, age):
              self.name = name
              self.age = age
              self.woofCount = 0   # we initialize the property in the constructor!
      
          def bark(self, times):
              # Then we can set and get the property in this method
              self.woofCount += times
              print(f'{self.name} says: {"woof!" * times} ({self.woofCount} woofs!)')
      
      d = Dog('Dot', 4)
      
      d.bark(1) # Dot says: woof! (1 woofs!)
      d.bark(4) # Dot says: woof!woof!woof!woof! (5 woofs!)

  7. Advantages of Classes and Methods
    • Encapsulation
      • Organizes code
        A class includes the data and methods for that class.
      • Promotes intuitive design
        Well-designed classes should be intuitive, so the data and methods in the class match commonsense expectations.
      • Restricts access
        • len is a function, so we can call len(True) (which crashes)
        • upper is a method on strings but not booleans, so we cannot even call True.upper()
    • Polymorphism
      Polymorphism: the same method name can run different code based on type, like so:
      class Dog:
          def speak(self):
              print('woof!')
      
      class Cat:
          def speak(self):
              print('meow!')
      
      for animal in [ Dog(), Cat() ]:
          animal.speak() # same method name, but one woofs and one meows!

  8. Objects and Aliases
    # Objects are mutable so aliases change!
    # Run this with the visualizer to make it clear!
    
    import copy
    
    class Dog:
        def __init__(self, name, age, breed):
            self.name = name
            self.age = age
            self.breed = breed
    
    dog1 = Dog('Dino', 10, 'shepherd')
    dog2 = dog1            # this is an alias
    dog3 = copy.copy(dog1) # this is a copy, not an alias
    
    dog1.name = 'Spot'
    print(dog2.name) # Spot (the alias changed, since it is the same object)
    print(dog3.name) # Dino (the copy did not change, since it is a different object)

  9. Example: Animations with OOP
    Here is an updated version of the adding-and-deleting dots demo from here. This version adds methods to the Dot class so each dot is responsible for its own drawing and mouse handling.
    from cmu_112_graphics import *
    import random
    
    class Dot:
        def __init__(self, cx, cy, r, counter, color):
            self.cx = cx
            self.cy = cy
            self.r = r
            self.counter = counter
            self.color = color
    
        def redraw(self, app, canvas):
            # Only redraw this dot
            canvas.create_oval(self.cx-self.r, self.cy-self.r,
                               self.cx+self.r, self.cy+self.r,
                               fill='white', outline=self.color, width=15)
            canvas.create_text(self.cx, self.cy, text=str(self.counter),
                               fill='black')
    
        def containsPoint(self, x, y):
            return (((self.cx - x)**2 + (self.cy - y)**2)**0.5 <= self.r)
    
        def mousePressed(self, event):
            # We are guaranteed (event.x, event.y) is in this dot
            self.counter += 1
            self.color = getRandomColor()
    
        def timerFired(self, app):
            self.counter += 1
    
    def appStarted(app):
        app.dots = [ ]
        app.timerDelay = 1000 # once per second
    
    def getRandomColor():
        colors = ['red', 'orange', 'yellow', 'green', 'blue', 'pink',
                  'lightGreen', 'gold', 'magenta', 'maroon', 'salmon',
                  'cyan', 'brown', 'orchid', 'purple']
        return random.choice(colors)
    
    def mousePressed(app, event):
        # go through dots in reverse order so that
        # we find the topmost dot that intersects
        for dot in reversed(app.dots):
            if dot.containsPoint(event.x, event.y):
                dot.mousePressed(event)
                return
        # mouse click was not in any dot, so create a new dot
        newDot = Dot(cx=event.x, cy=event.y, r=20, counter=0, color='cyan')
        app.dots.append(newDot)
    
    def keyPressed(app, event):
        if (event.key == 'd'):
            if (len(app.dots) > 0):
                app.dots.pop(0)
            else:
                print('No more dots to delete!')
    
    def timerFired(app):
        for dot in app.dots:
            dot.timerFired(app)
    
    def redrawAll(app, canvas):
        for dot in app.dots:
            dot.redraw(app, canvas)
    
        # draw the text
        canvas.create_text(app.width/2, 20,
                           text='Example: Adding and Deleting Shapes',
                           fill='black')
        canvas.create_text(app.width/2, 40,
                           text='Mouse clicks outside dots create new dots',
                           fill='black')
        canvas.create_text(app.width/2, 60,
                           text='Mouse clicks inside dots increase their counter',
                           fill='black')
        canvas.create_text(app.width/2, 70,
                           text='and randomize their color.', fill='black')
        canvas.create_text(app.width/2, 90,
                           text='Pressing "d" deletes circles', fill='black')
    
    runApp(width=400, height=400)