---
# System prepended metadata

title: 'M10: Héritage et Polymorphisme'

---

# M10: Héritage et Polymorphisme

## a. Objectifs

-  Variables d'instance privées et accesseurs/modifieurs
-  Variables et méthodes de classe 
-  Heritage
    -  Hiérarchie de classes
    -  Redéfinition de méthodes
    -  Polymorphisme
    -   `self` vs `super()`

### b. Glossaire
> **Modifier**
 A function or method that changes one or more of the objects it receives as parameters. Most modifier functions are void (do not return a value).

> __Operator overloading__
> Extending built-in operators ( +, -, *, >, <, etc.) so that they do different things for different types of arguments. 

> __Polymorphic__
> A function that can operate on more than one type. 
Notice the subtle distinction: overloading has different functions (all with the same name) for different types, whereas a polymorphic function is a single function that can work for a range of types.
> Example: `print()` works with tuples, lists, ... 

> __Pure function__
> A function that does not modify any of the objects it receives as parameters. Most pure functions are not void but return a value.

> __Class attribute__
> A variable that is defined inside a class definition but outside any method. Class attributes are accessible from any method in the class and are shared by all instances of the class.
> 

> __Class Methods__
> Instead of accepting a `self` parameter, class methods take a `cls` parameter that points to the class—and not the object instance—when the method is called. Because the class method only has access to this `cls` argument, it can’t modify object instance state. That would require access to `self`. However, class methods can still **modify class attributes** that applies across all instances of the class!

> __Inheritance__
> The ability to define a new class that is a modified version of a previously defined class. The new class inherits all of the methods and class attributes of the parent. The primary advantage of this feature is that you can add new methods to a class without modifying the existing class. 

> __Super call__
> A super call can be used to gain access to inherited methods – from a parent or ancestor class – that has been overwritten in a child class. This can either be done by explicitly referring to that parent class, or by using the special `super()` function.



    
## c. Relevant slides


