<jupyter_start><jupyter_text>All the IPython Notebooks in this lecture series by Dr. <NAME> are available @ **[GitHub](https://github.com/milaan9/06_Python_Object_Class)**  Python OOPs ConceptsIn this class, you’ll learn about Object-Oriented Programming (OOP) in Python and its fundamental concept with the help of examples.  Object Oriented ProgrammingPython is a multi-paradigm programming language. It supports different programming approaches.One of the popular approaches to solve a programming problem is by creating objects. This is known as Object-Oriented Programming (OOP).Object-oriented programming (OOP) is a programming paradigm based on the concept of **“objects”**. The object contains both data and code: Data in the form of properties (often known as attributes), and code, in the form of methods (actions object can perform).An object has two characteristics:* attributes* behaviorFor example:A parrot is can be an object,as it has the following properties:* name, age, color as attributes* singing, dancing as behaviorThe concept of OOP in Python focuses on creating reusable code. This concept is also known as DRY (Don't Repeat Yourself).In Python, the concept of OOP follows some basic principles:  Class In Python, everything is an object. A class is a blueprint for the object. To create an object we require a model or plan or blueprint which is nothing but class.**Class represents the properties (attribute) and action (behavior) of the object. Properties represent variables, and actions are represented by the methods. Hence class contains both variables and methods.**We can think of class as a sketch of a parrot with labels. It contains all the details about the name, colors, size etc. Based on these descriptions, we can study about the parrot. Here, a parrot is an object.**Syntax:**```pythonclass classname:    '''documentation string'''    class_suite```* **Documentation string:** represent a description of the class. It is optional.* **class_suite:** class suite contains component statements, variables, methods, functions, attributes.The example for class of parrot can be :```pythonclass Parrot:    pass```Here, we use the **`class`** keyword to define an empty class **`Parrot`**. From class, we construct instances. An instance is a specific object created from a particular class.  Object The physical existence of a class is nothing but an object. In other words, the object is an entity that has a state and behavior. Therefore, an object (instance) is an instantiation of a class. So, when class is defined, only the description for the object is defined. Therefore, no memory or storage is allocated.**Syntax:**```pytonreference_variable = classname()```The example for object of parrot class can be:```pythonobj = Parrot()```Here, **`obj`** is an **`object`** of class Parrot.Suppose we have details of parrots. Now, we are going to show how to build the class and objects of parrots.<jupyter_code># Example 1: Creating Class and Object in Python

class Student:
    """This is student class with data"""    
    def learn(self):    # A sample method
        print("Welcome to Dr. <NAME>'s class on Python Programming")

stud = Student()        # creating object
stud.learn()            # Calling method

# Output: Welcome to Dr. M<NAME>'s class on Python Programming<jupyter_output>Welcome to Dr. <NAME>'s class on Python Programming<jupyter_text>Instance Variables and MethodsIf the value of a variable varies from object to object, then such variables are called instance variables. For every object, a separate copy of the instance variable will be created.When we create classes in Python, instance methods are used regularly. we need to create an object to execute the block of code or action defined in the instance method.We can access the instance variable and methods using the object. Use dot (**`.`**) operator to access instance variables and methods.In Python, working with an instance variable and method, we use the **`self`** keyword. When we use the **`self`** keyword as a parameter to a method or with a variable name is called the instance itself.>**Note:** Instance variables are used within the instance method<jupyter_code># Example 2: Creating Class and Object in Python

class Student:
    def __init__(self, name, percentage):
        self.name = name
        self.percentage = percentage

    def show(self):
        print("Name is:", self.name, "and percentage is:", self.percentage)

        
stud = Student("Arthur", 90)
stud.show()

# Output Name is: Arthur and percentage is: 90<jupyter_output>Name is: Arthur and percentage is: 90<jupyter_text><jupyter_code># Example 3: Creating Class and Object in Python

class Parrot:
    species = "bird"                    # class attribute
    def __init__(self, name, age):      # instance attribute
        self.name = name
        self.age = age

# instantiate the Parrot class
blu = Parrot("Blu", 10)
woo = Parrot("Woo", 15)

# access the class attributes
print("Blu is a {}".format(blu.__class__.species))
print("Woo is also a {}".format(woo.__class__.species))

# access the instance attributes
print("{} is {} years old".format( blu.name, blu.age))
print("{} is {} years old".format( woo.name, woo.age))<jupyter_output>Blu is a bird
Woo is also a bird
Blu is 10 years old
Woo is 15 years old<jupyter_text>**Explanation**:In the above program, we created a class with the name **`Parrot`**. Then, we define attributes. The attributes are a characteristic of an object.These attributes are defined inside the **`__init__`** method of the class. It is the initializer method that is first run as soon as the object is created.Then, we create instances of the **`Parrot`** class. Here, **`blu`** and **`woo`** are references (value) to our new objects.We can access the class attribute using **`__class__.species`**. Class attributes are the same for all instances of a class. Similarly, we access the instance attributes using **`blu.name`** and **`blu.age`**. However, instance attributes are different for every instance of a class.To learn more about classes and objects, go to [**Python Classes and Objects**](https://github.com/milaan9/06_Python_Object_Class/blob/main/002_Python_Classes_and_Objects.ipynb)  MethodMethods are functions defined inside the body of a class. They are used to define the behaviors of an object.<jupyter_code># Example 1: Creating Methods in Python

class Parrot:
    
    # instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    # instance method
    def sing(self, song):
        return "{} sings {}".format(self.name, song)

    def dance(self):
        return "{} is now dancing".format(self.name)

# instantiate the object
blu = Parrot("Blu", 10)

# call our instance methods
print(blu.sing("'Happy'"))
print(blu.dance())<jupyter_output>Blu sings 'Happy'
Blu is now dancing<jupyter_text>**Explanation**:In the above program, we define two methods i.e **`sing()`** and **`dance()`**. These are called instance methods because they are called on an instance object i.e **`blu`**.  Encapsulationn Python, encapsulation is a method of wrapping data and functions into a single entity. For example, A class encapsulates all the data (methods and variables). Encapsulation means the internal representation of an object is generally hidden from outside of the object’s definition.Using OOP in Python, we can restrict access to methods and variables. This prevents data from direct modification which is called encapsulation. In Python, we denote private attributes using underscore as the prefix i.e single **`_`** or double **`__`**. Need of EncapsulationEncapsulation acts as a protective layer. We can restrict access to methods and variables from outside, and It can prevent the data from being modified by accidental or unauthorized modification. Encapsulation provides security by hiding the data from the outside world.In Python, we do not have access modifiers directly, such as public, private, and protected. But we can achieve encapsulation by using single prefix underscore and double underscore to control access of variable and method within the Python program.<jupyter_code># Example 1: Data Encapsulation in Python

class Computer:

    def __init__(self):
        self.__maxprice = 900

    def sell(self):
        print("Selling Price: {}".format(self.__maxprice))

    def setMaxPrice(self, price):
        self.__maxprice = price

c = Computer()
c.sell()

# change the price
c.__maxprice = 1000
c.sell()

# using setter function
c.setMaxPrice(1000)
c.sell()<jupyter_output>Selling Price: 900
Selling Price: 900
Selling Price: 1000<jupyter_text>**Explanation**:In the above program, we defined a **`Computer`** class.We used **`__init__()`** method to store the maximum selling price of **`Computer`**. We tried to modify the price. However, we can't change it because Python treats the **`__maxprice`** as private attributes.As shown, to change the value, we have to use a setter function i.e **`setMaxPrice()`** which takes price as a parameter.<jupyter_code># Example 2: Data Encapsulation in Python

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary

    def show(self):
        print("Name is ", self.name, "and salary is", self.__salary)

# Outside class
E = Employee("Bella", 60000)
E.PrintName()
print(E.name)
print(E.PrintName())
print(E.__salary)

# AttributeError: 'Employee' object has no attribute '__salary'<jupyter_output><empty_output><jupyter_text>**Explanation**:In the above example, we create a class called **`Employee`**. Within that class, we declare two variables **`name`** and **`__salary`**. We can observe that the name variable is accessible, but **`__salary`** is the **private variable**. We cannot access it from outside of class. If we try to access it, we will get an error.  PolymorphismPolymorphism is based on the greek words **Poly** (many) and **morphism** (forms). We will create a structure that can take or use many forms of objects.Polymorphism is an ability (in OOP) to use a common interface for multiple forms (data types).**Example 1:** The student can act as a student in college, act as a player on the ground, and as a daughter/brother in the home. **Example 2:** In the programming language, the **`+`** operator, acts as a concatenation and arithmetic addition.**Example 3:** If we need to color a shape, there are multiple shape options (rectangle, square, circle). However we could use the same method to color any shape. In Python, polymorphism allows us to define the child class methods with the same name as defined in the parent class.<jupyter_code># Example 1: Using Polymorphism in Python

class Parrot:

    def fly(self):
        print("Parrot can fly")
    
    def swim(self):
        print("Parrot can't swim")

class Penguin:

    def fly(self):
        print("Penguin can't fly")
    
    def swim(self):
        print("Penguin can swim")

# common interface
def flying_test(bird):
    bird.fly()

#instantiate objects
blu = Parrot()
peggy = Penguin()

# passing the object
flying_test(blu)
flying_test(peggy)<jupyter_output>Parrot can fly
Penguin can't fly<jupyter_text>**Exaplanation**:In the above program, we defined two classes **`Parrot`** and **`Penguin`**. Each of them have a common **`fly()`** method. However, their functions are different.To use polymorphism, we created a common interface i.e **`flying_test()`** function that takes any object and calls the object's **`fly()`** method. Thus, when we passed the **`blu`** and **`peggy`** objects in the **`flying_test()`** function, it ran effectively.<jupyter_code># Example 2: Using Polymorphism in Python

class Circle:
    pi = 3.14

    def __init__(self, redius):
        self.radius = redius

    def calculate_area(self):
        print("Area of circle:", self.pi * self.radius * self.radius)

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def calculate_area(self):
        print("Area of Rectangle:", self.length * self.width)

cir = Circle(9)
rect = Rectangle(9, 6)
cir.calculate_area()
# Output Area of circle: 254.34

rect.calculate_area()
# Output Area od Rectangle: 54<jupyter_output>Area of circle: 254.34
Area of Rectangle: 54<jupyter_text>**Exaplanation**:In the above example, we created two classes called **`Circle`** and **`Rectangle`**. In both classes, we created the same method with the name **`calculate_area`**. This method acts differently in both classes. In the case of the **`Circle`** class, it calculates the area of the circle, whereas, in the case of a **`Rectangle`** class, it calculates the area of a rectangle.  InheritanceIn an Object-oriented programming language, inheritance is an important aspect. The main purpose of inheritance is the reusability of code because we can use the existing class to create a new class instead of creating it from scratch.In Python, **[inheritance](https://github.com/milaan9/06_Python_Object_Class/blob/main/003_Python_Inheritance.ipynb)** is the process of inheriting the properties of the base class (or parent class) into a derived class (or child class).In inheritance, the child class acquires and access all the data members, properties, and functions from the parent class. Also, a child class can also provide its specific implementation to the functions of the parent class. Use of inheritanceTo create classes that are built upon existing classes called reusability. So, we do not have to write the same code again and again.**Syntax:**```pythonclass BaseClass:    Body of base classclass DerivedClass(BaseClass):    Body of derived class```<jupyter_code># Example 1: Use of Inheritance in Python

# parent class
class Bird:
    
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# child class
class Penguin(Bird):

    def __init__(self):
        # call super() function
        super().__init__()
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()
peggy.swim()
peggy.run()<jupyter_output>Bird is ready
Penguin is ready
Penguin
Swim faster
Run faster<jupyter_text>**Exaplanation**:In the above program, we created two classes i.e. **`Bird`** (parent class) and **`Penguin`** (child class). The child class inherits the functions of parent class. We can see this from the **`swim()`** method.Again, the child class modified the behavior of the parent class. We can see this from the **`whoisThis()`** method. Furthermore, we extend the functions of the parent class, by creating a new **`run()`** method.Additionally, we use the **`super()`** function inside the **`__init__()`** method. This allows us to run the **`__init__()`** method of the parent class inside the child class.<jupyter_code># Example 2: Use of Inheritance in Python

class ClassOne:              # Base class
    def func1(self):
        print('This is Parent class')

class ClassTwo(ClassOne):    # Derived class
    def func2(self):
        print('This is Child class')

obj = ClassTwo()
obj.func1()
obj.func2()<jupyter_output>This is Parent class
This is Child class
{'fraction_non_alphanumeric': 0.09323082936999152, 'fraction_numerical': 0.006124177470845006, 'mean_word_length': 4.389747191011236, 'pattern_counts': {'":': 0, '<': 36, '<?xml version=': 0, '>': 37, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '240670', 'license': 'MIT', 'n_tokens_mistral': 4299, 'n_tokens_neox': 4071, 'n_words': 1971, 'path': '001_Python_OOPs_Concepts.ipynb', 'repo_name': 'girishkumarjha/06_Python_Object_Class', 'size': '15375'}