Discussion 6: OOP

This is an online worksheet that you can work on during discussions. Your work is not graded and you do not need to submit anything.

OOP

Object-oriented programming (OOP) is a programming paradigm that allows us to treat data as objects, like we do in real life.

For example, consider the class Student. Each of you as individuals is an instance of this class.

Details that all CS 61A students have, such as name, are called instance variables. Every student has these variables, but their values differ from student to student. A variable that is shared among all instances of Student is known as a class variable. For example, the extension_days attribute is a class variable as it is a property of all students.

All students are able to do homework, attend lecture, and go to office hours. When functions belong to a specific object, they are called methods. In this case, these actions would be methods of Student objects.

Here is a recap of what we discussed above:

  • class: a template for creating objects
  • instance: a single object created from a class
  • instance variable: a data attribute of an object, specific to an instance
  • class variable: a data attribute of an object, shared by all instances of a class
  • method: a bound function that may be called on all instances of a class

Instance variables, class variables, and methods are all considered attributes of an object.

Q1: WWPD: Student OOP

Below we have defined the classes Professor and Student, implementing some of what was described above. Remember that Python passes the self argument implicitly to methods when calling the method directly on an object.

class Student:

    extension_days = 3 # this is a class variable

    def __init__(self, name, staff):
        self.name = name # this is an instance variable
        self.understanding = 0
        staff.add_student(self)
        print("Added", self.name)

    def visit_office_hours(self, staff):
        staff.assist(self)
        print("Thanks, " + staff.name)

class Professor:

    def __init__(self, name):
        self.name = name
        self.students = {}

    def add_student(self, student):
        self.students[student.name] = student

    def assist(self, student):
        student.understanding += 1

    def grant_more_extension_days(self, student, days):
        student.extension_days = days

What will the following lines output?

>>> callahan = Professor("Callahan")
>>> elle = Student("Elle", callahan)
>>> elle.visit_office_hours(callahan)
>>> elle.visit_office_hours(Professor("Paulette"))
>>> elle.understanding
>>> [name for name in callahan.students]
>>> x = Student("Vivian", Professor("Stromwell")).name
>>> x
>>> [name for name in callahan.students]
>>> elle.extension_days
>>> callahan.grant_more_extension_days(elle, 7)
>>> elle.extension_days
>>> Student.extension_days

Q2: Email

