# CLASSES AND OBJECTS-Part-3

## Inheritance

- Inheritance is the capability of one class to derive or inherit the properties and attributes from another class.


- **Parent class** is the class being inherited from, also called base class or superclass (lets use the last one).


- **Child class** is the class that inherits from another class, also called derived class or subclass

- **Benefits of inheritance are:** 


- **Reusability**: We don’t have to write the same code again and again. Also, it allows us to add more features to a class without modifying it.
- **Transitive**: If class B inherits from another class A, then all the subclasses of B would automatically inherit from class A.

### Example-1
- Assume that we have written a class for straight lines


```python
#
class Line:
    def __init__(self, c0, c1):
        self.c0 = c0
        self.c1 = c1
    def __call__(self, x):
        return self.c0 + self.c1*x
    def table(self, L, R, n):
        """Return a table with n points for L <= x <= R."""
        s = ''
        import numpy as np
        for x in np.linspace(L, R, n):
            y = self(x)
            s += '%12g %12g\n'%(x, y)
        return s

L=Line(1,2)  # instance with c0=1 and c1=2
y=L(x=5)     # call to L1.__call__(5)
print(y)
print(L.table(0,1,3))

```

    11
               0            1
             0.5            2
               1            3
    


- Assume that we have written a class for parabolas


```python
class Parabola:
    def __init__(self, c0, c1, c2):
        self.c0 = c0
        self.c1 = c1
        self.c2 = c2
    def __call__(self, x):
        return self.c2*x**2 + self.c1*x + self.c0
    def table(self, L, R, n):
        """Return a table with n points for L <= x <= R."""
        s = ''
        import numpy as np
        for x in np.linspace(L, R, n):
            y = self(x)
            s += '%12g %12g\n' % (x, y)
        return s

p=Parabola(1,-1,2)  # instance with c0=1, c2=-1, and c3=2
p1=p(x=2.5)     # call to P1.__call__(2.5)
print(p1)
print(p.table(0,1,3))

```

    11.0
               0            1
             0.5            1
               1            2
    


## Extending vs Restricting Functionality
- Based in the mathematical relatonship between lines and parabolas we can use inheretance to save some code. For example, compare the class line and class Parabola, there is code which is similar (e.g., **def __ init __** and **def __ call __** ) and code which is exactlt the same (e.g., **def table**)
- Inheritance can be used to **extend** or **restrict** the functionality of the superclass. 
- Any one of the Line or Parabola classes can be the superclass, then the other will be the subclass.


### Extending Functionality
- Lets first show how to write class Parabola as a subclass of class Line, and implement just the new additional code that we need and that is not already written in the superclass Line:



```python
# Parent class: Line.  Child class: Parabola
# Assuming that the class Line is as above with no modifications, then class Parabola is written:
class Parabola(Line):
    def __init__(self, c0, c1, c2):
        Line.__init__(self, c0, c1) # let Line store c0 and c1
        self.c2 = c2
    def __call__(self, x):
        return Line.__call__(self, x) + self.c2*x**2

p = Parabola(1,-1,2) # instance
p1 = p(x=2.5) # call to p.__call__(2.5)
print(p1)
print(p.table(0, 1, 3))
```

    11.0
               0            1
             0.5            1
               1            2
    


**Program Flow.** 
- Calling **Parabola(1, -1, 2)** leads to a call to the constructor method **__ init __** in the superclass Line, where the arguments c0, c1, and c2 takes values 1, −1, and 2, respectively. The self argument in the constructor is the object that will be returned and referred to by the variable **p**. 
- We can not use the constructor **__ ini __**, in the parent class as it is, to replace fully the initial attributes in the subclass Parabola as one parameter, c2, is missing. Inside the subclass Parabola constructor we call the constructor in superclass Line. In this  latter method, we create two attributes in the self object. Printing out dir(self) will explicitly demonstrate what self contains so far in the construction process. Back in class Parabola’s constructor, we add a third attribute c2 to the same self object. Then the self object is invisibly returned and referred to by p.

- The other statement, p1 = p(x=2.5), has a similar program flow. First we enter the p.**__ call __** method with self as p and x valued 2.5. The program flow jumps to the **__ call __** method in the class Line for evaluating the linear part c0+c1x of the expression for the parabola, and then the flow jumps back to the **__ call __** method in class Parabola where we add the new quadratic term, **c2x^2** .

### Restricting Functionality

- In our example of Parabola as a subclass of Line, we used inheritance to extend the functionality of the superclass (just like a child that works and helps its parents). 
- Inheritance can also be used for restricting functionality.
- Mathematically a straight line is a special case of a parabola (with c2=0). Adopting this reasoning reverses the dependency of the classes: now it is more natural to let Line to be a subclass of the Parabola (Assuming line is a parabola with c2=0).


