Wednesday 16 October 2019

Unit –V Classes and OOP


Introduction, Object-oriented Programming, Classes, Class Attributes, Instances, Instance
Attributes, Binding and Method Invocation, Composition, Sub-classing and Derivation
Inheritance, Built-in Functions for Classes, Instances, and Other Objects.
================================================
Introduction to Object-oriented Programming:
Object-oriented Programming, is a programming paradigm which provides a means of structuring programs so that properties and behaviors are bundled into individual objects.
For example, an object could represent a person with a name property, age, address, etc., with behaviors like walking, talking, breathing, and running. Or an email with properties like recipient list, subject, body, etc., and behaviors like adding attachments and sending.
Classes provide the definitions of such objects, and instances are realizations of such definitions. Both are vital components for object-oriented design (OOD), which simply means to build your system architected in an object-oriented fashion.
One of the most important reasons to consider working in OOD is that it provides a direct approach to modeling and solving real-world problems and situations. For example, let us attempt to model an automobile mechanic shop where you would take your car in for repair. There are two general entities we would have to create: humans who interact with and in such a "system," and a physical location for the activities that define a mechanic shop.
A class called Person would be created to represent all humans involved in such an activity. Instances of Person would include the Customer, the Mechanic, and perhaps the Cashier. Each of these instances would have similar as well as unique behaviors.
For example, all would have the talk() method as a means of vocal communication as well as a drive_car() method. Only the Mechanic would have the repair_car() method and only the Cashier would have a ring_sale() method. The Mechanic will have a repair_certification attribute while all Persons would have a drivers_license attribute.
Finally, all of these instances would be participants in one overseeing class, called the RepairShop, which would have operating_hours, a data attribute that accesses time functionality to determine when Customers can bring in their vehicles and when Employees such as Mechanics and Cashiers show up for work. The RepairShop might also have a AutoBay class that would have instances such as SmogZone, TireBrakeZone, and perhaps one called GeneralRepair.
The point of our fictitious RepairShop is to show one example of how classes and instances plus their behaviors can be used to model a true-to-life scenario. You can probably also imagine classes such as an Airport, a Restaurant, a ChipFabPlant, a Hospital, or even a MailOrderMusic business, all complete with their own participants and functionality.
Classes:
A class is a data structure that we can use to define objects that hold together data values and behavioral characteristics. Classes are entities that are the programmatic form of an abstraction for a real-world problem, and instances are realizations of such objects. One analogy is to liken classes to blueprints or molds with which to make real objects (instances).
The term most likely originates from using classes to identify and categorize biological families of species to which specific creatures belong and can be derived into similar yet distinct subclasses. Many of these features apply to the concept of classes in programming.
In Python, class declarations are very similar to function declarations, a header line with the appropriate keyword followed by a suite as its definition, as indicated below:
def functionName(args):
'function documentation string'
function_suite
class ClassName(object):
'class documentation string'
class_suite
Both allow you to create functions within their declaration, closures or inner functions for functions within functions, and methods for functions defined in classes. The biggest difference is that you run functions but create objects with classes.
A class is like a Python container type on steroids. In this section, we will take a close look at classes and what types of attributes they have. Just remember to keep in mind that even though classes are objects (everything in Python is an object), they are not realizations of the objects they are defining.
When you create a class, you are practically creating your own kind of data type. All instances of that class are similar, but classes differ from one another. Classes also allow for derivation. You can create subclasses that are classes but inherit all of the features and attributes of the "parent" class.

Creating Classes:
Python classes are created using the class keyword. In the simple form of class declarations, the name of the class immediately follows the keyword:
class ClassName(bases):
class documentation string'
class_suite
bases is the set of one or more parent classes from which to derive and class_suite consists of all the component statements, defining class members, data attributes, and functions. Classes are generally defined at the top-level of a module so that instances of a class can be created anywhere in a piece of source code where the class is defined.
The __init__() function:
All classes have a function called __init__(), which is always executed when the class is being initiated.
Use the __init__() function to assign values to object properties, or other operations that are necessary to do when the object is being created:
class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age

