--- title: Découvrir la programmation orientée objet author: Steve Kossouho --- # Découvrir la programmation orientée objet ---- ## La programmation objet ? Quoi, pourquoi ? La programmation orientée objet, c'est des outils de langage fournis par certains langages de programmation pour vous permettre de décrire et de manipuler des concepts (objets, types). Lorsque l'on décrit un concept, on lui associe ce qu'on appelle des [attributs]{.naming} et [méthodes]{.naming} (comportements). Pour commencer, on peut imaginer un système où l'on mettrait en contact des propriétaires de chiens, avec la possibilité de créer des fiches de leurs chiens : ---- ### Diagramme de classes UML ![Diagramme UML représentant une classe de chiens](assets/images/classes-uml-dog.jpg) Exemple spartiate de diagramme de classes UML. ---- ## Découvrir la syntaxe de l'objet Dans les langages orientés objet, pour faire de l'objet, il faut écrire ou manipuler des **classes**. Les **classes** sont des modèles utilisés pour créer des objets. Dans l'exemple ci-dessous, Pierre et Marie sont des **objets** de la classe `Humain`. ```{.python .numberLines} class Human: name = None def parler(self): """Fonction uniquement disponible sur les humains""" print(f"{self} parle.") pierre = Human() # nouvel objet de la classe Human marie = Human() # encore un nouvel objet de la classe Human pierre.parler() ``` ---- ## Typographie des classes La typographie des classes conventionnelle est un peu différente (enfin !) de celle des variables, fonctions, modules et packages. En Python, pour nommer une classe, on utilisera le [PascalCase]{.naming} - Commence par une majuscule (ex. `Animal`) - Ne contient jamais d'underscore; - Les mots commencent par une majuscule (ex. `WildAnimal`) - Les sigles et acronymes gardent toutes leurs majuscules (ex. `CRTMonitor`) ---- ## Les attributs Les attributs, ce sont simplement des variables, mais uniquement accessibles sur la classe ou ses objets. Les attributs sont accessibles sur la classe et sur les objets de la classe. Tout objet instancié possède les mêmes valeurs d'attributs que la classe. ```{.python .numberLines} class Animal: name = None animal1 = Animal() # Crée un nouvel animal et l'assigne à une variable print(animal1.name) # Afficher la valeur de l'attribut `name` ``` [Exercice 1](https://github.com/sk-dwtoulouse/python-initiation-training/blob/main/exercices/07-objects/01-base-class.asciidoc#exercice-1) ---- ## Attributs : manipulation Préférez manipuler des attributs sur les objets plutôt que sur la classe. Le reste, c'est très simple, et on peut même ajouter de nouveaux attributs : ```{.python .numberLines} class Dummy: attribute1 = 19 item = Dummy() item.new_attr = "Hello" # nouvel attribut item.attribute1 = 60 # modifier un attribut ``` [Exercice 2](https://github.com/sk-dwtoulouse/python-initiation-training/blob/main/exercices/07-objects/01-base-class.asciidoc#exercice-2) ---- ## Méthodes Les méthodes, c'est exactement la même chose que les fonctions (sauf l'argument `self` qui est implicite) : ```{.python .numberLines} class Dummy: def dummy_func(self): print(self) item = Dummy() item.dummy_func() # ben, et self ?? ``` Seules les instances de la classe `Dummy`{.python} peuvent utiliser la méthode nommée `dummy_func()`{.python}; vous avez la certitude de ne pas utiliser cette méthode sur les objets d'une classe incorrecte. ---- ## L'argument `self` Quand vous écrivez des classes et que vous ajoutez des fonctions (on appelle ça des **méthodes**), elles ont toujours un premier argument nommé `self`. Il récupère toujours la référence de l'objet sur lequel vous avez demandé à exécuter la méthode. Quand vous appelez la méthode **sur un objet** (et pas sur une classe), ne passez pas de valeur entre parenthèses pour l'argument `self` : Python passe _implicitement_ l'objet sur lequel on a appelé la méthode à l'argument `self`. ```{.python .numberLines} class Dummy: def foo(self, number): print(number) dummy = Dummy() # `dummy` est implicitement passé dans `self` dummy.foo(15) # l'argument `number` reste obligatoire ``` [Exercice 3](https://github.com/sk-dwtoulouse/python-initiation-training/blob/main/exercices/07-objects/01-base-class.asciidoc#exercice-3-m%C3%A9thodes) ---- ## Différences entre une classe et ses objets - La classe sert de modèle pour instancier des objets - Les nouveaux objets possèdent toutes les propriétés de la classe - Les nouveaux objets vivent indépendamment les uns des autres - Les objets sont des objets, la classe est une classe ---- ## Instancier en passant des arguments ```{.python .numberLines} class Car: doors = 5 gearbox = "manual" # transmission brand = None # marque du véhicule ``` Avec cette déclaration de classe, créer un objet et l'initialiser prend quelques lignes… : ```{.python .numberLines} car = Car() car.doors = 3 car.gearbox = "automatic" car.brand = "Seat" ``` ---- C'est un peu long, notamment si l'on doit initialiser plusieurs objets qui ont beaucoup d'attributs. Il peut être intéressant de pouvoir instancier un objet d'une classe en utilisant des arguments, qui seraient utilisés pour modifier les attributs du nouvel objet, ex. : ```{.python .numberLines} car = Car(doors=3, gearbox="automatic", brand="Seat") # une ligne au lieu de 4 ``` ---- Pour ce faire, il faut définir ou redéfinir dans notre classe ce qu'on appelle en Python une **méthode spéciale** (ou "dunder") nommée spécifiquement `__init__`. Le nom de cette méthode est défini dans le langage Python. Cette méthode, si elle existe dans notre classe, est exécutée lorsque l'on instancie un nouvel objet, et les arguments passés entre parenthèses sont transmis à cette méthode : ```{.python .numberLines} class Car: doors = 5 gearbox = "manual" brand = None def __init__(self, doors=5, gearbox="manual", brand=None): # Grâce à cette méthode, on peut créer un objet en passant des arguments # avec les noms "gearbox", "doors" et "brand", tous facultatifs (None par défaut) self.doors = doors # self est l'objet que l'on vient de créer, doors est un argument self.gearbox = gearbox self.brand = brand lamborghini1 = Car(doors=3, brand="Lamborghini") ``` [Exercice 4](https://github.com/sk-dwtoulouse/python-initiation-training/blob/main/exercices/07-objects/01-base-class.asciidoc#exercice-4-red%C3%A9finition-de-linstanciation) ---- ## Héritage  et Polymorphisme  ---- ### Héritage L'héritage est un concept simple à appréhender : ```{.python .numberLines} class Animal: life_expectancy = None class Vertebrate(Animal): # Classe parente mise entre parenthèses vertebrae_count = None dog = Vertebrate() # Informations sur l'objet et les classes print(issubclass(Vertebrate, Animal)) # Vertebrate est-elle une spécialisation de Animal ? print(isinstance(dog, Vertebrate)) # dog est-il un vertébré ? print(isinstance(dog, Animal)) # dog est-il un animal ? ``` L'**héritage** permet de partir d'une classe de base, et de créer une autre classe plus spécialisée. Ici, les vertébrés sont effectivement une sous-classe du règne animal. Ils ont les mêmes attributs que tous les animaux (et les mêmes méthodes), mais ont peut-être des attributs et méthodes propres aux vertébrés. ---- ### Polymorphisme Le polymorphisme en Python, c'est le fait de pouvoir redéfinir une méthode dans une classe enfant : ```{.python .numberLines} class Animal: life_expectancy = None def move(self): print(f"Do something general with {self}") class Vertebrate(Animal): vertebrae_count = None def move(self): # On redéfinit move super().move() # appelle `move` comme défini dans la classe parente print("Do something different with vertebrae.") ``` **Note** : Une méthode redéfinie peut posséder une signature différente de celle existant dans la classe parente. ---- ### Le polymorphisme et la fonction `super()` Lorsque vous créez une classe enfant, qui redéfinit une méthode de la classe parente (ex. vous redéfinissez `move()`), il va souvent vous arriver de souhaiter recopier le code de la méthode que vous aviez dans la classe parente, et y ajouter quelques lignes. Plutôt que d'en recopier le code, Python vous propose la fonction `super()`, utilisable uniquement dans une méthode d'une classe qui utilise l'héritage. Cette fonction vous renvoie automatiquement votre objet `self`, mais il est modifié de façon à ce que tout appel de méthode sur cet objet exécute le code de la méthode tel que défini dans la classe parente. ---- ## Bonus : Courte introduction aux décorateurs avec @staticmethod Des fois, dans une classe, on souhaite placer des méthodes, qui sont utiles vis-à-vis de la classe, mais qui ne sont pas directement liées à des objets de la classe. Pour simplifier, on aimerait aussi se passer de l'argument `self`, et s'en servir comme de simples fonctions. C'est possible, grâce au décorateur `@staticmethod`. ---- ```{.python .numberLines} class Car: power_horse = None wheel_count = None @staticmethod def create_car_from_file(path): """Function to create car from file.""" return Car() ``` La fonction est rangée dans la classe comme si cette dernière était un package ou un module. Il ne faut pas en abuser mais c'est très pratique. ---- ## Bonus : Introspection L'introspection, c'est le fait, dans votre script, d'aller chercher des informations sur vos variables alors même que le programme est en train de tourner. C'est utilisé pour vérifier les types des variables, récupérer les noms des attributs d'un objet, etc. ---- ### Quelques fonctions d'introspection ```{.python .numberLines} class Car: wheel_count = 4 car1 = Car() print(getattr(car1, "wheel_count", None)) # renvoie car1.wheel_count, ou None si introuvable print(setattr(car1, "gearbox", None)) # exécute car1.gearbox = None print(hasattr(car1, "wheel_count")) # Renvoie si l'attribut existe dans l'objet print(dir(car1)) # liste les noms d'attributs de l'objet print(isinstance(car1, Car)) # car1 est-elle une voiture ? print(type(car1)) # renvoie la référence de la classe de l'objet ``` ---- ## Superbonus : Encapsulation Dans d'autres langages objet (pas en Python), l'encapsulation consiste, dans le corps d'une classe, à indiquer si des attributs de cette classe sont accessibles depuis le reste du code. Cela permet aux développeurs utilisant nos classes d'avoir la certitude qu'on ne touche pas directement à l'attribut. En Python, le concept d'encapsulation n'existe pas, **mais** on peut s'en approcher d'une certaine façon... ---- Nommer un attribut en le faisant précéder d'un double underscore (`__`) provoque un comportement spécifique de l'interpréteur Python : l'attribut est accessible avec son nom dans les méthodes de la classe, alors qu'il n'est accessible qu'avec un autre nom en dehors de la classe. Voir l'[article de Dan Bader sur la signification des underscores](https://dbader.org/blog/meaning-of-underscores-in-python). ---- ```{.python .numberLines} from uuid import UUID, uuid4 class Person: __uuid: UUID = None def __init__(self): self.__uuid = uuid4() # Tester le "name mangling d'attributs" if __name__ == "__main__": person = Person() print(getattr(person, "__uuid", "not found")) print(getattr(person, "_Person__uuid", "not found")) ```