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