p1 = Person("John"36)

print(p1.name)
print(p1.age)
The self Parameter:
The self parameter is a reference to the current instance of the class, and is used to access variables that belongs to the class.

Class attributes:
Class attributes belong to the class itself they will be shared by all the instances. Such attributes are defined in the class body parts usually at the top.
class sampleclass:
            count = 0         # class attribute
            def increase(self):
                        sampleclass.count += 1
# Calling increase() on an object
s1 = sampleclass()
s1.increase()               
print (s1.count )
# Calling increase on one more
# object
s2 = sampleclass()
s2.increase()
print (s2.count )
print (sampleclass.count )
Output:
1             
2                          
2

Instance Attributes:
Unlike class attributes, instance attributes are not shared by objects. Every object has its own copy of the instance attribute. (In case of class attributes all object refer to single copy).
To list the attributes of an instance/object, we have two functions:-
1. vars()- This function displays the attribute of an instance in the form of an dictionary.
2. dir()- This function displays more attributes than vars function, as it is not limited to instance. It displays the class attributes as well.

class sampleclass:
    def __init__(self):
        self.count = 0
    def increase(self):
        self.count += 1

# Calling increase on one more object
s1 = sampleclass()
s1.increase()
print (s1.count )
s1.increase()
print (s1.count )
s2 = sampleclass()
s2.increase()
print (s2.count )


>>> print("Dictionary From:",vars(s1))
Dictionary From: {'count': 3}
>>> print(dir(s1))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'count', 'increase']
Difference between Class Attribute and Instance Attribute:
·         An instance attribute is a Python variable belonging to one, and only one, object. This variable is only accessible in the scope of this object and it is defined inside the constructor function, __init__(self,..) of the class.
·         A class attribute is a Python variable that belongs to a class rather than a particular object. It is shared between all the objects of this class and it is defined outside the constructor function, __init__(self,...), of the class.

Binding and Method Invocation:
A method is simply a function defined as part of a class. This means that methods are class attributes. Methods can be called only when there is an instance of the class upon which the method was invoked.
When there is an instance present, the method is considered bound (to that instance). Without an instance, a method is considered unbound, and finally, the first argument in any method definition is the variable self, which represents the instance object invoking the method.
The variable self is used in class instance methods to reference the instance to which the method is bound. Because a method's instance is always passed as the first argument in any method call, self is the name that was chosen to represent the instance. You are required to put self in the method declaration (you may have noticed this already) but do not need to actually use the instance (self) within the method.
If you do not use self in your method, you might consider creating a regular function instead, unless you have a particular reason not to. After all, your code, because it does not use the instance object in any way, "unlinks" its functionality from the class, making it seem more like a general function.
Invoking Bound Methods:
Methods, whether bound or not, are made up of the same code. The only difference is whether there is an instance present so that the method can be invoked. In most cases, you the programmer will be calling a bound method. Let us say that you have a class MyClass and an instance of it called mc, and you want to call the MyClass.foo() method. Since you already have an instance, you can just call the method with mc.foo(). Recall that self is required to be declared as the first argument in every method declaration. Well, when you call a bound method, self never needs to be passed explicitly when you invoke it with an instance. That is your bonus for being "required" to declare self as the first argument.
The only time when you have to pass it in is when you do not have an instance and need to call a method unbound.
Invoking Unbound Methods:
Calling an unbound method happens less frequently. The main use case for calling a method belonging to a class that you do not have an instance for is the case where you are deriving a child class and override a parent method where you need to call the parent's constructor you are overriding.

Composition:
In composition a class is created with one or more instances of another class. The class which is having instances of another class is called as container where as the other class whose instances are created is known as content class.
Example: Class Employee is container and class Salary is content.
class Salary:  #content class
    def __init__(self, pay):
        self.pay = pay

    def get_total(self):
        return (self.pay*12)
