Object Oriented Python - Building Blocks



In this chapter, we will discuss object oriented terms and programming concepts in detail.Class is a just a factory for an instance. This factory contains the blueprint which describes how to make the instances. An instances or object are constructed from the class. In most cases, we can have more than one instances of a class. Every instance has a set of attribute and these attributes are defined in a class, so every instance of a particular class is expected to have the same attributes.

Class Bundles : Behavior and State

A class will let you bundle together the behavior and state of an object. Observe the following diagram for better understanding −

Bundles

The following points are worth notable when discussing class bundles −

  • The word behavior is identical to function – it is a piece of code that does something (or implements a behavior)

  • The word state is identical to variables – it is a place to store values within a class.

  • When we assert a class behavior and state together, it means that a class packages functions and variables.

Classes have methods and attributes

In Python, creating a method defines a class behavior. The word method is the OOP name given to a function that is defined within a class. To sum up −

  • Class functions − is synonym for methods

  • Class variables − is synonym for name attributes.

  • Class − a blueprint for an instance with exact behavior.

  • Object − one of the instances of the class, perform functionality defined in the class.

  • Type − indicates the class the instance belongs to

  • Attribute − Any object value: object.attribute

  • Method − a “callable attribute” defined in the class

Observe the following piece of code for example −

var = “Hello, John”
print( type (var)) # ‘str’> or <class 'str'>
print(var.upper()) # upper() method is called, HELLO, JOHN

Creation and Instantiation

The following code shows how to create our first class and then its instance.

class MyClass(object):
   pass
# Create first instance of MyClass
this_obj = MyClass()
print(this_obj)
# Another instance of MyClass
that_obj = MyClass()
print (that_obj)

Here we have created a class called MyClass and which does not do any task. The argument object in MyClass class involves class inheritance and will be discussed in later chapters. pass in the above code indicates that this block is empty, that is it is an empty class definition.

Let us create an instance this_obj of MyClass() class and print it as shown −

<__main__.MyClass object at 0x03B08E10>
<__main__.MyClass object at 0x0369D390>

Here, we have created an instance of MyClass. The hex code refers to the address where the object is being stored. Another instance is pointing to another address.

Now let us define one variable inside the class MyClass() and get the variable from the instance of that class as shown in the following code −

class MyClass(object):
   var = 9

# Create first instance of MyClass
this_obj = MyClass()
print(this_obj.var)

# Another instance of MyClass

that_obj = MyClass()
print (that_obj.var)

Output

You can observe the following output when you execute the code given above −

9
9

As instance knows from which class it is instantiated, so when requested for an attribute from an instance, the instance looks for the attribute and the class. This is called the attribute lookup.

Instance Methods

A function defined in a class is called a method. An instance method requires an instance in order to call it and requires no decorator. When creating an instance method, the first parameter is always self. Though we can call it (self) by any other name, it is recommended to use self, as it is a naming convention.

class MyClass(object):
   var = 9
   def firstM(self):
      print("hello, World")
obj = MyClass()
print(obj.var)
obj.firstM()

Output

You can observe the following output when you execute the code given above −

9
hello, World

Note that in the above program, we defined a method with self as argument. But we cannot call the method as we have not declared any argument to it.

class MyClass(object):
   def firstM(self):
      print("hello, World")
      print(self)
obj = MyClass()
obj.firstM()
print(obj)

Output

You can observe the following output when you execute the code given above −

hello, World
<__main__.MyClass object at 0x036A8E10>
<__main__.MyClass object at 0x036A8E10>

Encapsulation

Encapsulation is one of the fundamentals of OOP. OOP enables us to hide the complexity of the internal working of the object which is advantageous to the developer in the following ways −

  • Simplifies and makes it easy to understand to use an object without knowing the internals.

  • Any change can be easily manageable.

Object-oriented programming relies heavily on encapsulation. The terms encapsulation and abstraction (also called data hiding) are often used as synonyms. They are nearly synonymous, as abstraction is achieved through encapsulation.

Encapsulation provides us the mechanism of restricting the access to some of the object’s components, this means that the internal representation of an object can’t be seen from outside of the object definition. Access to this data is typically achieved through special methods − Getters and Setters.

This data is stored in instance attributes and can be manipulated from anywhere outside the class. To secure it, that data should only be accessed using instance methods. Direct access should not be permitted.

class MyClass(object):
   def setAge(self, num):
      self.age = num

   def getAge(self):
      return self.age

zack = MyClass()
zack.setAge(45)
print(zack.getAge())

zack.setAge("Fourty Five")
print(zack.getAge())

Output

You can observe the following output when you execute the code given above −

45
Fourty Five

