Object Oriented Programming

See also

Reading Assignment

Text: Chapter 11

So far in this class, we have been working with the most fundamental components of computer programming, and trying to learn how to use those components in common situations. We have worked a number of fairly simple projects (well, as you learn more, they will seem simple!), each designed to explore the basic features of those components in realistic problems.

Unfortunately, the problems we need to be able to address are far more complex than those we have experimented with. Over the course of the last 40 or so years that people have been seriously developing computer programs, they have discovered that programmers need help in building really big programs. The basic components are fine as far as they go, but we need more powerful tools to construct the really big programs. This is just like realizing that building a small house is not the same kind of task as building a skyscraper - we need bigger tools!

Around the mid 1980s, people started studying how programs were being constructed, and came up with a new way to think about programming components. The old structured approach was fine, as far as it went, but when we looked closer at the process of breaking down real problems, we noticed that we were building program components that represented real things in the world of the user of our programs. But, we did not have a way to represent those things in really efficient ways.

The result of all of this study was to start thinking of programming in terms of a new component - an object that has both physical (sort-of) characteristics, and a behavior that says how it acts. We have talked about building functions to capture the details of a particular activity in a way that hides exactly what is going on inside, and instead presents the user of that function with a simple way to work with the function - the interface definition. Functions are a great way to break up a complex program into more manageable chunks.

Can we do the same thing with these funky objects that model the real world?

A Bank Account class

Suppose we want to build a program to manage a bank. We expect to create a number of accounts and let customers make deposits and withdrawals. We also want to let them check their balances. Here is a start on such a class:

1
2
3
4
class BankAccount(object):

    def __init__(self, initial_balance = 0):
        self.balance = initial_balance

A class has a name, and may be related to other classes. All Python classes are related to a general class named object, so we say this when we set up the new class.

In this code, we have provided a special method called a constructor to set up a new bank account object, which we save in a variable of our own choosing. All classes must have such a constructor, and the name is always this funny __init__ name.

This constructor sets up a special variable called an instance variable that tracks the balance of this account. We set it with the value we pass in when we build a new account.

Note

Since Python has no idea what you will call this object when you build a new account, it uses a special name, self to refer to it in this code. When we build objects and want to interact with them, self will be set up to refer to a specific object.

Once the object has been created, we can send it messages using the defined methods we set up:

  • deposit - add some amount to this account
  • withdrawal - remove some amount from this account
  • check_balance - report how much is in this account

Here is the new code we add to our class to implement these methods. Note that each one must be indented the same as the constructor and each must include that funny self parameter so the class will work right.

1
2
3
4
5
6
7
8
    def deposit(self, amount):
        self.balance += amount

    def withdrawal(self, amount):
        self.balance -= amount 

    def check_balance(self):
        return self.balance

Finally, we set up some example code in this file to test our new class:

1
2
3
4
5
if __name__ == '__main__':
    account = BankAccount(100)
    print("Current balance:",account.check_balance())
    account.deposit(200)
    print("Current balance:",account.check_balance())

This code should run, but after looking at this example, it occurs to us that we could improve our class to make it report what is going on as we process customer activities:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BankAccount(object):

    def __init__(self, initial_balance = 0):
        self.balance = initial_balance
        print("Account opened with balance:", self.balance)

    def deposit(self, amount):
        self.balance += amount
        print("Deposited",amount,"New balance:",self.balance)

    def withdrawal(self, amount):
        self.balance -= amount 
        print("Withdrawal:",amount,"New balance:",self.balance)

    def check_balance(self):
        print("Balance check:",self.balance)
        return self.balance

if __name__ == '__main__':
    account = BankAccount(100)
    account.deposit(200)
    account.check_balance()

This looks a little better. Here is a sample run:

Account opened with balance: 100
Deposited 200 New balance: 300
Balance check: 300

Improving the class

This class does work, but it is not a very good manager of bank accounts. For one thing, the customer can withdraw any amount they like (not a good idea for the bank!)

We can modify the class to fix this problem, with a few changes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class BankAccount(object):

    def __init__(self, initial_balance = 0):
        self.balance = initial_balance
        print("Account opened with balance:", self.balance)

    def deposit(self, amount):
        self.balance += amount
        print("Deposited",amount,"New balance:",self.balance)

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount 
            print("Withdrawal:",amount,"New balance:",self.balance)
        else:
            print("Attempt to overdraw: Requested:",amount,"Available:",self.balance)

    def check_balance(self):
        print("Balance check:",self.balance)
        return self.balance