![](https://i.imgur.com/jhDexpJ.png)

![](https://i.imgur.com/684vzUk.png)

## d. Playground repl.it
[Check source code here](https://repl.it/repls/CheerfulBigheartedPentagon) 

### Shallow Copies vs Deep copies
```python 
class NokiaPhone :

  def __init__(self,s,p,t,n) :
    self.marque = "Nokia"
    self.serie = s
    self.poids = p
    self.taille = t
    self.numero = n

  def print_specs(self) :
    print(self.marque + " " + str(self.serie))
    print("Poids: " + str(self.poids) + " g")
    print("Taille: " + self.taille + " mm")

  def print(self):
    self.print_specs()
    print(str(self.numero) + "\n")

class PhoneNumber :
  def __init__(self,c,z,pr,po):
    self.country = c
    self.zone = z
    self.prefix = pr
    self.postfix = po

  def __str__(self):
    return "+{}(0){}/{}{}".format(self.country,\
    self.zone,self.prefix,self.postfix)

import copy

kim_number = PhoneNumber(32,10,4,79111)
kim_phone = NokiaPhone(5110,170,"132x48x31",kim_number)
# kim_phone.print()

# On copie et deepcopie le TEL de Kim
siegfried_phone = copy.deepcopy(kim_phone)
charles_phone = copy.copy(kim_phone)

# On change le postfix de Kim
kim_number.postfix = "79122"
# kim_phone.print()

# Quels états auront les copies/deep copies?
siegfried_phone.print() 
# Sigfried a bien gardé le numero original à l'instant de la copie
charles_phone.print()
# Charles a gardé une ref vers l'objet kim_phone, et a donc sont postfix updaté également 
```

### Variables d'instance vs Variables de classe
```python 
class Compte :

  taux_interet_A = 0.02
  __taux_interet_B = 0.03
  
  def __init__(self, titulaire):
    self.__titulaire = titulaire
    self.__solde = 0

  @classmethod
  def taux_interet(cls):
    return cls.__taux_interet_B
  
  @classmethod
  def set_taux_interet(cls,nouveau_taux):
    cls.__taux_interet_B = nouveau_taux

  def deposer(self,amt):
    self.__solde += amt

  def retirer(self, amt):
    self.__solde -= amt

  def titulaire(self):
    return self.__titulaire

  def solde(self):
    return self.__solde

  def __str__(self) :
    return "Compte de {} : solde = {}".\
      format(self.titulaire(),self.solde())


""" === VAR d'instance privées ==== """
compte_sig = Compte("Sigfried") 
# print(compte_sig.__solde)  Attribute Error
print(compte_sig.solde() == 0)

""" === VAR de classe publiques === """
print(compte_sig.taux_interet_A == Compte.taux_interet_A)
compte_sig.taux_interet_A = 0.04
# Attention a la distinction en var d'instance et var de classe
print(compte_sig.taux_interet_A != Compte.taux_interet_A)

""" === VAR de classe privées === """
# print(compte_sig.taux_interet_B) Attribute error
# print(Compte.taux_interet_B) Attribute error
print(Compte.taux_interet() == 0.03)
Compte.set_taux_interet(0.04)
print(Compte.taux_interet() == 0.04)
```
### Héritage 

* Do not hesitate to check [Python tutor](http://www.pythontutor.com/visualize.html#mode=edit)
```python 
""" === Héritage === """
class CompteCourant(Compte) :

  __frais_retirer = 0.05

  def __init__(self, titulaire,banque) :
    super().__init__(titulaire)
    self.__banque = banque

  def __str__(self) :
    return super().__str__() + "; banque = " + self.__banque

  # Nouvelle méthode
  def transferer(self,compte,somme) :
    res = self.retirer(somme)
    if res != "Solde insuffisant" :
      compte.deposer(somme)
    return res

  # Overriding = extension de la méthode de la classe-mère
  def retirer(self, somme):
    frais = CompteCourant.__frais_retirer
    # return Compte.retirer(self,somme+frais) OKAY mais pas top niveau récursion
    return super().retirer(somme+frais)


compte_kim = CompteCourant("Kim","ING")
compte_charles = CompteCourant("Charles","CBC")

compte_kim.deposer(100)
compte_kim.transferer(compte_charles,50)

print(compte_kim)
print(compte_charles)
```

# Restructuration: M10 

## 1. Overriding vs Overwriting

### a. OverWRITING
> * Réécriture d'une méthode par écrasement
> * On ne tire pas profit de l'héritage (pas d'appel ou appel incorrect à `super()`)
  
Exemple:

```python 
class A:
    def m1(self):
        print("A1")
        return 42
        
class B(A):
    # OverWRITING de m1, on ne réutilise pas la méthode de la classe-mère
    def m1(self):
        print("B1")

a, b = A(), B()
res1, res2 = a.m1(), b.m1()
print(res1,res2)
```

* Un objet de la classe B utilisera sa propre méthode `m1()` et non celle de sa classe-mère 


### b. OverRIDING
> * Réécriture d'une méthode par redéfinition
> * On tire profit de l'héritage, on étend la méthode de la classe-mère
  
Exemple:
```python 
class Compte :
  def __init__(self, titulaire):
     self.__titulaire = titulaire
     self.__solde = 0
     
  def retirer(self, amt):
     self.__solde -= amt
     
class CompteCourant(Compte) :
  __frais_retirer = 0.05
  
  def __init__(self, titulaire,banque) :
    super().__init__(titulaire)
    self.__banque = banque
    
  def retirer(self, somme):
  """
  - Overriding (= redéfinition) de la méthode retirer()
  - On étend la méthode retirer() de Compte(), grâce à son appel via super(),
    avec des frais pour chaque transaction.
  - Les frais ne seront applicables que sur les instances de CompteCourant
  """
      frais = CompteCourant.__frais_retirer
      return super().retirer(somme+frais)
  
```

* NB. l'OverRIDING est une forme d'OverWRITING améliorée (Un carré est un rectangle..) ;-)


## 2. Overloading vs Polymorphisme

### a. Method OverLOADING
> * Ecriture d'une méthode de telle sorte qu'elle puisse être appelée avec un nombre variable de paramètres
   
Exemple:
```python 
class Human:
    def sayHello(self, name=None):
        if name is not None:
            print 'Hello ' \+ name
        else:
            print 'Hello '
            
obj = Human()
obj.sayHello()
obj.sayHello('Guido')
```

* Dans l'exemple ci-dessus, la méthode `sayHello` est dite "overLOADED" ou surdéfinie car elle peut être appelée avec 0 ou 1 paramètre, et renvoyer un résultat spécifique.

### b. Operator OverLOADING
> * Ecriture d'une méthode magique de telle sorte qu'un même opérateur arithmétique (+,*,<,>, ==,...) puisse avoir différents effets selon le type de variables concerné
Utilité ? Opérations sur objets!

Exemple:
```python 
class MyTime:
# Previously defined methods here...
    def __add__(self, other):
        secs = self.to_seconds() + other.to_seconds()
        return MyTime(0, 0, secs)
        
    def __eq__(self, other):
        if type(other) is MyTime:
            return self.to_seconds() == other.to_seconds()
        else:
            return False
            
t1 = MyTime(1, 15, 42)
t2 = MyTime(3, 50, 30)
t3 = t1 + t2
print(t3) # Affiche 05:06:12
print(t1 == t2) # Affiche False
````

### c. Polymorphisme
> *  Une fonction qui peut prendre des arguments de différents types, pour un nombre fixe de paramètres, est dite polymorphique
> * Elle agit différemment en fonction du type de l'argument

Exemple
```python 
class Shark():
    def swim(self):
        print("The shark is swimming.")
    def swim_backwards(self):
        print("The shark cannot swim backwards, but can sink backwards.")
        
class Clownfish():
    def swim(self):
        print("The clownfish is swimming.")
    def swim_backwards(self):
        print("The clownfish can swim backwards.")
        
sammy = Shark()
casey = Clownfish()

for fish in (sammy, casey):
    fish.swim()
    fish.swim_backwards()

```
* Dans cet exemple, `swim` et `swim_backwards` sont polymorphiques. Pour Python, il n'y a pas d'ambiguïté à l'exécution!
* Testez par vous-mêmes ;-)
       


## 3. Variables d'instance privées et méthodes accesseurs

```python 
class Figure:
    def __init__(self,x,y,visible=False):
        self.__x = x
        self.__y = y
        self.__visible = visible
        
    def estVisible(self):
        return self.__visible
        
    def x(self):
        return self.__x
        
    def y(self):
        return self.__y
        
class Rectangle(Figure):
    def __init__(self,lo,la,x,y):
        super().__init__(x,y)
        self.__longueur = lo
        self.__largeur = la
        
    def longueur(self):
        return self.__longueur
        
    def largeur(self):
        return self.__largeur
        
    def __str__(self) :
        return "({},{},{},{},{})".format(self.longueur(),self.largeur(),\
                                    self.x(),self.y(),self.estVisible())

r = Rectangle(10,20,0,0)
# print(r.__x)
# print(r.__visible)
print(r.x())
print(r.estVisible())
print(r)
```

* Sans les méthodes accesseurs telles que `x(self)`, `y(self)` ou `estVisible(self)`, la méthode `__str__` de Rectangle ne sait pas accéder aux attributs privés de la classe Figure!

* Testez par vous-mêmes en modifiant `__str__` ou en décommentant les `print`
  