### Example-2


```python
class Parabola: 
    def __init__(self, c0, c1, c2):
        self.c0, self.c1, self.c2 = c0, c2, c2
    def __call__(self, x):
        return self.c0 + self.c1*x + self.c2*x**2
    def table(self, L, R, n): # implemented as shown above
        """Return a table with n points for L <= x <= R."""
        s = ''
        import numpy as np
        for x in np.linspace(L, R, n):
            y = self(x)
            s += '%12g %12g\n' % (x, y)
        return s
        
        
class Line(Parabola):
    def __init__(self, c0, c1):
        Parabola.__init__(self, c0, c1, 0)  # c2=0
```

### Example-3
- Writte a superclass **Natural** for the sequence of natural numbers 0,1,2,...N, for our example, the seqeunce is limited to 0,1,2,3,...,10
and subclasses for the sequence of (1) **even**, (2) **odd**, (3) **multiple of 3**, and (4) **primes**
- This is an effort to teach the simplest example possible


```python
"""
natural_OOP.py
Superclass/subclass examples
In search of the simplest example possible

Created on Fri Dec 17 09:22:12 2021
@author: Marco
"""

class Natural:      # superclass
    def __init__(self,a,b):
        self.a=a
        self.b=b
    def constL(self):
        L=[]
        for k in range(self.a,self.b+1):
            L.append(k)
        return L

N=Natural(0,10)  # i.e., a=0, b=10
print(N.constL())

class EvenVal(Natural):  # subclass
    def EV(self):
        EA=[]
        for j in self.constL():
            if j%2==0:
                EA.append(j)
        return EA
        
E=EvenVal(0,10)  # i.e., a=0, b=10
print(E.EV())       

class OddVal(Natural):  # subclass
    def OV(self):
        return [j for j in self.constL() if j%2!=0 ]

O=OddVal(0,10)  # i.e., a=0, b=10
print(O.OV()) 

class Mult3(Natural):  # subclass
    def M3(self):
        return [j for j in self.constL()[1:] if j%3==0 ]

m3=Mult3(0,10)  # i.e., a=0, b=10
print(m3.M3())

class Primo(Natural):
    # Searches for primes in a list
    def PP(self):
        PL=[]
        for N in self.constL()[2:]:
            flag=1
            k=2
            while k<=(N-1):
                if N%k==0:
                    flag=0
                    break
                k+=1
            if flag==1:
                PL.append(N)
        return PL

P=Primo(0,10)
print(P.PP())

class Primo2(Natural):
    # Searches for primes in a list
    def PP2(self):
        from sympy import isprime
        PL=[]
        for N in self.constL():
            if isprime(N):
                PL.append(N)
        return PL

P=Primo2(0,10)
print(P.PP2())
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    [0, 2, 4, 6, 8, 10]
    [1, 3, 5, 7, 9]
    [3, 6, 9]
    [2, 3, 5, 7]
    [2, 3, 5, 7]


### Example-4
- Assume that we have written a superclass X3 for the sequence of multiple of three numbers and a subclass for the square of multiple of 3. Is an extending or restricting functionality example?


```python
# Parent Class:
class X3:
    ''' Multiples of 3 class '''
    def __init__(self, first, last):
        self.first=first
        self.last=last
    def compX3(self):
        m3=[]
        for k in range(self.first,self.last+1):
            if k%3==0:
                m3.append(k)
        return m3

pm=X3(1,99)
print(pm.compX3())

# Child Class
class X3E2(X3):
    '''Square of multiple of 3 class'''
    def compX3E2(self):
        m3E2=[]
        for item in self.compX3():
            RR=item**2
            if RR<=self.last:
                m3E2.append(RR)
        return m3E2

cm=X3E2(1,99)
print(cm.compX3E2())
    
```

    [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99]
    [9, 36, 81]



```python

```

38974460399226259e7686b640c83f36f9da2e9e
ipynb
Jupyter Notebook
OOP3.ipynb
MarcoArochaOrdonez/ConqueringPython-with-JupyterNotebooks
0e5b0a4d6dfe3fa7a5158d048903f43deaefcdc8
CC0-1.0
OOP3.ipynb
MarcoArochaOrdonez/ConqueringPython-with-JupyterNotebooks
0e5b0a4d6dfe3fa7a5158d048903f43deaefcdc8
CC0-1.0
OOP3.ipynb
MarcoArochaOrdonez/ConqueringPython-with-JupyterNotebooks
0e5b0a4d6dfe3fa7a5158d048903f43deaefcdc8
CC0-1.0
Qwen/Qwen-72B
1. YES
2. YES