# 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


## 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
> * Réécriture d'une méthode par écrasement
> * On ne tire pas profit de l'héritage (pas d'appel ou appel incorrect à `super()`)
Exemple:
```python
class A:
def m1(self):
print("A1")
return 42
class B(A):
# OverWRITING de m1, on ne réutilise pas la méthode de la classe-mè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 méthode `m1()` et non celle de sa classe-mère
### b. OverRIDING
> * Réécriture d'une méthode par redéfinition
> * On tire profit de l'héritage, on étend la méthode de la classe-mè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 (= redéfinition) de la méthode retirer()
- On étend la méthode retirer() de Compte(), grâce à 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 améliorée (Un carré est un rectangle..) ;-)
## 2. Overloading vs Polymorphisme
### a. Method OverLOADING
> * Ecriture d'une méthode de telle sorte qu'elle puisse être appelée avec un nombre variable de paramè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 méthode `sayHello` est dite "overLOADED" ou surdéfinie car elle peut être appelée avec 0 ou 1 paramètre, et renvoyer un résultat spécifique.
### b. Operator OverLOADING
> * Ecriture d'une méthode magique de telle sorte qu'un même opérateur arithmétique (+,*,<,>, ==,...) puisse avoir différents effets selon le type de variables concerné
Utilité ? Opé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 différents types, pour un nombre fixe de paramètres, est dite polymorphique
> * Elle agit diffé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'ambiguïté à l'exécution!
* Testez par vous-mêmes ;-)
## 3. Variables d'instance privées et mé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 méthodes accesseurs telles que `x(self)`, `y(self)` ou `estVisible(self)`, la méthode `__str__` de Rectangle ne sait pas accéder aux attributs privés de la classe Figure!
* Testez par vous-mêmes en modifiant `__str__` ou en décommentant les `print`