Lab 6: Object-Oriented Programming and Inheritance

Due at 11:59pm on 03/04/2015.

Starter Files

Download lab06.zip. Inside the archive, you will find starter files for the questions in this lab, along with a copy of the OK autograder.

Submission

By the end of this lab, you should have submitted the lab with python3 ok --submit. You may submit more than once before the deadline; only the final submission will be graded.

Table of Contents

Object-Oriented Programming

Introduction

Object-oriented programming (OOP) is a style of programming that allows you to think of code in terms of "objects." Here's an example of a Car class:

class Car(object):
    num_wheels = 4

    def __init__(self, color):
        self.wheels = Car.num_wheels
        self.color = color

    def drive(self):
        if self.wheels <= Car.num_wheels:
            return self.color + ' car cannot drive!'
        return self.color + ' car goes vroom!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

Here's some terminology:

Types of variables

When dealing with OOP, there are three types of variables you should be aware of:

Question 1

Predict the result of evaluating the following calls in the interpreter. Then try them out yourself!

>>> class Account(object):
...     interest = 0.02
...     def __init__(self, account_holder):
...         self.balance = 0
...         self.holder = account_holder
...     def deposit(self, amount):
...         self.balance = self.balance + amount
...         print("Yes!")
...
>>> a = Account("Billy")
>>> a.account_holder
______
AttributeError: 'Account' object has no attribute 'account_holder'
>>> a.holder
______
'Billy'
>>> Account.holder
______
AttributeError: type object 'Account' has no attribute 'holder'
>>> Account.interest
______
0.02
>>> a.interest
______
0.02
>>> Account.interest = 0.03 >>> a.interest
______
0.03
>>> a.deposit(1000)
______
Yes!
>>> a.balance
______
1000
>>> a.interest = 9001 >>> Account.interest
______
0.03

Question 2

Modify the following Person class to add a repeat method, which repeats the last thing said. See the doctests for an example of its use.

Hint: you will have to modify other methods as well, not just the repeat method.

class Person(object):
    """Person class.

    >>> steven = Person("Steven")
    >>> steven.repeat()       # starts at whatever value you'd like
    'I squirreled it away before it could catch on fire.'
    >>> steven.say("Hello")
    'Hello'
    >>> steven.repeat()
    'Hello'
    >>> steven.greet()
    'Hello, my name is Steven'
    >>> steven.repeat()
    'Hello, my name is Steven'
    >>> steven.ask("preserve abstraction barriers")
    'Would you please preserve abstraction barriers'
    >>> steven.repeat()
    'Would you please preserve abstraction barriers'
    """
    def __init__(self, name):
        self.name = name
"*** YOUR CODE HERE ***"
self.previous = "I squirreled it away before it could catch on fire."
def say(self, stuff):
"*** YOUR CODE HERE ***"
self.previous = stuff
return stuff def ask(self, stuff): return self.say("Would you please " + stuff) def greet(self): return self.say("Hello, my name is " + self.name) def repeat(self):
"*** YOUR CODE HERE ***"
return self.say(self.previous)

Question 3

Modify the Account class from lecture so that it has a new attribute, transactions, that is a list keeping track of any transactions performed. See the doctest for an example.

class Account(object):
    """A bank account that allows deposits and withdrawals.

    >>> eric_account = Account('Eric')
    >>> eric_account.deposit(1000000)   # depositing my paycheck for the week
    1000000
    >>> eric_account.transactions
    [('deposit', 1000000)]
    >>> eric_account.withdraw(100)      # buying dinner
    999900
    >>> eric_account.transactions
    [('deposit', 1000000), ('withdraw', 100)]
    """

    interest = 0.02

    def __init__(self, account_holder):
        self.balance = 0
        self.holder = account_holder
"*** YOUR CODE HERE ***"
self.transactions = []
def deposit(self, amount): """Increase the account balance by amount and return the new balance. """
"*** YOUR CODE HERE ***"
self.transactions.append(('deposit', amount))
self.balance = self.balance + amount return self.balance def withdraw(self, amount): """Decrease the account balance by amount and return the new balance. """
"*** YOUR CODE HERE ***"
self.transactions.append(('withdraw', amount))
if amount > self.balance: return 'Insufficient funds' self.balance = self.balance - amount return self.balance

Property Decorator

Sometimes we need to run a calculation to determine the value for an instance attribute every time we look it up. We can make a zero argument method behave like an instance attribute by using the property decorator:

class Purse():
    """
    A purse class with a bursting property method.

    >>> coin_purse = Purse(101)
    >>> coin_purse.bursting    # Look Mom! No Parentheses!
    True
    >>> coin_purse.coins -= 10 # Paying taxes
    >>> coin_purse.bursting    # Not bursting anymore
    False
    """
    def __init__(self, coins):
        self.coins = coins

    @property
    def bursting(self):
        return self.coins > 100

The property method allows us to call a method that has no arguments without adding parentheses.

Question 4

Implement the class BadBankAccount, which is a subclass of Account. BadBankAccount allows the account holder to withdraw more money than they have (i.e. overdraw). Once overdrawn, BadBankAccount prevents the account holder from withdrawing money again until the account has a positive balance. You should also implement the property method overdrawn, which returns a boolean that tells whether or not an account is currently locked due to being overdrawn.

