<jupyter_start><jupyter_text>[Les Classes](https://docs.python.org/3/tutorial/classes.htmla-first-look-at-classes)<jupyter_code>class MaPremiereClasse:
    def __init__(self, nom): # self désigne l'instance «courante»
        self.nom = nom       # déclaration de l'attribut «nom»

    def saluer(self):        # méthode = fn définie dans une classe
        print(f'Bonjour {self.nom}!')
une_instance = MaPremiereClasse('<NAME>') # instantiation
print(f'une_instance: {une_instance}')
print(f'type: {type(une_instance)}')
print(f'une_instance.nom: {une_instance.nom}')<jupyter_output><empty_output><jupyter_text>Créons une «autre» instance:<jupyter_code>instance_homonyme = MaPremiereClasse('<NAME>')
print(f'Est-ce la même instance? {instance_homonyme is une_instance}')
print(f'Sont-elles «égales»? {instance_homonyme == une_instance}')
print(f'Adresses mémoires:\n    une_instance -> {id(une_instance)},\n    instance_homonyme -> {id(instance_homonyme)}.')<jupyter_output><empty_output><jupyter_text>Méthodes Les fonctions situées dans une classe sont appelées **méthodes**. Elles sont utilisées de manière similaire aux fonctions.<jupyter_code>alice = MaPremiereClasse(nom='Alice')
alice.saluer()<jupyter_output><empty_output><jupyter_text>`__init__()` `__init__()` est une **méthode *spéciale*** qui est utilisée pour *initialiser* les **instances** de la classe. Elle est appelée automatiquement lorsque vous créer une instance de la classe.<jupyter_code>class Exemple:
    def __init__(self):
        print("Là, nous sommes à l'intérieur d'__init__")
        
print("Avant la création d'une instance de la classe Exemple")
exemple = Exemple()
print('Après la création de cette instance')<jupyter_output><empty_output><jupyter_text>`__init__()` est typiquement utilisée pour initialiser les variables d'instance de votre classe. Elles peuvent être listées en arguments après `self`. Pour être en mesure d'accéder à ces variables d'instance plus tard, pendant la durée de vie de celle-ci, il faut associer ces variables à `self` avec une syntaxe de la forme `self. = ma_variable`.`self` est le premier argument des méthodes de votre classe et c'est aussi votre moyen d'accéder aux variables d'instances et aux autres méthodes.<jupyter_code>class Exemple:
    def __init__(self, var1, var2):
        self.nom_attribut1 = var1
        self.nom_attribut2 = var2
        
    def affiche_valeurs_attributs(self):
        print(f'{self.nom_attribut1} {self.nom_attribut2}')
        
e = Exemple('abc', 123)
e.affiche_valeurs_attributs() # raccourci pour «Exemple.affiche_valeurs_attributs(e)» et donc self désigne e.<jupyter_output><empty_output><jupyter_text>`__str__()` `__str__()` est une autre **méthode *spéciale*** que vous pouvez appeler lorsqu'une instance d'une classe doit être convertie en une chaîne de caractère (typiquement lorsque vous voulez l'afficher avec `print`). Autrement dit, en définissant la méthode `__str__` pour votre classe, vous décidez de la version textuelle des instances de votre classe. La méthode doit retourner une chaîne de caractères.<jupyter_code>class Personne:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age
        
    def __str__(self):
        return f"Mon nom est {self.nom} et j'ai {self.age} ans."
    
jacques = Personne('Jacques', 82)
print(f'Voici la représentation textuelle de jacques: {jacques}')<jupyter_output><empty_output><jupyter_text>Variables de classe / variables d'instance Les **variables de classe** sont *partagées entre toutes les instances de cette classe* tandis que les variables d'instance peuvent avoir des valeurs différentes d'une instance à une autre de cette classe.<jupyter_code>class Exemple:
    # Voici des variables de classes
    nom = 'Classe exemple'
    description = "Juste un exemple d'une simple classe"

    def __init__(self, var1):
        # Voici une variable d'instance
        self.variable_instance = var1

    def montrer_info(self):
        info = f'variable d\'instancee: {self.variable_instance}, nom: {Exemple.nom}, description: {Exemple.description}'
        print(info)


instance_1 = Exemple('truc')
instance_2 = Exemple('machin')

# Le nom et la description sont partagées (ont même valeur) d'une instance à l'autre
assert instance_1.nom == instance_2.nom == Exemple.nom
assert instance_1.description == instance_2.description == Exemple.description

# Si vous modifier la valeur d'une variable de classe, le changement est perçu par chaque instance
Exemple.nom = 'Nom modifié'
instance_1.montrer_info()
instance_2.montrer_info()<jupyter_output><empty_output><jupyter_text>Publique / privée En python il n'y a pas de séparation stricte entre les méthodes ou variable d'instance publique/privée. La **convention** est de débuter le nom d'une variable ou d'une méthode d'instance avec un caractère de soulignement `_` (underscore) *si elle doit être considéré comme privée*.Privée veut dire qu'elle ne devrait pas être accessible depuis l'extérieur de la classe, c'est à dire depuis du code qui **utilise** (par opposition à *définir*) cette classe (*code client*).Par exemple, supposons disposer d'une classe `Personne` qui définie une variable d'instance `age`. Nous voulons que cette variable ne puisse être accéder directement (par exemple pour la modifier) après la création d'une instance. En python, on écrirait:<jupyter_code>class Personne:
    def __init__(self, age):
        self._age = age
        
personne_exemple = Personne(age=15)
# Vous ne pouvez pas faire ça:
# print(personne_exemple.age)
# Ni ça:
# personne_exemple.age = 16<jupyter_output><empty_output><jupyter_text>Si vous souhaitez que `age` puisse être consultée directement sans être modifiable, vous pouvez utiliser le décorateur `@property`:<jupyter_code>class Personne:
    def __init__(self, age):
        self._age = age
        
    @property # le décorateur pour la méthode qui suit
    def age(self): # cette méthode peut être utilisée sans les parenthèses habituelles ...
        return self._age
        
personne_exemple = Personne(age=15)
# Maintenant vous pouvez faire cela:
print(personne_exemple.age)
# Mais pas cela:
#personne_exemple.age = 16<jupyter_output><empty_output><jupyter_text>De cette façon vous avez un contrôle d'accès pour les variables d'instances de votre classe.<jupyter_code>class Personne:
    def __init__(self, age):
        self._age = age
        
    @property
    def age(self):
        return self._age
    
    def celebrer_anniversaire(self):
        self._age += 1
        print(f'Joyeux anniversaire pour tes {self._age} ans!')
        
personne_exemple = Personne(age=15)
personne_exemple.celebrer_anniversaire()<jupyter_output><empty_output><jupyter_text>Introduction à l'héritage - *inheritance* On souhaite **réutiliser** du code défini dans une classe et le **spécialiser**.Un `Chat` ou un `Chien` **est un** `Animal` et:<jupyter_code>class Animal:
    def saluer(self):
        print('Bonjour, je suis un ... animal!')

    @property
    def nourriture_preferee(self):
        return 'boeuf'


class Chien(Animal):
    def saluer(self):
        print('Wouf Wouf')


class Chat(Animal):
    @property
    def nourriture_preferee(self):
        return 'poisson'
chien = Chien()
chien.saluer()
print(f"La nourriture préférée d'un chien est le {chien.nourriture_preferee}")

chat = Chat()
chat.saluer()
print(f"La nourriture préférée d'un chat est le {chat.nourriture_preferee}")<jupyter_output><empty_output><jupyter_text>Observer que si une méthode de même nom est *redéfinie*, elle prime (**spécialisation**); sinon c'est celle de la classe «mère» `Animal` qui est utilisée (**réutilisation**).  Exercices  1. Compléter les parties manquantes de la classe `Calculateur`Compléter les portions `____` de l'implémentation de `Calculateur` afin de vérifier les assertions données plus loin.<jupyter_code>class Calculateur:
    def __init__(self, var1, ____):
        self.____ = var1
        self.____ = _____
    
    def calculer_puissance(self):
        return self.____ ** ____.____
    
    def calculer_somme(____, var3):
        return ____.____ + ____.____ + var3
calc = Calculateur(2, 3)
assert calc.calculer_puissance() == 8
assert calc.calculer_somme(4) == 9<jupyter_output><empty_output><jupyter_text>Solution<jupyter_code>class Calculateur:
    def __init__(self, var1, var2):
        self.v1 = var1
        self.v2 = var2
    
    def calculer_puissance(self):
        return self.v1 ** self.v2
    
    def calculer_somme(self, var3):
        return self.v1 + self.v2 + var3<jupyter_output><empty_output><jupyter_text>2. Finaliser la classe `ManipulateurDeChaine` Compléter les portions `____` et implémenter la méthode `creer_titre()`.<jupyter_code>class ManipulateurDeChaine:
    """____"""
    
    categorie = ____
    
    def __init__(self, original):
        self.chaine = ____
        
    def reverser_mots(self):
        mots = self.chaine.____
        self.chaine = ' '.join(reversed(____))
        
    def creer_titre(self):
        # Implémenter vous-même cette méthode
        
    def obtenir_chaine_manipulee(____):
        return self._____
assert ManipulateurDeChaine.__doc__ == 'Chaîne de documentation de ManipulateurDeChaine'
assert ManipulateurDeChaine.categorie == 'Manipulateur'

chaine_manip = ManipulateurDeChaine('pyThON cOOL')

chaine_manip.reverser_mots()
assert chaine_manip.obtenir_chaine_manipulee() == 'cOOL pyThON'

chaine_manip.creer_titre()
assert chaine_manip.obtenir_chaine_manipulee() == 'Cool Python'<jupyter_output><empty_output><jupyter_text>Solution<jupyter_code>class ManipulateurDeChaine:
    """Chaîne de documentation de ManipulateurDeChaine"""
    
    categorie = 'Manipulateur'
    
    def __init__(self, original):
        self.chaine = original
        
    def reverser_mots(self):
        mots = self.chaine.split()
        self.chaine = ' '.join(reversed(mots))
        
    def creer_titre(self):
        # Implémenter vous-même cette méthode
        mots = self.chaine.split()
        titre = []
        for mot in mots:
            titre.append(mot.capitalize())
        self.chaine = ' '.join(titre)
        
    def obtenir_chaine_manipulee(self):
        return self.chaine<jupyter_output><empty_output><jupyter_text>3. Créer une classe `Chien` Créer la classe `Chien` dont voici les spécifications:* Les Chiens dépense leur énergie en aboyant et en regagne en dormant* Une nouvelle instance de `Chien` a 10 unités d'énergie* `Chien` a une méthode `dormir` qui permet de récupérer 2 unités d'energie* `Chien` a une méthode `aboyer` qui dépense 1 unité d'énergie* `Chien` a une méthode `obtenir_energie` qui retourne le niveau d'énergie restant<jupyter_code>class Chien:
    # À vous de jouer!
chien = Chien()
assert chien.obtenir_energie() == 10

chien.aboyer()
chien.aboyer()
chien.aboyer()
assert chien.obtenir_energie() == 7

chien.dormir()
assert chien.obtenir_energie() == 9

un_autre_chien = Chien()
assert un_autre_chien.obtenir_energie() == 10<jupyter_output><empty_output><jupyter_text>Solution<jupyter_code>chien = Chien()
assert chien.obtenir_energie() == 10

chien.aboyer()
chien.aboyer()
chien.aboyer()
assert chien.obtenir_energie() == 7

chien.dormir()
assert chien.obtenir_energie() == 9

un_autre_chien = Chien()
assert un_autre_chien.obtenir_energie() == 10<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10024687004055723, 'fraction_numerical': 0.006612590372068418, 'mean_word_length': 4.422084130019121, 'pattern_counts': {'":': 0, '<': 71, '<?xml version=': 0, '>': 73, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '254631', 'license': 'CC0-1.0', 'n_tokens_mistral': 3969, 'n_tokens_neox': 3944, 'n_words': 1038, 'path': '00_bases_python/complements/03_les_classes.ipynb', 'repo_name': 'efloti/cours-nsi-premiere', 'size': '11346'}