Object Oriented Programming

Objects are structures which contain data (attributes) and code (hold by methods). In Python everything is an object, so, objects can contain other objects and methods.

1. Defining a class


In [1]:
class A:
    pass

The previous code is identical to:


In [2]:
class A(object):
    pass

because all objects inherits from the Object object.


In [3]:
class A():
    def Im_a_method(self):
        print("Hi!")
        self.__Im_a_hidden_method()
    def __Im_a_hidden_method(self):
        print("you shouln't call me outside this class")

2. Defining (instantiating) an object


In [6]:
x = A()
print(isinstance(x,A))


True

In [8]:
s = "hola"
isinstance(s, str)


Out[8]:
True

3. Calling to methods


In [27]:
x.Im_a_method()


Hi!
you shouln't call me outside this class

In [28]:
x.__Im_a_hidden_method()


-----------------------------------------------
AttributeErrorTraceback (most recent call last)
<ipython-input-28-305f8c056730> in <module>()
----> 1 x.__Im_a_hidden_method()

AttributeError: 'A' object has no attribute '__Im_a_hidden_method'

3. Class check

To know the class of an object:


In [ ]:
x.__class__

Remember! In Python everything is an object:


In [ ]:
10.0.__class__

4. Defining class attributes

Class attributes (variables and methods) are accesible even if the class is not instantiated.

4.1 Class variables

Because class variables do no depends on any instance, they can be used, for example, to share data between instances:


In [ ]:
class A:
    class_variable = 1
    
A.class_variable

In [ ]:
A.class_variable = 2
A.class_variable

In [ ]:
a = A()
a.class_variable

In [ ]:
b = A()
b.class_variable

4.2 Class methods

By default, (instance) methods are created when the class is instantiated. On the other hand, class methods are created when the class is defined. In order to distinguish them, class methods must be decorated with @classmethod or @staticmethod. The only difference between both types of methods is that in a @classmethod, the name of the class is passed automatically by the interpreter as the first argument.


In [9]:
class A:
    def instance_method(self, x):
        print('executing instance method with args (%s,%s)' % (self, x))
        
    @classmethod
    def class_method(cls, x):
        print('executing class method class_method with args (%s,%s)' % (cls, x))

    @staticmethod
    def static_method(x):
        print('executing static (class) method static_method with arg (%s)' % x)

In [10]:
A.instance_method(1)


-----------------------------------------------
TypeError     Traceback (most recent call last)
<ipython-input-10-ed729e336796> in <module>()
----> 1 A.instance_method(1)

TypeError: instance_method() missing 1 required positional argument: 'x'

In [ ]:
A().instance_method(1)

In [ ]:
A.class_method(1)

In [ ]:
A.static_method(1)

4.2 Defining instance attributes

Instance attributes (methods and variables) are only accesible when the class has been instantiated. Of the instance methods, one of the most important is the constructor of the class. All instance methods must include a parameter which holds the data instantiated. By convention, this parameter is called self.


In [2]:
class A:
    def __init__(self):
        print('Constructor called')
        
    def setter(self, x=1):
        self.x = x
        
    def getter(self):
        return self.x
a = A()


Constructor called

In [3]:
a.setter(2)
print(a.getter())


2

In [7]:
hasattr(A, 'setter')


Out[7]:
True

A curiosity:

Objects store the instance variables in a dictionary:


In [ ]:
x.__dict__

We can create a new entry in the (class) dictionary (a new instance variable) with:


In [ ]:
x.b = 1
x.b

In [ ]:
x.__dict__

5. Inheritance

Extends functionality of a (previously defined) class.

5.1 Simple inheritance


In [44]:
class Class_A:
    '''Base class A'''
    
    def method_1(self):
        self.x = 1
        print('Class_A.method_1 called')
        
class Class_B(Class_A): # Notice that Class_B depends on (inherits from) Class_A
    '''Derived class B'''
    
    def method_1(self):
        print('Class_B.method_1 called')
    
    # This method extends "Class_A"
    def method_2(self):
        Class_A.x = 2 # "Class_A" is the prefix of "x" in "Class_B" for "Class_A.x"
        print('Class_B.method_2 called')
        Class_A.method_1(self)
        super().method_1() # This is identical to the previous
        self.method_1()

In [45]:
Class_A().method_1()


Class_A.method_1 called

In [46]:
Class_B().method_2()


Class_B.method_2 called
Class_A.method_1 called
Class_A.method_1 called
Class_B.method_1 called

In [ ]:
Class_B().method_1()

In [ ]:
print(Class_A.x)

5.2 Multiple inheritance


In [ ]:
class Class_A:
    '''Base class A'''
    def method_1(self):
        print('Class_A.method_1 called')
        
class Class_B:
    '''Base class B'''
    def method_1(self):
        print('Class_B.method_1 called')
        
class Class_C(Class_A, Class_B): # Class_C inherits from Class_A and Class_B
    '''Derived class C'''
    def method_1(self):
        Class_A.method_1(self)
        Class_B.method_1(self)

In [ ]:
Class_C().method_1()

In [ ]:
class Point():
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __add__(self, point):
        '''Overloads "+"'''
        return Point(self.x + point.x, self.y + point.y)
    def __str__(self):
        '''Overloads print()'''
        return '(' + str(self.x) + ',' + str(self.y) + ')'

In [ ]:
a = Point(1,2)
b = Point(3,4)
print(a+b)

7. "Abstract" methods

Python does not define a special syntax for declaring methods that should be implemented in a child class. However, an "abstract" method can be implemented in the "abstract" class as:


In [ ]:
class A():
    def do(self):
        raise NotImplementedError
        
x = A()
x.do()

In [ ]:
class B(A):
    def do(self):
        print('Do something useful')
        
x = B()
x.do()

Example


In [ ]:
class Stack:
     def __init__(self):
         self.items = []

     def put(self, item):
         self.items.append(item)

     def get(self):
         return self.items.pop()

     def size(self):
         return len(self.items)

In [ ]:
class Queue:
    def __init__(self):
        self.items = []

    def put(self, item):
        self.items.insert(0,item)

    def get(self):
        return self.items.pop()

    def size(self):
        return len(self.items)

Use the OPP paradigm to create a shorter implementation of both data structures.