class BadBankAccount(Account):
    """ A subclass of bank account that allows an account holder to overdraw
    once, and then prevents them from withdrawing more money. You should also
    implement the property method overdrawn, which allows an account holder to
    check if they are overdrawn.

    >>> harold_account = BadBankAccount('Harold')
    >>> harold_account.deposit(100)   # depositing my paycheck for the week
    100
    >>> harold_account.withdraw(101)  # buying dinner
    -1
    >>> harold_account.overdrawn
    True
    >>> harold_account.withdraw(100000)
    You have overdrawn, please add more money!
    -1
    >>> harold_account.deposit(10)
    9
    >>> harold_account.overdrawn
    False
    """

    def withdraw(self, amount):
        """Decrease the account balance by amount and return the
        new balance.
        """
"*** YOUR CODE HERE ***"
if self.overdrawn: print('You have overdrawn, please add more money!') return self.balance
self.balance = self.balance - amount return self.balance
"*** YOUR CODE HERE ***"
@property def overdrawn(self): return self.balance < 0

Inheritance

Question 5

Predict the result of evaluating the following calls in the interpreter. Then try them out yourself!

>>> class Account(object):
...     interest = 0.02
...     def __init__(self, account_holder):
...         self.balance = 0
...         self.holder = account_holder
...     def deposit(self, amount):
...         self.balance = self.balance + amount
...         print("Yes!")
...
>>> class CheckingAccount(Account):
...     def __init__(self, account_holder):
...         Account.__init__(self, account_holder)
...     def deposit(self, amount):
...         Account.deposit(self, amount)
...         print("Have a nice day!")
...
>>> a = Account("Billy")
>>> a.balance
______
0
>>> c = CheckingAccount("Eric") >>> c.balance
______
0
>>> a.deposit(30)
______
Yes!
>>> c.deposit(30)
______
Yes! Have a nice day!
>>> c.interest
______
0.02

Question 6

Suppose now that we wanted to define a class called DoubleTalker to represent people who always say things twice:

>>> steven = DoubleTalker("Steven")
>>> steven.say("hello")
"hello hello"
>>> steven.say("the sky is falling")
"the sky is falling the sky is falling"

Consider the following three definitions for DoubleTalker that inherit from the Person class:

class DoubleTalker(Person):
    def __init__(self, name):
        Person.__init__(self, name)
    def say(self, stuff):
        return Person.say(self, stuff) + " " + self.repeat()

class DoubleTalker(Person):
    def __init__(self, name):
        Person.__init__(self, name)
    def say(self, stuff):
        return stuff + " " + stuff

class DoubleTalker(Person):
    def __init__(self, name):
        Person.__init__(self, name)
    def say(self, stuff):
        return Person.say(self, stuff + " " + stuff)

Determine which of these definitions work as intended. Also determine for which of the methods the three versions would respond differently. (Don't forget about the repeat method!)

The last one works as intended. For the first and second ones, calling repeat would fail.

Extra Questions

The following questions are for extra practice — they can be found in the the lab06_extra.py file. It is recommended that you complete these problems as well, but you do not need to turn them in for credit.

Question 7

We'd like to be able to cash checks, so let's add a deposit_check method to our CheckingAccount class. It will take a Check object as an argument, and check to see if the payable_to attribute matches the CheckingAccount's holder. If so, it marks the Check as deposited, and adds the amount specified to the CheckingAccount's total.

Write an appropriate Check class, and add the deposit_check method to the CheckingAccount class. Make sure not to copy and paste code! Use inheritance whenever possible.

See the doctests for examples of how this code should work.

class CheckingAccount(Account):
    """A bank account that charges for withdrawals.

    >>> check = Check("Steven", 42)  # 42 dollars, payable to Steven
    >>> steven_account = CheckingAccount("Steven")
    >>> eric_account = CheckingAccount("Eric")
    >>> eric_account.deposit_check(check)  # trying to steal steven's money
    The police have been notified.
    >>> eric_account.balance
    0
    >>> check.deposited
    False
    >>> steven_account.balance
    0
    >>> steven_account.deposit_check(check)
    42
    >>> check.deposited
    True
    >>> steven_account.deposit_check(check)  # can't cash check twice
    The police have been notified.
    """
    withdraw_fee = 1
    interest = 0.01

    def withdraw(self, amount):
        return Account.withdraw(self, amount + self.withdraw_fee)

def deposit_check(self, check): if check.payable_to != self.holder or check.deposited: print("The police have been notified.") else: self.deposit(check.amount) check.deposited = True return self.balance
class Check(object):
"*** YOUR CODE HERE ***"
def __init__(self, payable_to, amount): self.payable_to = payable_to self.amount = amount self.deposited = False

Question 8

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

class Keyboard:
    """A Keyboard takes in an arbitrary amount of buttons, and has a
    dictionary of positions as keys, and values as Buttons.

    >>> b1 = Button(0, "H")
    >>> b2 = Button(1, "I")
    >>> k = Keyboard(b1, b2)
    >>> k.buttons[0].key
    'H'
    >>> k.press(1)
    'I'
    >>> k.typing([0, 1])
    'HI'
    >>> k.typing([1, 0])
    'IH'
    >>> b1.pressed
    2
    >>> b2.pressed
    3
    """

    def __init__(self, *args):
"*** YOUR CODE HERE ***"
self.buttons = {} for button in args: self.buttons[button.pos] = button
def press(self, info): """Takes in a position of the button pressed, and returns that button's output"""
"*** YOUR CODE HERE ***"
if info in self.buttons.keys(): b = self.buttons[info] b.pressed += 1 return b.key return ''
def typing(self, typing_input): """Takes in a list of positions of buttons pressed, and returns the total output"""
"*** YOUR CODE HERE ***"
accumulate = '' for pos in typing_input: accumulate+=self.press(pos) return accumulate
class Button: def __init__(self, pos, key): self.pos = pos self.key = key self.pressed = 0