Python : Instance representation and Access control
In python when we create an instance , the attributes are internally stored in a python
dictionary. Dictionay plays a major part in internal representation of python objects. Let us consider
a simple class Person
as shown below.
class Person(object):
def __init__(self, name, age, id ):
self.name = name
self.age = age
self._id = id
def get_lastname(self):
return self.name.split(' ')[1]
Now let us access its attributes using the dictionary as shown below..
>>> p = Person('Jithesh Chandrasekharan', 32,1)
>>> p.__dict__
{'name': 'Jithesh Chandrasekharan', '_id': 1, 'age': 32}
>>> p.__dict__['name']
'Jithesh Chandrasekharan'
>>> p.__dict__['age']
32
One major advantage(some times error prone) of this dictionary representation is that we can add attributes to an object on the fly as shown below. In this case we are adding a new attribute on the instance.
>>> p.spam = 2
>>> p.__dict__
{'_id': 1, 'name': 'Jithesh Chandrasekharan', 'spam': 2, 'age': 32}
You can also delete instance variables as shown below..
>>> del p.spam
As you can see the instance __dict__
holds only the attributes, so where are the methods ? Methods are part of
class representation and hence we can access methods through class.__dict__
as shown below. Each instance holds
a __class__
variable as shown below and class holds a __dict__
which has method entries.
>>> p.__class__
<class 'oop.Person'>
So when you call p.get_lastname()
. Python is first resolving the class from __class__
variable of the instance,
then it will get method from the internal __dict__
of the class as shown below.
>>> Person.__dict__
mappingproxy({'__dict__': <attribute '__dict__' of 'Person' objects>, '__init__':
<function Person.__init__ at 0x0000014087BD5488>,
'__weakref__': <attribute '__weakref__' of 'Person' objects>,
'__module__': 'oop', '__doc__': None,
'get_lastname': <function Person.get_lastname at 0x0000014087BD5510>})
Now it will get the method from the __dict__
and calls the method passing instance as parameter.
>>> Person.__dict__['get_lastname']
<function Person.get_lastname at 0x0000014087BD5510>
>>> Person.__dict__['get_lastname'](p)
'Chandrasekharan'
Access Modifiers ?
One of the biggest advantage here is flexibility, but it raises an important question when writing a
large program, how we can control access to these attributes. There is no notion of private/public
in python. It largely depends on naming conventions. So variables prefixed with underscore _id
are
considered as private. This naming convention is applicable to methods too. But naming convention really
solves the problem ? Its not actually restricting any access. So let us see how we can implement some control
around attributes.
As a first step let us see how we can take ownership of an attribute using properties in Python.
Let us start with a simple Holding class as shown below.
class Holding(object):
def __init__(self, name, date, shares, price):
self.name = name
self.date = date
self.shares = shares
self.price = price
def cost(self):
return self.shares*self.price
In this example, we want to ensure that shares
should be an int
and price
should be float
. In order
to enforce these restriction we can make these two attributes as property (@property
) as shown below.
class Holding(object):
def __init__(self, name, date, shares, price):
self.name = name
self.date = date
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self,new_shares):
if not isinstance(new_shares,int):
raise TypeError('Expecting an int !')
self._shares = new_shares
@property
def price(self):
return self._price
@price.setter
def price(self,new_price):
if not isinstance(new_price, float):
raise TypeError('Expecting a float')
self._price = new_price
@property
def cost(self):
return self.shares*self.price
So what we have done here is hiding the actual price
and shares
in private variable _price
and _shares
.
Then we made our price
and shares
as properties. So now the internal representation is as shown below. So
we encapsulated our shares and price attributes.
hld1 = Holding('JJJ','23-2-2222',22,33.00)
{'name': 'JJJ', '_shares': 22, 'date': '23-2-2222', '_price': 33.0}
This way we can have a type checking before setting the value. In this case we ensure that correct types are passed.
@property
def shares(self):
return self._shares
@shares.setter
def shares(self,new_shares):
if not isinstance(new_shares,int):
raise TypeError('Expecting an int !')
self._shares = new_shares
Now this will raise an exception.Here we are passing a string for shares instead of int.
hld1 = Holding('JJJ','23-2-2222','2232',33)
raise TypeError('Expecting an int !')
Now another important change we made here is to change our cost method to a property. This way
user can access cost
method like a property.
@property
def cost(self):
return self.shares*self.price
Coding is fun enjoy…