Consider the task of writing software for a bank. They would like to represent a bank account and typical operations on that account. For example, we may want to keep track of an account’s owner (name), the account ID, balance, and access PIN. Operations may include making a deposit, withdrawal, balance check, etc.
Writing a program would first need to establish the data, e.g.:
names = ["Ameet","Andy", "Tia"]
acctid = [1111, 1134, 2543]
balances = [0.54, 120.12, 300]
pins = [..]
Now, let’s say we want to make a withdrawal:
id = int(input("id: "))
amt = float(input("amt: ")
for i in range(len(acctid)):
if acctid[i] == id:
balances[i] -= amt
print("%d's new balance %.2f" % (acctid[i], balances[i]))
print("Thank you, %s" % names[i])
We could extend this to add data and other functionality. However, this isn’t a very satisfying solution. The code will quickly get unwieldily, becoming susceptible to bugs. It also isn’t easy to read, or reuse for a different application. We will return to the concept of object-oriented programming to come up with a better solution.
In object-oriented programming, we saw that objects described complex types that contained data and methods that could act upon the data. For example, we saw that lists and strings were objects, as were elements of the Graphics library. Each of the objects we created stored data (e.g., a center, radius and color for Circle
objects) and methods (e.g., move()
, getCenter()
, setFill()
).
Classes are the types of an object; they define the specific methods and data an object stores. We will spend the next two weeks defining classes and creating objects from them. To start, we’ll consider a class definition to solve the problem above.
Open up accounts.py
. Read through the mostly completed class definition for a bank account. Using this class will be able to define Account
objects.
Open up accounts.py
. What do you think each line is doing? What can you explain with your neighbor?
The first key element is the class definition, which follows the format:
class ClassName(object):
ClassName
is a placeholder for the actual name you want to give; class
is a keyword to indicate that you are defining a new class. The additional item, (object)
, is an indication that the class will follow certain protocols. You should assume that this will always be there (you can put something else in there if you want to build upon an existing class, but that is beyond our discussion)
Within the class, you will see several class methods. Methods follow the same format as functions, but have the distinction of belonging to a specific class.
class ClassName(object):
def __init__(self, param1, ...):
#Constructor - initializes object and data
def getter1(self):
#Return some piece of information
def setter1(self, valToSet):
#Change something about the data
Two broad category of methods are accessors (or getters) and mutators (or setters). Some methods do both. Methods are the primary method for interfacing with the data maintained within the class - we do not directly touch the data, but rather use methods to get/set information.
For example, consider lists. When we want to add a value to the end of the list, we do not directly access the data within the list. Rather, we use the append(x)
method which handles that work for us.
Python also has special built-in methods that are flanked by underlines e.g., __init__()
. __init__
gets called whenever we call the constructor to create a new object. For example, when creating a Point
object:
pt = Point(x,y)
Python will run the __init__(self,x,y)
method of the Point
class. Every class must have a constructor - it specifies what data we need to keep track of and gives it an initial value. There are other special methods that we will not cover. These include __len__
(to obtain the length of a sequence e.g., len(ls)
calls the list.__len__()
method) and __str__
(to convert an object to a string representation, str(x)
).
Lastly, notice that all methods have a self
parameter. This parameter does not actually require the user to give an argument; Python automatically assigns the object that called the method as the self
parameter. For example, if we create an Account
object, acct
and call acct.getBalance()
, the self
parameter points to acct
.
Test out how the Account
class works. What is the output of each of these commands, and can you draw the stack diagram that accompanies it?
>>> from account import *
>>> a1 = Account("Ameet", 803456, 1234,0.54)
>>> print(a1.toString())
>>> a1.getBalance()
>>> a1.withdraw(1234, 0.25)
>>> a1.getBalance()
Just as with top-down design, you should practice incremental development when defining a class. Usually, this involves writing a function, thoroughly testing it, moving onto the next function, etc. There are several options for how to test, including using a separate main program. However, a common strategy is to include some test code at the bottom of your class definition file. This way, the test code can always be run in the future when updates are made. To do so, include the following lines at the bottom of your file:
if __name__ == `__main__`:
#Include testing code here
The first line ensures that the testing code does not run when you import the file, but rather only when someone calls the file from the command line:
$ python3 account.py
Take some time to implement and test the changePin()
method. It is very important that you employ unit testing just as you did with the top-down design labs
Analyze and test the withdraw()
function. Once you understand how that works, implement and test the deposit()
method.
If you have time, implement and test the computeInterest()
method.
Inside a class definition, the object is referred to as self
. If we want to access its data, we use dot notation e.g., self.balance
is the balance on the account. This means that the variable’s scope is the object itself, not the function that it was created.
It should be noted that the definition of an object includes data and methods. So, if we want to refer to a call a method we need to use dot notation since the method belongs to the object e.g., acct.getBalance()
. But what if we want to call a method within another method? That is, within account.py
, we wanted to call a method? Well, since self
is the object and the method we want to call belongs to self
, we can call it e.g., self.getBalance()
As an example, take a look at the solution to computeInterest()
in account_soln.py
:
def computeInterest(self, percent):
if percent > 0:
self.balance = self.balance + self.balance * percent
Another way to think of the balance update is that we are adding money to the account. We have a method that does that - deposit()
. So, if want to use deposit()
we could change our solution to:
def computeInterest(self, percent):
if percent > 0:
interest = self.balance * percent
self.deposit(self.pin, interest) #adds interest to balance
In this program, we actually have three functions that change the balance (computeInterest
,deposit
,withdraw
). A smart design choice would be to create a common method e.g., adjustBalance()
that gets reused by all three methods:
def adjustBalance(self, amount):
self.balance += amount
Then we could redefine our methods to use adjustBalance
instead of manually changing the balance (modified lines are followed by #***
):
def withdraw(self, pin, amt):
if (self.pin == pin and self.balance >= amt):
self.adjustBalance(-amt) #*** subtracts amt
return True
return False
def deposit(self, pin, amt):
if pin == self.pin and amt >= 0:
self.adjustBalance(amt) #***
return True
return False
def computeInterest(self, percent):
if percent > 0:
interest = self.balance*percent
self.adjustBalance(interest) #***
Previously, you have seen how to put items of any type into a list, including strings and graphics objects. Open up testAccounts.py
and trace through the code. We create four different Account
objects and add them to a list before calling some methods on each object to test our code. What is the type of the following values:
acct_list
?acct_list[i]
?acct_list[i].getBalance()
?In atm.py
, I have provide a simple program that emulates an ATM. It will read account information from a file, use the Account
class to create a list of objects to manage the accounts, and present the user with menu options that allow them to perform basic banking operations. As an exercise (most likely out of class), accomplish the following:
Complete the function createAccountListFromFile(filename)
. You should read each line of the file, parsing it for the account data. Break the line up into pieces and use the Account
constructor to create a new instance. If you are lost, follow the examples in testAccounts.py
.
Test the printDB()
function to see if you have properly read in the file and all of its contents.
Test the withdrawMenu()
function, which allows a user to identify an account, PIN, and withdrawal amount. Notice how the function safeguards agains invalid account IDs (account doesn’t exist). The withdraw()
method abstracts the process of validating the PIN and withdrawal amount (returning False
if either is invalid).
Implement the depositMenu()
method. It should be similar to the withdrawMenu()
option.
Implement balanceMenu()
to see if you can access the balance of an account. You will need to ask for the account ID, but our implementation does not require a PIN value.
Implement a class called Team that stores and manipulates information about a competitive team (e.g., soccer, debate, baseball). Each team keeps track of its name, year, city, wins, and losses. In addition:
wonGame
that increments the team’s number of wins by 1.lostGame
that updates the number of losses.getWinPercent
that returns the percentage of games won out of the total number of games played. If a team has played 0 games, return a winning percentage of 0.0. If a team has won 3 out of 4 games, return a winning percentage of 0.75