Learning Scientific Programming with Python (2nd edition)
P4.6.1: Modifying the BankAccount class
Question P4.6.1
(a) Modify the base BankAccount
class (Section 4.6.2 of the book) to verify that the account number passed to its __init__
constructor conforms to the Luhn algorithm described in Exercise P2.5.3.
(b) Modify the CurrentAccount
class to implement a free overdraft. The limit should be set in the __init__
constructor; withdrawals should be allowed to within the limit.
Solution P4.6.1
(a) The definition of BankAccount
in the code that follows uses the Luhn checksum method to validate the account number.
The account number may be provided as an integer or as a string (possibly with spaces or hyphens delimiting groups of numbers):
>>> from bank_account2 import BankAccount
>>> BankAccount('John Brown', 7298-904')
<bank_account2.BankAccount at 0x103fc1350>
>>> BankAccount('John Brown', 7298905)
...
ValueError: Invalid account number
(b) An interest-free overdraft is implemented in the CurrentAccount
class below as well; a change has been made to the base class's BankAccount
method to output negative balances cleanly.
>>> from bank_account2 import CurrentAccount
>>> account = CurrentAccount('John Brown', '729-8904', 0, 500)
>>> account.deposit(400)
>>> account.withdraw(350)
>>> account.check_balance()
The balance of account number 729-8904 is $50.00
>>> account.withdraw(200)
>>> account.check_balance()
The balance of account number 729-8904 is -$150.00
>>> account.withdraw(600)
$600.00 exceeds the single transaction limit of $500.00
>>> account.withdraw(500)
>>> account.check_balance()
The balance of account number 729-8904 is -$650.00
>>> account.withdraw(500)
Insufficient funds
The modified code is:
class BankAccount:
"""A abstract base class representing a bank account."""
currency = "$"
def __init__(self, customer, account_number, balance=0):
"""
Initialize the BankAccount class with a customer, account number
and opening balance (which defaults to 0.)
"""
self.customer = customer
self.set_account_number(account_number)
self.balance = balance
def set_account_number(self, account_number):
if not self.verify_account_number(account_number):
raise ValueError("Invalid account number")
self.account_number = account_number
def verify_account_number(self, account_number):
"""Verify the account number, returning True if it is valid."""
ccl = [int(d) for d in str(account_number) if d not in ("-", " ")]
for i in range(len(ccl) - 2, 0, -2):
ccl[i] *= 2
if ccl[i] > 9:
ccl[i] = 1 + (ccl[i] - 10)
checksum = sum(ccl) % 10
return not checksum
def deposit(self, amount):
"""Deposit amount into the bank account."""
if amount > 0:
self.balance += amount
else:
print("Invalid deposit amount:", amount)
def withdraw(self, amount):
"""
Withdraw amount from the bank account, ensuring there are sufficient
funds.
"""
if amount > 0:
if amount > self.balance:
print("Insufficient funds")
else:
self.balance -= amount
else:
print("Invalid withdrawal amount:", amount)
def check_balance(self):
"""Print a statement of the account balance."""
s_sign = "-" if self.balance < 0 else ""
print(
f"The balance of account number {self.account_number} is"
f" {s_sign}{self.currency}{abs(self.balance):.2f}"
)
class CurrentAccount(BankAccount):
"""A class representing a current (checking) account."""
def __init__(
self,
customer,
account_number,
annual_fee,
transaction_limit,
balance=0,
overdraft_limit=1000,
):
"""Initialize the current account."""
self.annual_fee = annual_fee
self.transaction_limit = transaction_limit
self.overdraft_limit = overdraft_limit
super().__init__(customer, account_number, balance)
def withdraw(self, amount):
"""
Withdraw amount if sufficient funds exist in the account and amount
is less than the single transaction limit.
"""
if amount <= 0:
print("Invalid withdrawal amount:", amount)
return
if amount > self.balance + self.overdraft_limit:
print("Insufficient funds")
return
if amount > self.transaction_limit:
print(
f"{self.currency}{amount:.2f} exceeds the single transaction"
f" limit of {self.currency}{self.transaction_limit:.2f}"
)
return
self.balance -= amount
def apply_annual_fee(self):
"""Deduct the annual fee from the account balance."""
self.balance = max(0.0, self.balance - self.annual_fee)