The data should be stored only if it is correct and valid, using Exception handling constructs. As we can see above, there is no restriction on the user input to setAge() method. It could be a string, a number, or a list. So we need to check onto above code to ensure correctness of being stored.

class MyClass(object):
   def setAge(self, num):
      self.age = num

   def getAge(self):
      return self.age
zack = MyClass()
zack.setAge(45)
print(zack.getAge())
zack.setAge("Fourty Five")
print(zack.getAge())

Init Constructor

The __init__ method is implicitly called as soon as an object of a class is instantiated.This will initialize the object.

x = MyClass()

The line of code shown above will create a new instance and assigns this object to the local variable x.

The instantiation operation, that is calling a class object, creates an empty object. Many classes like to create objects with instances customized to a specific initial state. Therefore, a class may define a special method named ‘ __init__() ‘ as shown −

def __init__(self):
   self.data = []

Python calls __init__ during the instantiation to define an additional attribute that should occur when a class is instantiated that may be setting up some beginning values for that object or running a routine required on instantiation. So in this example, a new, initialized instance can be obtained by −

x = MyClass()

The __init__() method can have single or multiple arguments for a greater flexibility. The init stands for initialization, as it initializes attributes of the instance. It is called the constructor of a class.

class myclass(object):
   def __init__(self,aaa, bbb):
      self.a = aaa
      self.b = bbb

x = myclass(4.5, 3)
print(x.a, x.b)

Output

4.5 3

Class Attributes

The attribute defined in the class is called “class attributes’ and the attributes defined in the function is called ‘instance attributes’. While defining, these attributes are not prefixed by self, as these are the property of the class and not of a particular instance.

The class attributes can be accessed by the class itself ( className.attributeName) as well as by the instances of the class (inst.attributeName). So, the instances have access to both the instance attribute as well as class attributes.

>>> class myclass():
   age = 21
>>> myclass.age
21
>>> x = myclass()
>>> x.age
21
>>>

A class attribute can be overridden in an instance, even though it is not a good method to break encapsulation.

There is a lookup path for attributes in Python. The first being the method defined within the class, and then the class above it.

>>> class myclass(object):
   classy = 'class value'
>>> dd = myclass()
>>> print (dd.classy) # This should return the string 'class value'
class value
>>>
>>> dd.classy = "Instance Value"
>>> print(dd.classy) # Return the string "Instance Value"
Instance Value
>>>
>>> # This will delete the value set for 'dd.classy' in the instance.
>>> del dd.classy
>>> >>> # Since the overriding attribute was deleted, this will print 'class
value'.

>>> print(dd.classy)
class value
>>>

We are overriding the ‘classy’ class attribute in the instance dd. When it’s overridden, the Python interpreter reads the overridden value. But once the new value is deleted with ‘del’, the overridden value is no longer present in the instance, and hence the lookup goes a level above and gets it from the class.

Working with Class and Instance Data

In this section, let us understand how the class data relates to the instance data. We can store data either in a class or in an instance. When we design a class, we decide which data belongs to the instance and which data should be stored into the overall class.

An instance can access the class data. If we create multiple instances, then these instances can access their individual attribute values as well the overall class data.

Thus, a class data is the data that is shared among all the instances. Observe the code given below for better undersanding −

class InstanceCounter(object):
   count = 0 # class attribute, will be accessible to all instances
   def __init__(self, val):
      self.val = val
      InstanceCounter.count +=1 # Increment the value of class attribute, accessible through class name
# In above line, class ('InstanceCounter') act as an object
   def set_val(self, newval):
      self.val = newval

   def get_val(self):
      return self.val

   def get_count(self):
      return InstanceCounter.count
a = InstanceCounter(9)
b = InstanceCounter(18)
c = InstanceCounter(27)

for obj in (a, b, c):
   print ('val of obj: %s' %(obj.get_val())) # Initialized value ( 9, 18, 27)
   print ('count: %s' %(obj.get_count())) # always 3

Output

val of obj: 9
count: 3
val of obj: 18
count: 3
val of obj: 27
count: 3

In short, class attributes are same for all instances of class whereas instance attributes is particular for each instance. For two different instances, we will have two different instance attributes.

class myClass:
   class_attribute = 99

   def class_method(self):
      self.instance_attribute = 'I am instance attribute'

print (myClass.__dict__)

Output

You can observe the following output when you execute the code given above −

{'__module__': '__main__', 'class_attribute': 99, 'class_method': , '__dict__': , '__weakref__': , '__doc__': None}

The instance attribute myClass.__dict__ as shown −

>>> a = myClass()
>>> a.class_method()
>>> print(a.__dict__)
{'instance_attribute': 'I am instance attribute'}
Advertisements