class Employee: # container class
    def __init__(self, pay, bonus):
        self.pay = pay
        self.bonus = bonus
        self.obj_salary = Salary(self.pay)

    def annual_salary(self):
        return "Total: " + str(self.obj_salary.get_total() + self.bonus)

obj_emp = Employee(6, 5)
print(obj_emp.annual_salary())



Subclassing, Derivation and Inheritance:
Composition works fine when classes are distinct and are a required component of larger classes, but when you want "the same class but with some modification," derivation is a more logical option.
One of the more powerful aspects of OOP is the ability to take an already defined class and extend it or make modifications to it without affecting other pieces of code in the system that use the currently existing classes.
OOD allows for class features to be inherited by child classes or subclasses.
These subclasses derive the core of their attributes from base (super) classes.
In addition, this derivation may be extended for multiple generations. Classes involved in a one-level derivation (or that are adjacent vertically in a class tree diagram) have a parent and child class relationship. Those classes that derive from the same parent (or that are adjacent horizontally in a class tree diagram) have a sibling relationship. Parent and all higher-level classes are considered ancestors.

Creating Subclasses:
The syntax for creating a subclass looks just like that for a regular class, a class name followed by one or more parent classes to inherit from:
classSubClassName (ParentClass1[, ParentClass2, ...]):
'optional class documentation string'
class_suite
If your class does not derive from any ancestor class, use object as the name of the parent class.


Example:

class Person(object):
     
    def __init__(self, name):
        self.name = name
 
    def getName(self):
        return self.name

    def isEmployee(self):
        return False
 
class Employee(Person):
 
    def isEmployee(self):
        return True
 
emp = Person("Raju")  # An Object of Person
print(emp.getName(), emp.isEmployee())
 
emp = Employee("Sanju") # An Object of Employee
print(emp.getName(), emp.isEmployee())


issubclass():
Python provides a function issubclass() that directly tells us if a class is subclass of another class.
class Base(object):
            pass # Empty Class
class Derived(Base):
            pass # Empty Class
# Driver Code
print(issubclass(Derived, Base))
print(issubclass(Base, Derived))
d = Derived()
b = Base()
# b is not an instance of Derived
print(isinstance(b, Derived))
# But d is an instance of Base
print(isinstance(d, Base))

In Python, there are two types of Inheritance:
  1. Multiple Inheritance
  2. Multilevel Inheritance 

Multiple inheritance:
Python supports multiple inheritance. We specify all parent classes as comma separated list in bracket.
Multiple Inheritance means that you're inheriting the property of multiple classes into one. In case you have two classes, say A and B, and you want to create a new class which inherits the properties of both A and B, then:
class A:
    # variable of class A
    # functions of class A
class B:
    # variable of class A
    # functions of class A
class C(A, B):
    # class C inheriting property of both class A and B
    # add more properties to class C

So just like a child inherits characteristics from both mother and father, in python, we can inherit multiple classes in a single child class.

Example:
class Base1(object):
            def __init__(self):
                        self.name1 = "Raju"
                        print ("From Base1 i am Raju ")
class Base2(object):
            def __init__(self):
                        self.name2 = "Sanju"              
                        print ("From Base2 i am Sanju")
class Derived(Base1, Base2):
            def __init__(self):
                        # Calling constructors of Base1 and Base2 classes
                        Base1.__init__(self)
                        Base2.__init__(self)
                        print ("Derived")
            def printStrs(self):
                        print(self.name1, self.name2)             
ob = Derived()
ob.printStrs()


Multilevel Inheritance:
In multilevel inheritance, we inherit the classes at multiple separate levels. We have three classes A, B and C, where A is the super class, B is its sub(child) class and C is the sub class of B.

class A:
    # properties of class A
