---
# System prepended metadata

title: 'OOPS 2 - Dunders, Inheritence'

---

# OOPS 2 - Dunders, Inheritance Continued

---
title: Agenda
description:
duration: 300
card_type: cue_card
---

### Agenda

* Dunder / Magic methods
* Inheritance
* Private Properties
* Multiple Inheritance
* Method Resolution Order

---
title: Quiz-1
description: Quiz-1
duration: 60
card_type: quiz_card
---

# Question

How many objects would be created by runing the following code?

Code
```python=
class A:
    print("Am I a class?")

A()

obj1 = A()

if A():
    print(A())
```

# Choices

- [x] 4
- [ ] 2
- [ ] 3
- [ ] 0

---
title: Dunder / Magic methods
description:
duration: 1200
card_type: cue_card
---

## Dunder / Magic methods

* These are special methods that have double underscores at the beginning and end of their names (e.g., __init__). 
* These methods provide a way for a class to define its behavior. 
* By modifying these methods, you can customize the default behavior of your objects.

Code
```python=
class Car:
    def __init__(self, name, mileage):
        self.name = name
        self.mileage = mileage
```

Code
```python=
c1 = Car("Nexon", 12)
c2 = Car("Altroz", 15)
```

Code
```python=
print(c1)
# Name -> Mileage
```

> Output

    <__main__.Car object at 0x7bd1cb5cc880>

Whenever call the `print()` function over some object then there should always be a pre-defined message linked to that object that gets printed. 

Hence, printing is a behaviour in Python.

With dunder/magic methods, we can modify this behaviour.

Code
```python=
class Car:
    def __init__(self, name, mileage):
        self.name = name
        self.mileage = mileage

    # Whenever an object is printed, __str__() function is called.
    def __str__(self):
        return f"{self.name} -> {self.mileage}"

    def __add__(self, other):
        return self.mileage + other.mileage

    def __lt__(self, other):
        return self.mileage < other.mileage

    def __call__(self):
        print("I WAS CALLED")
```

Code
```python=
c1 = Car("Nexon", 12)
c2 = Car("Altroz", 15)
```

Code
```python=
print(c1)
```

> Output

    Nexon -> 12

Code
```python=
c1 + c2
# Car.__add__(c1, c2)
```

> Output

    27

---
title: Quiz-2
description: Quiz-2
duration: 60
card_type: quiz_card
---

# Question

What will be the output of the following code?

Code
```python=
class A:
  def __init__(self, a):
    self.a = a
  def __add__(self, b):
    if isinstance(b, int):
      return A(self.a + b)
    return A(self.a + b.a)
  def __str__(self):
    return f"{self.a}"


one = A(1)
two = A(2)

print(one + two + 100)
```

# Choices

- [x] 103
- [ ] 3
- [ ] Error
- [ ] None of the above

---
title: Inheritance
description:
duration: 1200
card_type: cue_card
---

## Inheritance

<img src="https://d2beiqkhq929f0.cloudfront.net/public_assets/assets/000/015/907/original/Screenshot_2022-10-07_at_10.04.40_AM.png?1665117262">

\
Code
```python=
class Parent:
  def __init__(self):
    print("parent class")

class Child(Parent): # inheriting parent class
  def __init__(self):
    print("child class")
```

Code
```python=
class SchoolMember:
    def __init__(self, name):
        self.name = name

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

class Staff(SchoolMember):
    def __init__(self, name, salary):
        self.salary = salary

class Teacher(Staff):
    def __init__(self, name, salary, subject):
        self.subject = subject
```

<img src="https://d2beiqkhq929f0.cloudfront.net/public_assets/assets/000/015/908/original/Screenshot_2022-10-07_at_10.05.56_AM.png?1665117338">

\
Code
```python=
s1 = Student("Krishna", "A")

type(s1)
```

> Output

    __main__.Student

Code
```python=
s1.grade
```

> Output

    'A'

Code
```python=
s1.name # class inherited but still can't use the properties
```

> Output

    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-17-58cbae705711> in <cell line: 1>()
    ----> 1 s1.name # class inherited still cannot use the properties

    AttributeError: 'Student' object has no attribute 'name'

#### The problem is that we never called the `__init__()` function of the parent class inside the child class.

#### We can use the `super()` method to directly call the methods of parent class.

Code
```python=
class SchoolMember:
    def __init__(self, name):
        self.name = name

class Student(SchoolMember):
    def __init__(self, name, grade):
        super().__init__(name)
        # super() automatically corresponds to the direct parent class
        self.grade = grade

class Staff(SchoolMember):
    def __init__(self, name, salary):
        super().__init__(name)
        self.salary = salary

class Teacher(Staff):
    def __init__(self, name, salary, subject):
        super().__init__(name, salary)
        self.subject = subject
```

Code
```python=
s1 = Student("Sharath", "A")
print(s1.name)
```

> Output

    Sharath

Code
```python=
s1.name = "Random"
print(s1.name)
```

> Output

    Random

Code
```python=
t1 = Teacher("Bipin", 10, "Python")

t1.name, t1.salary, t1.subject
```

> Output

    ('Bipin', 10, 'Python')

---
title: Quiz-3
description: Quiz-3
duration: 60
card_type: quiz_card
---

# Question

What would be the output of the following?

