350 lines
12 KiB
Markdown
350 lines
12 KiB
Markdown
---
|
||
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
|
||
|
||

|
||
|
||
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"))
|
||
```
|