# 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 > * 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`