Code
```python=
class A:
    def do_something(self):
        print("I do something!", end=" ")

class B(A):
    def do_something(self):
        super().do_something()
        print("I also do something!", end=" ")

b = B()
b.do_something()
```

# Choices

- [ ] I also do something!
- [ ] I also do something! I do something!
- [x] I do something! I also do something!

---
title: Need of private properties
description:
duration: 600
card_type: cue_card
---

## Need of private properties -

Code
```python=
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdrawl(self, amount):
        self.balance -= amount

    def deposit(self, amount):
        self.balance += amount
```

Code
```python=
b1 = BankAccount(10000)
```





Code
```python=
b1.deposit(5000)

b1.balance
```

> Output

    15000

Code
```python=
b1.withdrawl(15000)

b1.balance
```

> Output

    0

Code
```python=
b1.balance = 23456789854323456789987654345678909876543

b1.balance
```

> Output

    23456789854323456789987654345678909876543

Just like that, any property within the instance can be modified to anything.

---
title: Break & Doubt Resolution
description:
duration: 600
card_type: cue_card
---

### Break & Doubt Resolution

`Instructor Note:`
* Kindly take this time (up to 5-10 mins) to give a short break to the learners.
* Meanwhile, you can ask the them to share their doubts (if any) regarding the topics covered so far.

---
title: Private Properties
description:
duration: 1200
card_type: cue_card
---

## Private Properties

- will be accessible within the class.
- won't be accessible outside the class definition.

Code
```python=
class BankAccount:
    def __init__(self, balance):
        # initializing private property
        self.__balance = balance

    # Setter
    def withdraw(self, amount):
        self.__balance -= amount

    def deposit(self, amount):
        self.__balance += amount

    # Getter
    def check_balance(self):
        return self.__balance
```

Code
```python=
b1 = BankAccount(10000)

# cannot be accessed like this
b1.__balance
```

> Output

    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-34-88d3b8138d4e> in <cell line: 2>()
          1 # cannot be accessed like this
    ----> 2 b1.__balance

    AttributeError: 'BankAccount' object has no attribute '__balance'

Code
```python=
b1.deposit(10000)

b1.withdraw(5000)

b1.check_balance()
```

> Output

    25000

#### Nothing can be completely *private* in Python.

When we create variables like `__private_var`, Python internally creates it with name of `_ClassName__private_var`. 

Hence, we can still access it with the above name.

<img src="https://d2beiqkhq929f0.cloudfront.net/public_assets/assets/000/015/909/original/Screenshot_2022-10-07_at_10.18.54_AM.png?1665118116">

\
Code
```python=
b1._BankAccount__balance = 159854568765456789876545678765678
# Name Mangling

b1.check_balance()
```

> Output

    159854568765456789876545678765678

Code
```python=
# Checking the properties and methods within any object -
dir(b1)
```

> Output

    ['_BankAccount__balance',
     '__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__',
     'check_balance',
     'deposit',
     'withdraw']

Code
```python=
b1.__balance = "random balance"

dir(b1)
```

> Output

    ['_BankAccount__balance',
     '__balance',
     '__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__',
     'check_balance',
     'deposit',
     'withdraw']

---
title: Multiple Inheritance
description:
duration: 900
card_type: cue_card
---

## Multiple Inheritance

- Inheriting more than one class.

Code
```python=
class A:
    def __init__(self, a):
        self.a = a

class B:
    def __init__(self, b):
        self.b = b

# C inherits from both A and B
class C(A, B):
    def __init__(self, a, b, c):
        # Assumption 1
        # super().__init__(a, b)

        # Assumption 2
        # super().__init__(a)
        # super().__init__(b)

        # Assumption 3
        A.__init__(self, a)
        B.__init__(self, b)
        self.c = c
```

Code
```python=
c1 = C(1,2,3)

c1.c
```

> Output

    3

---
title: Method Resolution Order
description:
duration: 1200
card_type: cue_card
---

## Method Resolution Order

<img src="https://d2beiqkhq929f0.cloudfront.net/public_assets/assets/000/015/910/original/Screenshot_2022-10-07_at_10.31.02_AM.png?1665118892">

\
Code
```python=
class A:
    x = 10

class B(A):
    pass

class C(B):
    pass

class D(A):
    x = 5

class E(C, D):
    pass

# E -> C -> B -> D -> A
```

Code
```python=
e1 = E()
e1.x
```

> Output

    5

Code
```python=
# It'll give the method resolution order
E.__mro__
```

> Output

    (__main__.E, __main__.C, __main__.B, __main__.D, __main__.A, object)

---
title: Quiz-4
description: Quiz-4
duration: 60
card_type: quiz_card
---

# Question

Select the correct Method Resolution Order for class `E`.

Code
```python=
class A:
  pass
class B:
  pass
class C(A, B):
  pass
class D:
  pass
class E(C, D):
  pass
```

# Choices

- [x] E -> C -> A -> B -> D
- [ ] E -> D -> C -> B -> A
- [ ] E -> A -> B -> D -> C
- [ ] None of the above

---
title: Practice Coding Question(s)
description: 
duration: 600
card_type: cue_card
---

### Practice Coding Question(s)

You can pick the following question and solve it during the lecture itself.

This will help the learners to get familiar with the problem solving process and motivate them to solve the assignments.

<span style="background-color: pink;">Make sure to start the doubt session before you start solving the question.</span>

Q. https://www.scaler.com/hire/test/problem/96310/