In this unit we are going to explore some more basic features of OOP in Python. In particular, we'll take a closer look at how we can implement the idea of encapsulation mentioned previously.
In proper object-oriented programming languages the idea of encapsulation is taken quite seriously and there are specific keywords that allow you to restrict access to the attributes of a class. That's not so in Python. In general, in Python every attribute is public, i.e. it can be accessed from outside the class in which it is defined. However, in keeping with the OOP philosophy, there are some conventions that you can follow in order to inform other programmers that some of the attributes in your classes have certain restrictions (note that this still does not change how Python operates by default). Generally speaking, we can introduce certain writing conventions for protected and private attributes, as explained next.
For protected attributes, the Python convention is to prefix the attribute name with a single underscore. However, doing that does not change the class users' ability to modify the attribute from outside the class. The single underscore is simply a marker letting them know not to access or change the attribute from outside the class.
For private attributes, you prefix the attribute name with a double underscore. This renders the corresponding attribute inaccessible from outside the class. If you follow this route, you need to provide getter and setter methods (if you still want to access that attribute from outside the class).These are also known as accessor and mutator methods, and they are discussed below (in the next section) in the context of simple concrete examples (and are taken from the textbook mentioned at the end of this document).
# Example to illustrate that "protected" attributes
# can be accessed/changed from outside a class:
class Dot(object):
def __init__(self, color):
self._color = color # _color = protected attribute
# just ONE underscore
def print_it(self):
# displays on the screen the color of your object
print("Color of object is: ", self._color)
# quick demo:
def main():
dot1 = Dot('blue') # create a blue dot
dot1.print_it() # check it
dot1._color = 'yellow' # change the color to yellow
dot1.print_it() # check it again
# are you convinced?
main()
# Example to illustrate that "private" attributes
# can NOT be accessed/changed from outside a class:
class Dot(object):
def __init__(self, color):
self.__color = color # _color = PRIVATE attribute
# DOUBLE underscores
def print_it(self):
# displays on the screen the color of your object
print("Color of object is: ", self.__color)
# quick demo:
def main():
dot1 = Dot('blue') # create a blue dot
dot1.print_it() # check it
dot1.__color = 'yellow' # does NOT do what it's intended to....
dot1.print_it() # check it again (nothing's changed)
print(vars(dot1)) # display on the screen the attributes of object 'dot1'
main()
If the attribute is private you can no longer access it from outside the class. For that reason, your classes will have to be equipped with methods that allow users to change such attributes as well as check on their status (what's stored in them, etc). The next section deals with this particular aspect.
It is common practice in OOP to make all of a class data attributes private, and to provide public methods for accessing and changing those attributes. This ensures that the object owning those attributes is in control of the changes made to them.
A method that returns a value from a class attribute but does not change it is known as an accessor method.
A method that stores a value in a data attribute or changes the value of a data attribute in some other way is known as a mutator method.
Accessor methods are known as "getters", mutator methods as "setters".
The first example creates a class called BankAccount, which is supposed to do what its name suggests. Bank accounts can be initialised with a certain amount of money in them (that's done in the __init__ method); there are two mutator methods (deposit() and withdraw()) and one accessor method (get_balance()). Note that the objects instantiated from this class will have just one attribute (that's the private attribute __balance).
# (examples from 'Starting out with Python, by T. Gaddis, 2018)
#
class BankAccount(object):
# The __init__ method accepts an argument for
# the account's balance. It is assigned to
# the __balance attribute.
def __init__(self, bal):
self.__balance = bal
# The deposit method makes a deposit into the
# account.
def deposit(self, amount):
self.__balance += amount
# The withdraw method withdraws an amount
# from the account.
def withdraw(self, amount):
if self.__balance >= amount:
self.__balance -= amount
else:
print('Error: Insufficient funds')
# The get_balance method returns the balance
def get_balance(self):
return self.__balance
The lines of code included below are used for testing the class 'BankAccount'. As explained in the previous sessions, you should develop the habit of writing such testing code inside a small function. After the code is "compiled", you do need to type 'main()' in the console and then press 'ENTER'. Alternatively, at the end of the code I included a call to the 'main()' function (this is the very last line); with this approach when you click 'RUN' in the Spyder programming environment, the code is "compiled" and then executed for you. The output will be displayed in the console.
# demo for the BankAccount class:
def main():
# Get the starting balance.
start_bal = float(input('Enter your starting balance: '))
# Create a BankAccount object.
savings = BankAccount(start_bal)
# Deposit the user's paycheck.
pay = float(input('How much were you paid this week? '))
print('I will deposit that into your account.')
savings.deposit(pay)
# Display the balance.
print('Your account balance is $', \
format(savings.get_balance(), ',.2f'),
sep='')
# Get the amount to withdraw.
cash = float(input('How much would you like to withdraw? '))
print('I will withdraw that from your account.')
savings.withdraw(cash)
# Display the balance.
print('Your account balance is $', \
format(savings.get_balance(), ',.2f'),
sep='')
# Call the main function.
main()
Another example of class with mutator and accessor methods is included below. This class simulates the tossing of a coin, so it is aptly named Coin. The objects from this class have a single (private) attribute, __sideup. There's one mutator method (called toss()) and one accessor method (called get_sideup()).
import random
# The Coin class simulates a coin that can
# be flipped.
class Coin(object):
# The __init__ method initializes the
# __sideup data attribute with 'Heads'.
def __init__(self):
self.__sideup = 'Heads'
# The toss method generates a random number
# in the range of 0 through 1. If the number
# is 0, then sideup is set to 'Heads'.
# Otherwise, sideup is set to 'Tails'.
def toss(self):
if random.randint(0, 1) == 0:
self.__sideup = 'Heads'
else:
self.__sideup = 'Tails'
# The get_sideup method returns the value
# referenced by sideup.
def get_sideup(self):
return self.__sideup
# demo for the Coin class
def main():
# Create three objects from the Coin class.
coin1 = Coin()
coin2 = Coin()
coin3 = Coin()
# Display the side of each coin that is facing up.
print('I have three coins with these sides up:')
print(coin1.get_sideup())
print(coin2.get_sideup())
print(coin3.get_sideup())
print()
# Toss the coin.
print('I am tossing all three coins...')
print()
coin1.toss()
coin2.toss()
coin3.toss()
# Display the side of each coin that is facing up.
print('Now here are the sides that are up:')
print(coin1.get_sideup())
print(coin2.get_sideup())
print(coin3.get_sideup())
print()
# call the main function:
main()
A final example is included immediately below. This involves a class CellPhone that holds data about a mobile phone. The class has three (private) attributes: __manufact, __model, and __retail_price. Each attribute has its own acessor and mutator methods; the former start with the word get, while the latter start with set.
# The CellPhone class holds data about a cell phone.
class CellPhone(object):
# The __init__ method initializes the attributes.
def __init__(self, manufact, model, price):
self.__manufact = manufact
self.__model = model
self.__retail_price = price
# The set_manufact method accepts an argument for
# the phone's manufacturer.
def set_manufact(self, manufact):
self.__manufact = manufact
# The set_model method accepts an argument for
# the phone's model number.
def set_model(self, model):
self.__model = model
# The set_retail_price method accepts an argument
# for the phone's retail price.
def set_retail_price(self, price):
self.__retail_price = price
# The get_manufact method returns the
# phone's manufacturer.
def get_manufact(self):
return self.__manufact
# The get_model method returns the
# phone's model number.
def get_model(self):
return self.__model
# The get_retail_price method returns the
# phone's retail price.
def get_retail_price(self):
return self.__retail_price
# This program tests the CellPhone class.
def main():
# Get the phone data.
man = input('Enter the manufacturer: ')
mod = input('Enter the model number: ')
retail = float(input('Enter the retail price: '))
print()
# Create an instance of the CellPhone class.
phone = CellPhone(man, mod, retail)
# Display the data that was entered.
print('Here is the data that you entered:')
print('Manufacturer:', phone.get_manufact())
print('Model Number:', phone.get_model())
print('Retail Price: $', format(phone.get_retail_price(), ',.2f'), sep='')
# Call the main function.
main()
# ANOTHER DEMO CODE FOR THE 'CELLPHONE' CLASS
# This program creates five CellPhone objects and
# stores them in a list.
#import cellphone
from __future__ import print_function
def main():
# Get a list of CellPhone objects.
phones = make_list()
# Display the data in the list.
print('Here is the data you entered:')
display_list(phones)
# The make_list function gets data from the user
# for five phones. The function returns a list
# of CellPhone objects containing the data.
def make_list():
# Create an empty list.
phone_list = []
# Add five CellPhone objects to the list.
print('Enter data for 3 phones.')
for count in range(1, 4):
# Get the phone data.
print('Phone number ' + str(count) + ':')
man = input('Enter the manufacturer: ')
mod = input('Enter the model number: ')
retail = float(input('Enter the retail price: '))
print
# Create a new CellPhone object in memory and
# assign it to the phone variable.
phone = CellPhone(man, mod, retail)
# Add the object to the list.
phone_list.append(phone)
# Return the list.
return phone_list
# The display_list function accepts a list containing
# CellPhone objects as an argument and displays the
# data stored in each object.
def display_list(phone_list):
for item in phone_list:
print(item.get_manufact())
print(item.get_model())
print(item.get_retail_price())
print()
# Call the main function.
main()
REFERENCE:
T. Gaddis, Starting out with Python (Fourth Edition), Pearson Education Ltd., 2018