Inheritance

        Inheritance is a way of creating a new class from an existing class.
Syntax:

Class Emp :                     [base class]
#code
.....
Class Pro (Emp) :          [derived/child class]
#code

    We can use the methods and attributes of Emp in Pro object.
Also, we can overwrite or add new attributes and methods i Pro class.

Types of Inheritance :

1. Single Inheritance
2. Multiple Inheritance
3. Multi level Interitance

Single Inheritance : occurs when child class inherits only a single parent class.
Base class -----> Derived Class

Multiple Inheritance :  occurs when the child class inherits from more thn on parent class

parent 1  ----------->
                            child
 parent2  ----------->
         
Multi level Inheritance : when a child class becomes a parent for another child class

parent --> child1 -->child2

super() method : is used to access the methods of a super class in the derived class.

super(). --init--()         [calls construction of the base class]

class methods :  A class method is a method which is bound to the class and not the object of the class.

@classmethod decorator is used to create a class method.

Syntax :
@classmethod
def(cls,p1,p2) :
....
@property decorators
consider the following class
Class Emp :
@property
def name(self) :
return self.ename

if e = Emp() is an object of class emp, we can print (ename) to print the ename/call name() function.

@getters and @setters : the method name with @property decorator is called getter method.

we can define a function + @name setter decorator like below:

@name.setter
def name(self, value) :
self. ename = value

Operator Overloading in Python : Operators in python xcan be overloaded using dunder methods. these methods are called when a given operator is used on the objets.
Operators in python can be overloaded using the follwoing methods :

p1 + p2 --> p1 --add-- (p2)
p1 - p2  --> p1 --sub-- (p2)
p1 * p2 --> p1 --mul-- (p2)
p1 / p2 --> p1 --truediv-- (p2)
p1 // p2 --> p1 --floordiv-- (p2)

other dunder /magic methods in python :

--str--() -> used to set what gets displayed upon calling str(obj)
--len--() -> used to set what gets displayed upon calling --len--() orlen (obj)

Programs :

1. #Inheritance

    class Employee:
        company = "Google"

        def showDetails(self):
            print("This is an employee")

    class Programmer(Employee):
        language = "Python"
        # company = "Youtube"

        def getLanguage(self):
            print(f"The language is {self.language}")
        
        def showDetails(self):
            print("This is an programmer")


    e = Employee()
    e.showDetails()
    p = Programmer()
    p.showDetails()
    print(p.company)

2. #single inheritance

class Employee:
    company = "Google"

    def showDetails(self):
        print("This is an employee")

class Programmer(Employee):
    language = "Python"
    # company = "Youtube"

    def getLanguage(self):
        print(f"The language is {self.language}")
    
    def showDetails(self):
        print("This is an programmer")


e = Employee()
e.showDetails()
p = Programmer()
p.showDetails()
print(p.company)

3. #Multiple Inheritance

class Freelancer:
    company = "Fiverr"
    level = 0

    def upgradeLevel(self):
        self.level = self.level + 1

class Employee:
    company = "Visa"
    eCode = 120


class Programmer(Freelancer, Employee):
    name = "Rohit"

p = Programmer()
p.upgradeLevel()
print(p.company)

4. #Multi Level Inheritance

class Person:
    country = "India"
    def takeBreath(self):
        print("I am breathing...")

class Employee(Person):
    company = "Honda"

    def getSalary(self):
        print(f"Salary is {self.salary}")
    
    def takeBreath(self):
        print("I am an Employee so I am luckily breathing..")

class Programmer(Employee):
    company = "Fiverr"
    
    def getSalary(self):
        print(f"No salary to programmers")
    
    def takeBreath(self):
        print("I am a Progarmmer so I am breathing++..")

p = Person()
p.takeBreath()
# print(p.company) # throws an error

e = Employee()
e.takeBreath()
print(e.company)

pr = Programmer()
pr.takeBreath()
print(pr.company)
print(pr.country)

5. #Super Class

class Person:
    country = "India"

    def __init__(self):
        print("Initializing Person...\n")

    def takeBreath(self):
        print("I am breathing...")

class Employee(Person):
    company = "Honda"

    def __init__(self):
        super().__init__()
        print("Initializing Employee...\n")

    def getSalary(self):
        print(f"Salary is {self.salary}")
    
    def takeBreath(self):
        super().takeBreath()
        print("I am an Employee so I am luckily breathing..")

class Programmer(Employee):
    company = "Fiverr"

    def __init__(self):
        # super().__init__()
        print("Initializing Programmer...\n")

    def getSalary(self):
        print(f"No salary to programmers")
    
    def takeBreath(self):
        super().takeBreath()
        print("I am a Progarmmer so I am breathing++..")

# p = Person()
# p.takeBreath() 

# e = Employee()
# e.takeBreath() 

pr = Programmer()
# pr.takeBreath() 

6. #class methods

class Employee:
    company = "Camel"
    salary = 100
    location = "Delhi"

    # def changeSalary(self, sal):
    #     self.__class__.salary = sal

    @classmethod
    def changeSalary(cls, sal):
        cls.salary = sal

e = Employee()
print(e.salary)
e.changeSalary(455)
print(e.salary)
print(Employee.salary)


7. #Property Decorator

class Employee:
    company = "Bharat Gas"
    salary = 5600
    salarybonus = 400
    # totalSalary = 6100

    @property
    def totalSalary(self):
        return self.salary + self.salarybonus

    @totalSalary.setter
    def totalSalary(self, val):
        self.salarybonus = val - self.salary

e = Employee()
print(e.totalSalary)
e.totalSalary = 5800
print(e.salary)
print(e.salarybonus)

8. #operator overloading

class Number:
    def __init__(self, num):
        self.num = num

    def __add__(self, num2):
        print("Lets add")
        return self.num + num2.num

    def __mul__(self, num2):
        print("Lets multiply")
        return self.num * num2.num

n1 = Number(4)
n2 = Number(6)
sum = n1 + n2
mul = n1 * n2
print(sum)
print(mul)

9. #dunder methods

class Number:
    def __init__(self, num):
        self.num = num

    def __add__(self, num2):
        print("Lets add")
        return self.num + num2.num

    def __mul__(self, num2):
        print("Lets multiply")
        return self.num * num2.num
    
    def __str__(self):
        return f"Decimal Number: {self.num}"
    
    def __len__(self):
        return 1

n = Number(9)
print(n)
print(len(n))