Coding, How-To

A Simple Guide to OOP in Python 3

For every problem, there could be more than one way to solve it. To understand Object-Oriented Programming (OOP) and use it to its best utility, we need to understand some basic principles. The best way to demonstrate that is by example.

Problem Statement

Suppose in my system, I have to keep track of credit card details. In a very simplistic design, here are the attributes that I need to create for my CreditCard class:

  • cardNumber: Card number
  • cardType: Visa or Mastercard or Amex etc.
  • ccv: The code at the back of the card
  • currency: Default is SGD, unless otherwise specified
  • creditLimit: The limit of spending on the card
  • issuingBank: Bank that issued the card
  • cardOwner: Name of owner on the credit card
  • cardExpiry: Expiry date of card
  • amountSpent: Total amount spent for the card
  • amountDue: Amount due for payment in that month
  • dueDate: Due date for payment for amountDue

You may include more attributes depending on your design, but I shall use these for now. Note that all dates in this class are datetime instances.

Encapsulation

Oooh what a big word! This is one of the features of OOP, but it definitely isn’t solely unique to OOP. The role of encapsulation essentially hides vital information from outside the class. This prevents unauthorised access to the values of object attributes, or the internal structure of a class. We should be selective on which data can be viewed or changed from outside the class to preserve the integrity of our program. In this case, we may first set all our attributes in the CreditCard class to private.

To ensure that the values of certain attributes are protected from unnecessary change outside the class, we need to decide which attributes should have mutators and which should not. In our problem statement, all the attributes, except the creditLimit, amountDue, dueDate and cardExpiry, should not be changed with a mutator.

We may have accessors for all the attributes of this class, so that we can display them appropriately.

Class Attributes

The attributes we have defined above are known as data attributes; they differ from object to object. On the other hand, class attributes are tied to the class itself, and may be accessed using .. I shall not go into detail about what they are and each and every scenario to use them (you may refer to this link and this link to learn more about it). Instead, I will focus more on demonstrating its usage through examples.

You do not need to understand thoroughly every single way to use class attributes though, because in most scenarios, you won’t be using class attributes for those purposes. Remember, there are many ways to write a program.

For our problem statement, we could create a class attribute called supportedCardTypes, that is a tuple and contains the types (like Visa, Mastercard etc.) that are supported by our system. I picked an immutable type as using a mutable type like a list to represent the data may have undesired side effects. Using this class attribute, my class will decide whether to initialise the cardType upon object creation, or produce an error due to an unsupported card type.

class CreditCard:
	supportedCardTypes = ('Visa', 'Mastercard', 'Amex', 'UnionPay')
	def __init__(self, cardNumber, cardType, ccv, creditLimit, \
		issuingBank, cardOwner, cardExpiry, currency='SGD'):
		# The prefix double underscores make the attributes private
		self.__cardNumber = cardNumber
		if cardType in CreditCard.supportedCardTypes:
			self.__cardType = cardType
		else:
			print("Invalid card type")
			self.__cardType = ''
		self.__ccv = ccv
		self.__currency = currency
		self.__creditLimit = creditLimit
		self.__issuingBank = issuingBank
		self.__cardOwner = cardOwner
		self.__cardExpiry = cardExpiry
		self.__amountSpent = 0
		self.__amountDue = 0
		self.__dueDate = None

Class Methods

Class methods are also known as behaviours. Objects from the same class will have similar behaviours, so we define them using methods.


We shall define our accessors so that we may access our data attributes outside the CreditCard class.

The following are our accessors, one for each data attribute.

	# Accessors/getters
	def get_cardNumber(self):
		return self.__cardNumber

	def get_cardType(self):
		return self.__cardType

	def get_ccv(self):
		return self.__ccv

	def get_currency(self):
		return self.__currency

	def get_creditLimit(self):
		return self.__creditLimit

	def get_issuingBank(self):
		return self.__issuingBank

	def get_cardOwner(self):
		return self.__cardOwner

	def get_cardExpiry(self):
		return self.__cardExpiry

	def get_amountSpent(self):
		return self.__amountSpent

	def get_amountDue(self):
		return self.__amountDue

	def get_dueDate(self):
		return self.__dueDate

We shall also define our mutators for creditLimit, amountDue, dueDate and cardExpiry.

	# Mutators/setters
	def set_creditLimit(self, limit):
		self.__creditLimit = limit

	def set_amountDue(self, amount):
		self.__amountDue = amount

	def set_dueDate(self, date):
		self.__dueDate = date

	def set_cardExpiry(self, date):
		self.__cardExpiry = date

We should have methods that will withdraw or deposit to the credit card account. Upon withdrawal, the amountSpent will increase. Depositing into the card will reduce the amountSpent. Note that amountSpent can be a negative value if the user deposits more than the amount they spent.

	# Actions/behaviours
	def withdraw(self, amount):
		if self.__amountSpent + amount > self.__creditLimit:
			print('Amount spent exceeds credit limit. Withdrawal failed.')
		else:
			self.__amountSpent += amount
			print('Withdrawal success.')

	def deposit(self, amount):
		self.__amountSpent -= amount

We may have some display methods to show certain information of the card, like the card monthly statement, amount spent so far, and card information. For the last one, we could use the __str__ method to create a string representation of the CreditCard instance.

	# Display/retrieve information
	def displayCurrentStatement(self):
		if self.__dueDate != None and self.__amountDue != 0:
			s = 'Amount due is {}{}, due date is {}'.format(self.__currency, \
				self.__amountDue, self.__dueDate.strftime("%Y-%m-%d"))
			print(s)
		else:
			print('Credit card not due yet.')

	def displayAmountSpent(self):
		s = 'Amount Spent: {}{}\nAmount Left to Spend: {}{}'.format(self.__currency, \
			self.__amountSpent, self.__currency, \
			self.__creditLimit - self.__amountSpent)
		print(s)

	def __str__(self):
		s = 'Card Information: {} {}\nCredit Limit: {}{}\nIssuing Bank: {}\nName on Card: {}\nExpiry Date: {}'.format(self.__cardType, \
			self.__cardNumber, self.__currency, self.__creditLimit, \
			self.__issuingBank, self.__cardOwner, \
			self.__cardExpiry.strftime("%Y-%m-%d"))
		return s

For the purpose of this demonstration, we shall create the instances of the CreditCard class within the same file. Most of the time, the codes will be contained in separate files. For the following code, you may add on your own codes by creating more than one instance of CreditCard, and calling the other methods. Try testing the code with credit card types that are not included in the supportedCardTypes attribute.


from datetime import datetime
card = CreditCard('123456789000', 'Visa', 456, 9000, 'OCBC', 'Luvelle', datetime(2019, 6, 30))
print(card)
card.displayCurrentStatement()
card.displayAmountSpent()

Putting it Together

Check out the demo code in this link!

Happy coding! 🙂


Leave a Reply

Your email address will not be published. Required fields are marked *