We would like to write three different classes (Server, Client, and Email) to simulate a system for sending and receiving emails. A Server has a dictionary mapping client names to Client objects, and can both send Emails to Clients in the Server and register new Clients. A Client can both compose emails (which first creates a new Email object and then sends it to the recipient client through the server) and receive an email (which places an email into the client's inbox).

Emails will only be sent/received within the same server, so clients will always use the server they're registered in to send emails to other clients that are registered in the same rerver.

An example flow: A Client object (Client 1) composes an Email object with message "hello" with recipient Client 2, which the Server routes to Client 2's inbox.

Email example

To solve this problem, we'll split the section into two halves (students on the left and students on the right):

  • Everyone will implement the Email class together
  • The first half (left) will implement the Server class
  • The other half (right) will implement the Client class

Fill in the definitions below to finish the implementation!

Run in 61A Code
Run in 61A Code
Run in 61A Code

Q3: Keyboard

We'd like to create a Keyboard class that takes in an arbitrary number of Buttons and stores these Buttons in a dictionary. The keys in the dictionary will be ints that represent the position on the Keyboard, and the values will be the respective Button. Fill out the methods in the Keyboard class according to each description, using the doctests as a reference for the behavior of a Keyboard.

Hint: You can iterate over *args as if it were a list.

Run in 61A Code

Q4: Smart Fridge

The SmartFridge class is used by smart refrigerators to track which items are in the fridge and let owners know when an item has run out.

The class internally uses a dictionary to store items, where each key is the item name and the value is the current quantity. The add_item method should add the given quantity of the given item and report the current quantity. You can assume that the use_item method will only be called on items that are already in the fridge, and it should use up the given quantity of the given item. If the quantity would fall to or below zero, it should only use up to the remaining quantity, and remind the owner to buy more of that item.

Finish implementing the SmartFridge class definition so that its add_item and use_item methods work as specified by the doctests.

You may find Python's formatted string literals, or f-strings useful. A quick example:

>>> feeling = 'love'
>>> course = '61A!'
>>> f'I {feeling} {course}'
'I love 61A!'

If you're curious about alternate methods of string formatting, you can also check out an older method of Python string formatting. A quick example:

>>> ten, twenty, thirty = 10, 'twenty', [30]
>>> '{0} plus {1} is {2}'.format(ten, twenty, thirty)
'10 plus twenty is [30]'
Run in 61A Code

Inheritance

To avoid redefining attributes and methods for similar classes, we can write a single base class from which the similar classes inherit. For example, we can write a class called Pet and define Dog as a subclass of Pet:

class Pet:

    def __init__(self, name, owner):
        self.is_alive = True    # It's alive!!!
        self.name = name
        self.owner = owner

    def eat(self, thing):
        print(self.name + " ate a " + str(thing) + "!")

    def talk(self):
        print(self.name)

class Dog(Pet):

    def talk(self):
        super().talk()
        print('This Dog says woof!')

Inheritance represents a hierarchical relationship between two or more classes where one class is a more specific version of the other: a dog is a pet (We use is a to describe this sort of relationship in OOP languages, and not to refer to the Python is operator).

Since Dog inherits from Pet, the Dog class will also inherit the Pet class's methods, so we don't have to redefine __init__ or eat. We do want each Dog to talk in a Dog-specific way, so we can override the talk method.

We can use super() to refer to the superclass of self, and access any superclass methods as if we were an instance of the superclass. For example, super().talk() in the Dog class will call the talk() method from the Pet class, but passing the Dog instance as the self.

This is a little bit of a simplification, and if you're interested you can read more in the Python documentation on super.

Q5: That's inheritance, init?

Let's say we want to create a class Monarch that inherits from another class, Butterfly. We've partially written an __init__ method for Monarch. For each of the following options, state whether it would correctly complete the method so that every instance of Monarch has all of the instance attributes of a Butterfly instance. You may assume that a monarch butterfly has the default value of 2 wings.

class Butterfly():
    def __init__(self, wings=2):
        self.wings = wings

class Monarch(Butterfly):
    def __init__(self):
        _________________________________________
        self.colors = ['orange', 'black', 'white']
super.__init__()
super().__init__()
Butterfly.__init__()
Butterfly.__init__(self)

Some butterflies like the Owl Butterfly have adaptations that allow them to mimic other animals with their wing patterns. Let's write a class for these MimicButterflies. In addition to all of the instance variables of a regular Butterfly instance, these should also have an instance variable mimic_animal describing the name of the animal they mimic. Fill in the blanks in the lines below to create this class.

class MimicButterfly(______________):
    def __init__(self, mimic_animal):
        _______________.__init__()
        ______________ = mimic_animal

What expression completes the first blank?

What expression completes the second blank?

What expression completes the third blank?

Q6: Cat

Below is a skeleton for the Cat class, which inherits from the Pet class we saw in the Inheritance introduction. To complete the implementation, override the __init__ and talk methods and add a new lose_life method.

Hint: You can call the __init__ method of Pet (the superclass of Cat) to set a cat's name and owner.

Hint: The __init__ method is not a real constructor, and can be called like any other method.

Run in 61A Code

Q7: NoisyCat

More cats! Fill in this implementation of a class called NoisyCat, which is just like a normal Cat. However, NoisyCat talks a lot: in fact, it talks twice as much as a regular Cat! If you'd like to test your code, feel free to copy over your solution to the Cat class above.

Run in 61A Code