Sunday 10 April 2016

Classes 4: Encapsulation

Encapsulation can be a bit of a sticky subject. Many people have strong views on the subject and others just don't care. But what is it? Basically it is two distinct but related concepts. We can make class members like attributes private or protected. This stops anything outwith the class from accessing those members directly. We can also bundle data with its associated method. Which we did in Classes 3: Properties.

When we decide to make a class member like an attribute private. We have basically made the decision that it must be protected from the outside world. And this I think is the biggest problem with the whole concept when we have direct access to the source code. Nothing is really protected because with direct access to the source code. We can change whatever we want.

For this reason it is better to think in terms of a compiled program or library that might be shared with many people outside your organisation. Normally an outsider would access your class using the documented API. An analogy might be a customer at a bank.

A bank is a class of business. An attribute of a bank is that it holds customer accounts. Each account has a value. The amount of cash associated with the account. Account attributes of bank classes are however private. The method we use to access the account's value is the clerk. The clerk is the getter. If we give the clerk the correct information, which is different for every customer account and thus variable, the clerk will return the value of the account. We get our money.

We can put this analogy into action with the following example. After we create accounts for Bob and Sue with the bank class, the only way to access those accounts is through clerk method. All of the attributes are private.

#!/usr/bin/env python3

__project__= "Python Classes: The Bank Example"
__author__ = "Kevin Lynch"
__version__ = "$Revision: 1 $"
__date__ = "$Date: 2016/04/11 15:23:00 $"
__copyright__ = "Copyright (c) 2016 Kevin Lynch"
__license__ = "GPLv3"

# Create the bank class.
class bank:
        # Initialise bank with custom attributes.
        def __init__(self,account,pin,cash):
                # This is a bank. So all attributes are private.
                self.__account = account
                self.__pin = pin
                self.__cash = cash
                
        # Create the getter method.
        def clerk(self,account,pin):
                if account == self.__account:
                        if pin == self.__pin:
                                return str(self.__cash) # We get our money.
                        else:
                                return "CALLED THE COPS!!!" # Wrong pin code.
                else:
                        return "no such account" # Wrong account number.

# Global variables
bobsAccount = bank(12345678,9990,500)
suesAccount = bank(87654321,2204,5)

# We can ask the clerk how much money Bob and Sue have.
print("Bob has " + bobsAccount.clerk(12345678,9990) + " in the bank.\n\n")
print("Sue has " + suesAccount.clerk(87654321,2204) + " in the bank.\n\n")

# Sue forgot her pin code and used Bob's instead.
print("Sue has " + suesAccount.clerk(87654321,9990) + " in the bank.\n\n")

# Bob can't remember his account number.
print("Bob has " + bobsAccount.clerk(12945070,9990) + " in the bank.\n\n")

# Uncomment the following code to prove the attributes are private.
# print("Bob has " + str(bobsAccount.__cash) + " in the bank.\n\n")

The Big Lie
Uncommenting the last line of code in the example seems to prove the attributes of bank are indeed private. But this is not actually the case. Everything in Python is public. We just need to know where to look.

Every Python object has a number of built-in attributes and methods that are added automatically. The method __init__() is one such example. This method runs automatically every time an instance of a class is created. When we define __init__() in our code, we are overriding the default. And this allows us to add a degree of flexibility to the attributes, properties and methods we have add to the class.

All of the attributes within a class are contained within a dictionary object called __dict__. As we've seen in the Electronic Point Of Sale project. Dictionary objects can be accessed in much the same way as lists. And this means a determined developer would have no real problem in finding and interrogating private attributes within a class.

To get around this problem, supposedly, we can override the built in getters and setters that all Python classes have. These are called __getattr__ and __setattr__ respectively. However I've never seen this working and don't seem to be able to make it work. So, so far as I'm concerned for the time being. It doesn't work (at least not without crashing your program) and everything is public. Even when it's private.

Reading The Dictionary

#!/usr/bin/env python3

__project__= "Python Classes: The Bank Example"
__author__ = "Kevin Lynch"
__version__ = "$Revision: 2 $"
__date__ = "$Date: 2016/04/11 20:27:00 $"
__copyright__ = "Copyright (c) 2016 Kevin Lynch"
__license__ = "GPLv3"

# Create the bank class.
class bank:
        # Initialise bank with custom attributes.
        def __init__(self,account,pin,cash):
                # This is a bank. So all attributes are private.
                self.__account = account
                self.__pin = pin
                self.__cash = cash
             
# Global variable. Bob creates his account with the bank.
bobsAccount = bank(12345678,9990,500)

# Lets look at the dictionary.
print("\n\nReading the whole dictionary")
print(bobsAccount.__dict__)

# Now lets search for the cash.
d = bobsAccount.__dict__ # This will make the dictionary easier to work with.
for key in d.keys():
        # Search for the keyword cash and do something.
        if "cash" in key:
                print("\n\nCash found in " + key)
                print(key + " = " + str(d[key]))
             
                # We can even edit the value of the attribute.
                print("\n\nLets give Bob more cash!!!")
                d[key] = 1000
                print(key + " = " + str(d[key]))              
                break
        else:
                print("\nCash not found in " + key)
             
# Now lets prove we actually altered Bob's account.
# Since we know the name of the attribute. We can access it directly.
print("\n\nCash in Bob's account = " + str(bobsAccount._bank__cash))

Why Bother Then?
There are a few reasons. The straw man is that it's what developers from the likes of Java or C++ backgrounds have been taught to do. So it's considered "good practice". However it would also be good practice if people just read the API reference, used the class properly and saved other programmers from having to write countless lines of extra code.

A much better reason is that it ensures there is one and only one obvious means of accessing and altering class members. Which in turn helps to bolster the integrity of those class members and the stability of the program overall. By using encapsulation with getters and setters, we can validate data being passed around the program. Everything in theory happens in an orderly and predictable way.

Clearly this isn't a massive problem with very small and simple examples. But when a program runs into hundreds, thousands or even millions of lines of code, passes through the hands of dozens of programmers. It becomes a problem.

No comments:

Post a Comment