class B(A):
    # class B inheriting property of class A
    # more properties of class B
class C(B):
    # class C inheriting property of class B
    # thus, class C also inherits properties of class A
    # more properties of class C

Accessing parent members in a subclass:
We can access parent members in a subclass using two methods
1.  Using Parent class name
2.    Using function super()

Using Parent class name:
Base Class members can be accessed in derived class using base class name.
class Base(object):
    def __init__(self, x):
        self.x = x
class Derived(Base):
    def __init__(self, x, y):
        Base.x = x
        self.y = y
    def printXY(self):
        print(Base.x, self.y)

d = Derived(10, 20)
d.printXY()

Using function super():
Base class members can be accessed in derived class using super().
class Base(object):
    def __init__(self, x):
        self.x = x
class Derived(Base):
    def __init__(self, x, y):
        super(Derived, self).__init__(x)
        self.y = y
    def printXY(self):
        # Here Base.x won't work here because super() is used in constructor
        print(self.x, self.y)
d = Derived(10, 20)
d.printXY()

Built-in Functions for Classes, Instances, and Other Objects:
issubclass():
The issubclass() function returns True if the specified object is a subclass of the specified object, otherwise False.
Syntax:
issubclass(object, subclass)
Example:Check if the class myObj is a subclass of myAge:

class myAge:
  age = 36
class myObj(myAge):
  name = "John"
  age = myAge
x = issubclass(myObj, myAge)

isinstance():
The isinstance() function checks if the object (first argument) is an instance or subclass of classinfo class (second argument).
Syntax:
isinstance(object, class)
Example: 

class myObj:
  name = "John"
y = myObj()
x = isinstance(y, myObj)

hasattr():
The hasattr() function returns True if the specified object has the specified attribute, otherwise False.
Syntax:
hasattr(object, attribute)
Example: Check if the "Person" object has the "age" property:
class Person:
  name = "John"
  age = 36
  country = "Norway"

x = hasattr(Person, 'age')


getattr():
The getattr() function returns the value of the specified attribute from the specified object.
Syntax:
getattr(object, attribute, default)
Example: Use the "default" parameter to write a message when the attribute does not exist:
class Person:
  name = "John"
  age = 36
  country = "Norway"
x = getattr(Person, 'page', 'my message')

setattr():
The setattr() function sets the value of the specified attribute of the specified object.
Syntax:
setattr(object, attribute, value)
Example :Change the value of the "age" property of the "person" object:

class Person:
  name = "John"
  age = 36
  country = "Norway"
setattr(Person, 'age', 40)

delattr():

The delattr() function will delete the specified attribute from the specified object.
Syntax:
delattr(object, attribute)
Example: Delete the "age" property from the "person" object:

class Person:
  name = "John"
  age = 36
  country = "Norway"
delattr(Person, 'age')

dir():
The dir() function returns all properties and methods of the specified object, without the values.
This function will return all the properties and methods, even built-in properties which are default for all object.
Syntax:
dir(object)
Example: Display the content of an object:

class Person:
  name = "John"
  age = 36
  country = "Norway"
print(dir(Person))

super():
The super() function is used to give access to methods and properties of a parent or sibling class.
The super() function returns an object that represents the parent class.
Syntax
super()
Example: Create a class that will inherit all the methods and properties from another class:

class Parent:
  def __init__(self, txt):
    self.message = txt
  def printmessage(self):
    print(self.message)
class Child(Parent):
  def __init__(self, txt):
    super().__init__(txt)
x = Child("Hello, and welcome!")
x.printmessage()

vars():
The vars() function returns the __dic__ attribute of an object.
The __dict__ attribute is a dictionary containing the object's changeable attributes.
Calling the vars() function without parameters will return a dictionary containing the local symbol table.
Syntax
vars(object)
Example: Return the __dict__ atribute of an object called Person:

class Person:
  name = "John"
  age = 36
  country = "norway"
x = vars(Person)