if __name__ == '__main__':
    account = BankAccount(100)
    account.deposit(200)
    account.withdraw(600)
    account.check_balance()

Now, the account cannot be overdrawn, which is better for the bank:

Account opened with balance: 100
Deposited 200 New balance: 300
Attempt to overdraw: Requested: 600 Available: 300
Balance check: 300

More banking rules

Suppose the bank sets up a few more rules. For instance, we might want to mark the account as either open or closed for transactions. If there is an attempt to overdraw the account, we will close it preventing any new transactions, until the customer specifically reactivates the account. (Perhaps the bank manager has to do this step).

Here is a new class that implements this new rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class BankAccount(object):

    def __init__(self, initial_balance = 0):
        self.balance = initial_balance
        self.closed = False
        print("Account opened with balance:", self.balance)

    def deposit(self, amount):
        if self.closed:
            print("No deposits allowed on a closed account")
            return
        self.balance += amount
        print("Deposited",amount,"New balance:",self.balance)

    def withdraw(self, amount):
        if self.closed:
            print("No withdrawals allowed on a closed account")
            return
        if amount <= self.balance:
            self.balance -= amount 
            print("Withdrawal:",amount,"New balance:",self.balance)
        else:
            self.closed = True
            print("Attempt to overdraw by",amount - self.balance)
            print("Account is closed")

    def check_balance(self):
        if self.closed:
            print("Account is closed, call bank manager")
            return 0
        print("Balance check:",self.balance)
        return self.balance

    def reactivate(self):
        self.closed = False

if __name__ == '__main__':
    account = BankAccount(100)
    account.deposit(200)
    account.withdraw(100)
    account.withdraw(400)
    account.deposit(500)
    account.check_balance()

To make this rule work, we need a new instance variable, closed, which is a boolean that says the account is active or closed. When we build the new account, we mark it as active. If the account is closed, we reject any transactions. If the account is active, but we attempt to overdraw, we mark the account as closed!

Account opened with balance: 100
Deposited 200 New balance: 300
Withdrawal: 100 New balance: 200
Attempt to overdraw by 200
Account is closed
No deposits allowed on a closed account
Account is closed, call bank manager

Looks like this is working.

1
2
3
4
5
    def show_assets(self):
        assets = 0
        for account in self.accounts:
            assets += account.get_balance()
        print("Current assets:",assets)

Now, let’s show an example of the code it will take to process a deposit for a specific account:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    def find_account(self, ID):
        for account in self.accounts:
            if account.get_ID() == ID: return account
        return None

    def deposit(self, ID, amount):
        print("Depositing",amount,"to account",ID)
        account = self.find_account(ID)
        if account:
            account.deposit(amount)
        else:
            print("Invalid ID - ignored")

Here, we need to search the list of accounts to find the one we are interested in, then deposit to that account.

Here is the complete program:

class BankAccount(object):

    def __init__(self, amount):
        self.balance = amount
        self.closed = False

    def set_ID(self, ID):
        self.ID = ID

    def get_ID(self):
        return self.ID

    def get_balance(self):
        return self.balance

    def deposit(self, amount):
        self.balance += amount
        print("\tNew balance =",self.balance)

class BankManager(object):

    account_ID = 1000
4
    def __init__(self):
        self.accounts = []

    def create_account(self, amount):
        account = BankAccount(amount)
        account.set_ID(self.account_ID)

        account.closed = False
        print("Account opened with balance:", account.get_balance())
        print("  Your account ID is:",self.account_ID)
        self.account_ID += 1
        self.accounts.append(account)
        return account.get_ID()

    def show_assets(self):
        assets = 0
        for account in self.accounts:
            assets += account.get_balance()
        print("Current assets:",assets)

    def find_account(self, ID):
        for account in self.accounts:
            if account.get_ID() == ID: return account
        return None

    def deposit(self, ID, amount):
        print("Depositing",amount,"to account",ID)
        account = self.find_account(ID)
        if account:
            account.deposit(amount)
        else:
            print("Invalid ID - ignored")

if __name__ == '__main__':
    bank = BankManager()
    ID1 = bank.create_account(100)
    ID2 = bank.create_account(200)
    bank.show_assets()
    bank.deposit(ID1, 300)
    bank.show_assets()