<jupyter_start><jupyter_text>"Chapitre 6 : Les classes et objets en python"> "Python : Chapitre 6- Lesson 1"- toc: false - badges: true- hide_binder_badge: true- hide_github_badge: true- comments: false- layout: post- author: <NAME>- permalink: /python-intro-gen/chapter/6/lesson/1/ Nous avons déjà vu comment nous pouvons utiliser un dictionnaire pour regrouper des données associées et comment nous pouvons utiliser des fonctions pour créer des raccourcis pour des groupes d'instructions couramment utilisés. Une fonction exécute une action en utilisant un ensemble de paramètres d'entrée. Toutes les fonctions ne sont pas applicables à tous les types de données. Les classes sont un moyen de regrouper des données et des fonctions connexes qui agissent sur ces données.Une classe est un type de données, tout comme une `string`, un entier ou une liste. Lorsque nous créons un objet de ce type de données, nous l'appelons une instance d'une classe.Comme nous l'avons déjà mentionné, dans certains autres langages, certaines entités sont des objets et d'autres non. En Python, tout est objet, tout est une instance d'une certaine classe. Les classes et les types sont eux-mêmes des objets, et ils sont de type type. Vous pouvez connaître le type de n'importe quel objet à l'aide de la fonction `type` :<jupyter_code>type(42)<jupyter_output><empty_output><jupyter_text>Les valeurs de données que nous stockons à l'intérieur d'un objet sont appelées attributs et les fonctions associées à l'objet sont appelées méthodes. Nous avons déjà utilisé les méthodes de certains objets intégrés, comme les `string` et les listes.Lorsque nous concevons nos propres objets, nous devons décider comment nous allons regrouper les choses et ce que nos objets vont représenter.Parfois, nous écrivons des objets qui correspondent très intuitivement aux choses du monde réel. Par exemple, si nous écrivons du code pour simuler des réactions chimiques, nous pourrions avoir des objets `Atom` que nous pouvons combiner pour créer un objet `Molécule`. Cependant, il n'est pas toujours nécessaire, souhaitable ou même possible de rendre tous les objets de code parfaitement analogues à leurs homologues du monde réel.Parfois, nous pouvons créer des objets qui n'ont aucun équivalent dans le monde réel, simplement parce qu'il est utile de regrouper certaines fonctions.  1. Création d'une classe (`class`)Voici un exemple de classe personnalisée simple qui stocke des informations sur une personne :<jupyter_code>import datetime # we will use this for date objects

class Person:

    def __init__(self, name, surname, birthdate, address, telephone, email):
        self.name = name
        self.surname = surname
        self.birthdate = birthdate

        self.address = address
        self.telephone = telephone
        self.email = email

    def age(self):
        today = datetime.date.today()
        age = today.year - self.birthdate.year

        if today < datetime.date(today.year, self.birthdate.month, self.birthdate.day):
            age -= 1

        return age

person = Person(
    "Jane",
    "Doe",
    datetime.date(1992, 3, 12), # year, month, day
    "No. 12 Short Street, Greenville",
    "555 456 0987",
    "<EMAIL>"
)

print(person.name)
print(person.email)
print(person.age())<jupyter_output><empty_output><jupyter_text>Nous commençons la définition de la classe avec le mot-clé `class`, suivi du nom de la classe et de deux points. Nous énumérerions toutes les classes parentes entre parenthèses avant les deux points, mais cette classe n'en a pas, nous pouvons donc les laisser de côté.À l'intérieur du corps de la classe, nous définissons deux fonctions : ce sont les méthodes de notre objet. La première s'appelle `__init__`, qui est une méthode spéciale. Lorsque nous appelons l'objet de classe, une nouvelle instance de la classe est créée et la méthode `__init__` sur ce nouvel objet est immédiatement exécutée avec tous les paramètres que nous avons passés à l'objet de classe. Le but de cette méthode est donc de mettre en place un nouvel objet à partir des données que nous vous avons fournies.La deuxième méthode est une méthode personnalisée qui calcule l'âge de notre personne en utilisant la date de naissance et la date du jour. 2. Le paramètre `self`Vous avez peut-être remarqué que ces deux définitions de méthode ont `self` comme premier paramètre, et nous utilisons cette variable à l'intérieur des corps des méthodes mais nous ne semblons pas passer ce paramètre. C'est parce que chaque fois que nous appelons une méthode sur un objet, l'objet lui-même est automatiquement transmis comme premier paramètre. Cela nous donne un moyen d'accéder aux propriétés de l'objet depuis l'intérieur des méthodes de l'objet.Vous devriez maintenant pouvoir voir que notre fonction `__init__` crée des attributs sur l'objet et les définit sur les valeurs que nous avons transmises en tant que paramètres. Nous utilisons les mêmes noms pour les attributs et les paramètres, mais ce n'est pas obligatoire.La fonction `age` ne prend aucun paramètre à l'exception de `self`, elle utilise uniquement les informations stockées dans les attributs de l'objet et la date actuelle (qu'elle récupère à l'aide du module `datetime`).Notez que l'attribut de date de naissance est lui-même un objet. La classe `date` est définie dans le module `datetime`, et nous créons une nouvelle instance de cette classe à utiliser comme paramètre de date de naissance lorsque nous créons une instance de la classe `Person`. Nous n'avons pas à l'affecter à une variable intermédiaire avant de l'utiliser comme paramètre de `Person`. Nous pouvons simplement le créer lorsque nous appelons `Person`, tout comme nous créons des `string` pour les autres paramètres.N'oubliez pas que la définition d'une fonction ne la fait pas s'exécuter. Définir une classe ne fait pas non plus fonctionner quoi que ce soit, cela indique simplement à Python la classe. La classe ne sera pas définie tant que Python n'aura pas exécuté l'intégralité de la définition, vous pouvez donc être sûr de pouvoir référencer n'importe quelle méthode à partir de n'importe quelle autre méthode sur la même classe, ou même référencer la classe à l'intérieur d'une méthode de la classe. Au moment où vous appelez cette méthode, la classe entière sera définitivement définie. Exercice 1Expliquez à quoi se réfèrent les variables suivantes et leur scope : - 1) `Person` - 2) `person` - 3) `surname` - 4) `self` - 5) `age` (le nom de la fonction) - 6) `age` (la variable utilisée dans la fonction) - 7) `self.email` - 8) `person.email` 3. Les attributs d'une instanceIl est important de noter que les attributs définis sur l'objet dans la fonction `__init__` ne forment pas une liste exhaustive de tous les attributs que notre objet est autorisé à avoir.Dans certains langages, vous devez fournir une liste des attributs de l'objet dans la définition de classe, des espaces réservés sont créés pour ces attributs autorisés lors de la création de l'objet et vous ne pouvez pas ajouter de nouveaux attributs à l'objet ultérieurement. En Python, vous pouvez ajouter de nouveaux attributs, et même de nouvelles méthodes, à un objet à la volée. En fait, il n'y a rien de spécial à propos de la fonction `__init__` lorsqu'il s'agit de définir des attributs. Nous pourrions stocker une valeur d'âge en cache sur l'objet à l'intérieur de la fonction d'âge :<jupyter_code>def age(self):
    if hasattr(self, "_age"):
        return self._age

    today = datetime.date.today()

    age = today.year - self.birthdate.year

    if today < datetime.date(today.year, self.birthdate.month, self.birthdate.day):
        age -= 1

    self._age = age
    return age<jupyter_output><empty_output><jupyter_text>Nous pourrions même ajouter un attribut complètement indépendant de l'extérieur de l'objet :<jupyter_code>person.pets = ['cat', 'cat', 'dog']<jupyter_output><empty_output><jupyter_text>Il est très courant que les méthodes d'un objet mettent à jour les valeurs des attributs de l'objet, mais il est considéré comme une mauvaise pratique de créer de nouveaux attributs dans une méthode sans les initialiser dans la méthode `__init__`. Définir des propriétés arbitraires depuis l'extérieur de l'objet est encore plus mal vu, car cela brise le paradigme orienté objet (dont nous parlerons dans le chapitre suivant).La méthode `__init__` sera certainement exécutée avant toute autre chose lorsque nous créons l'objet, c'est donc un bon endroit pour faire toute notre initialisation des données de l'objet. Si nous créons un nouvel attribut en dehors de la méthode `__init__`, nous courons le risque d'essayer de l'utiliser avant qu'il ne soit initialisé.Dans l'exemple age ci-dessus, nous devons vérifier si un attribut `_age` existe sur l'objet avant d'essayer de l'utiliser, car si nous n'avons pas exécuté la méthode age auparavant, elle n'aura pas encore été créée. Ce serait beaucoup plus propre si nous appelions cette méthode au moins une fois depuis `__init__`, pour s'assurer que `_age` est créé dès que nous créons l'objet.L'initialisation de tous nos attributs dans `__init__`, même si nous les définissons simplement sur des valeurs vides, rend notre code moins sujet aux erreurs. Cela facilite également la lecture et la compréhension – nous pouvons voir en un coup d'œil quels sont les attributs de notre objet.Une méthode `__init__` n'a pas à prendre de paramètres (sauf self) et elle peut être complètement absente.  4. Les fonctions `getattr`, `setattr` et `hasattr`Que se passe-t-il si nous voulons obtenir ou définir la valeur d'un attribut d'un objet sans coder en dur son nom ? Nous pouvons parfois vouloir boucler plusieurs noms d'attributs et effectuer la même opération sur chacun d'eux. Pour faire cela nous utiliserons les fonctions : `getattr`, `setattr` et `hasattr` La fonction `hastattr`Cet fonction permet de savoir si un objet possède un attribut ou non :<jupyter_code># Check if my object has attribut :a
print(hasattr(person, 'a'))

person.a = 'hello'
print(hasattr(person, 'a'))<jupyter_output><empty_output><jupyter_text>La fonction `getattr`Cet fonction permet de récupérer la valeur d'un attribut de l'objet :<jupyter_code>person.a = 12
person.b = "hello"
person.c = .002

# Get person.a, person.b, etc.
for key in ["<KEY>"]:
    print(getattr(person, key, None))

# Is equivalent to :
print(person.a)
print(person.b)
print(person.c)
print(person.d) # Oups this fails<jupyter_output><empty_output><jupyter_text>La fonction `setattr`Cet fonction permet de définir la valeur d'un attribut d'un objet. Par exemple, nous copions des données d'un dictionnaire vers un objet :<jupyter_code>mydict = {'d': None, 'e': 321, 'f': 'World'}

for key, value in mydict.items():
    setattr(person, key, mydict[key])

# Is equivalent to :
person.d = None
person.e = 321
person.f = 'World'<jupyter_output><empty_output><jupyter_text>Exercice 2Réécrivez la classe `Person` afin que l'âge d'une personne soit calculé pour la première fois lorsqu'une nouvelle instance de personne est créée, et recalculé (lorsqu'il est demandé) si le jour a changé depuis la dernière fois qu'il a été calculé.  5. Les attributs de classeTous les attributs définis sur une instance `Person` sont des attributs d'instance, ils sont ajoutés à l'instance lorsque la méthode `__init__` est exécutée. Cependant, nous pouvons également définir des attributs qui sont définis sur la classe. Ces attributs seront partagés par toutes les instances de cette classe. À bien des égards, ils se comportent comme des attributs d'instance, mais vous devez être conscient de certaines mises en garde.Nous définissons les attributs de classe dans son corps, au même niveau d'indentation que les définitions de méthode (un niveau au-dessus de l'intérieur des méthodes) :<jupyter_code>class Person:

    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    def __init__(self, title, name, surname):
        if title not in self.TITLES:
            raise ValueError("%s is not a valid title." % title)

        self.title = title
        self.name = name
        self.surname = surname<jupyter_output><empty_output><jupyter_text>Comme vous pouvez le voir, nous accédons à l'attribut de classe `TITLES` comme nous accéderions à un attribut d'instance, il est rendu disponible en tant que propriété sur l'objet d'instance, auquel nous accédons à l'intérieur de la méthode via la variable self.Tous les objets Person que nous créons partageront le même attribut de classe `TITLES`.Les attributs de classe sont souvent utilisés pour définir des constantes qui sont étroitement associées à une classe particulière. Bien que nous puissions utiliser des attributs de classe à partir d'instances de classe, nous pouvons également les utiliser à partir d'objets de classe, sans créer d'instance :<jupyter_code># we can access a class attribute from an instance
person.TITLES

# but we can also access it from the class
Person.TITLES<jupyter_output><empty_output><jupyter_text>L'objet de classe n'a accès à aucun attribut d'instance, ceux-ci ne sont créés que lorsqu'une instance est créée :<jupyter_code># This will give us an error
Person.name
Person.surname<jupyter_output><empty_output><jupyter_text>Les attributs de classe peuvent également parfois être utilisés pour fournir des valeurs d'attribut par défaut.<jupyter_code>class Person:
    deceased = False

    def mark_as_deceased(self):
        self.deceased = True<jupyter_output><empty_output><jupyter_text>Lorsque nous définissons un attribut sur une instance qui a le même nom qu'un attribut de classe, nous remplaçons l'attribut de classe par un attribut d'instance, qui aura la priorité sur lui. Si nous créons deux objets `Person` et appelons la méthode `mark_as_deceased` sur l'un d'eux, nous n'affecterons pas l'autre. Nous devons cependant être prudents lorsqu'un attribut de classe est de type mutable : car si nous le modifions sur place, nous affecterons tous les objets de cette classe en même temps. N'oubliez pas que toutes les instances partagent les mêmes attributs de classe :<jupyter_code>class Person:
    pets = []

    def add_pet(self, pet):
        self.pets.append(pet)

jane = Person()
bob = Person()

jane.add_pet("cat")
print(jane.pets)
print(bob.pets) # oops!<jupyter_output><empty_output><jupyter_text>Ce que nous devrions faire dans des cas comme celui-ci, c'est initialiser l'attribut mutable en tant qu'attribut d'instance, à l'intérieur de `__init__`. Ensuite, chaque instance aura sa propre copie distincte :<jupyter_code>class Person:

    def __init__(self):
        self.pets = []

    def add_pet(self, pet):
        self.pets.append(pet)

jane = Person()
bob = Person()

jane.add_pet("cat")
print(jane.pets)
print(bob.pets)<jupyter_output><empty_output><jupyter_text>Notez que les définitions de méthode sont dans le même scope que les définitions d'attribut de classe, nous pouvons donc utiliser les noms d'attribut de classe comme variables dans les définitions de méthode (sans `self`, qui n'est défini qu'à l'intérieur des méthodes) :<jupyter_code>class Person:
    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    def __init__(self, title, name, surname, allowed_titles=TITLES):
        if title not in allowed_titles:
            raise ValueError(f"{title} is not a valid title.")

        self.title = title
        self.name = name
        self.surname = surname<jupyter_output><empty_output><jupyter_text>Exercice 3Expliquez les différences entre les attributs `name`, `surname` et `profession`, et quelles valeurs ils peuvent avoir dans différentes instances de cette classe :<jupyter_code>class Smith:
    surname = "Smith"
    profession = "smith"

    def __init__(self, name, profession=None):
        self.name = name
        if profession is not None:
            self.profession = profession<jupyter_output><empty_output><jupyter_text>6. Les decorateurs de classe `@classmethod`Tout comme nous pouvons définir des attributs de classe, qui sont partagés entre toutes les instances d'une classe, nous pouvons définir des méthodes de classe. Pour ce faire, nous utilisons le décorateur `@classmethod` pour décorer une méthode ordinaire.Une méthode de classe a toujours son objet appelant comme premier paramètre, mais par convention nous renommerons ce paramètre de `self` en `cls`. Si nous appelons la méthode de classe depuis une instance, ce paramètre contiendra l'objet instance, mais si nous l'appelons depuis la classe, il contiendra l'objet classe. En appelant le paramètre `cls`, nous nous rappelons qu'il n'est pas garanti d'avoir des attributs d'instance.A quoi servent les méthodes de classe ? Parfois, il existe des tâches associées à une classe que nous pouvons effectuer à l'aide de constantes et d'autres attributs de classe, sans avoir besoin de créer d'instances de classe. Si nous devions utiliser des méthodes d'instance pour ces tâches, nous aurions besoin de créer une instance sans raison, ce qui serait un gaspillage. Parfois, nous écrivons des classes uniquement pour regrouper des constantes liées avec des fonctions qui agissent sur elles, nous pouvons ne jamais instancier ces classes du tout.Parfois, il est utile d'écrire une méthode de classe qui crée une instance de la classe après avoir traité l'entrée afin qu'elle soit dans le bon format pour être transmise au constructeur de classe. Cela permet au constructeur d'être simple et de ne pas avoir à implémenter de code d'analyse ou de nettoyage compliqué :<jupyter_code>class Person:

    def __init__(self, name, surname, birthdate, address, telephone, email):
        self.name = name
        # (...)

    @classmethod
    def from_text_file(cls, filename):
        # extract all the parameters from the text file
        return cls(*params) # this is the same as calling Person(*params)<jupyter_output><empty_output><jupyter_text>`@staticmethod`L'objet appelant n'est pas transmis à une méthode statique comme premier paramètre. Cela signifie qu'il n'a pas du tout accès au reste de la classe ou de l'instance. Nous pouvons les appeler à partir d'une instance ou d'un objet de classe, mais ils sont le plus souvent appelés à partir d'objets de classe, comme les méthodes de classe.Si nous utilisons une classe pour regrouper des méthodes liées qui n'ont pas besoin d'accéder les unes aux autres ou à d'autres données sur la classe, nous pouvons utiliser cette technique. L'avantage d'utiliser des méthodes statiques est que nous éliminons les paramètres `cls` ou `self` inutiles de nos définitions de méthodes. L'inconvénient est que si nous voulons occasionnellement faire référence à une autre méthode de classe ou à un autre attribut dans une méthode statique, nous devons écrire le nom de la classe en entier, ce qui peut être beaucoup plus détaillé que d'utiliser la variable `cls` qui nous est disponible dans un méthode de classe.Voici un exemple comparant les trois types de méthodes :<jupyter_code>class Person:
    TITLES = ('Dr', 'Mr', 'Mrs', 'Ms')

    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def fullname(self): # instance method
        # instance object accessible through self
        return f"{self.name} {self.surname}"

    @classmethod
    def allowed_titles_starting_with(cls, startswith): # class method
        # class or instance object accessible through cls
        return [t for t in cls.TITLES if t.startswith(startswith)]

    @staticmethod
    def allowed_titles_ending_with(endswith): # static method
        # no parameter for class or instance object
        # we have to use Person directly
        return [t for t in Person.TITLES if t.endswith(endswith)]


jane = Person("Jane", "Smith")

print(jane.fullname())

print(jane.allowed_titles_starting_with("M"))
print(Person.allowed_titles_starting_with("M"))

print(jane.allowed_titles_ending_with("s"))
print(Person.allowed_titles_ending_with("s"))<jupyter_output><empty_output><jupyter_text>`@property`Parfois, nous utilisons une méthode pour générer dynamiquement une propriété d'un objet, en la calculant à partir des autres propriétés de l'objet. Parfois, vous pouvez simplement utiliser une méthode pour accéder à un seul attribut et le retourner. Vous pouvez également utiliser une méthode différente pour mettre à jour la valeur de l'attribut au lieu d'y accéder directement. De telles méthodes sont appelées getters et setters, car elles « obtiennent » et « définissent » respectivement les valeurs des attributs.Dans certains langages, vous êtes encouragé à utiliser des getters et des setters pour tous les attributs, et à ne jamais accéder directement à leurs valeurs et il existe des fonctionnalités de langage qui peuvent rendre les attributs inaccessibles, sauf via les setters et les getters. En Python, accéder directement à des attributs simples est parfaitement acceptable, et écrire des getters et setters pour chacun d'eux est considéré comme inutilement verbeux. Les setters peuvent être gênants car ils ne permettent pas l'utilisation d'opérateurs d'affectation composés :<jupyter_code>class Person:
    def __init__(self, height):
        self.height = height

    def get_height(self):
        return self.height

    def set_height(self, height):
        self.height = height

jane = Person(153) # Jane is 153cm tall

jane.height += 1 # Jane grows by a centimetre
jane.set_height(jane.height + 1) # Jane grows again<jupyter_output><empty_output><jupyter_text>Comme nous pouvons le voir, incrémenter l'attribut `height` via un setter est beaucoup plus détaillé. Bien sûr, nous pourrions écrire un deuxième setter qui incrémente l'attribut par le paramètre donné, mais nous devrions faire quelque chose de similaire pour chaque attribut et chaque type de modification que nous voulons effectuer. Nous aurions un problème similaire avec les modifications sur place, comme l'ajout de valeurs aux listes.Quelque chose qui est souvent considéré comme un avantage des setters et des getters est que nous pouvons changer la façon dont un attribut est généré à l'intérieur de l'objet sans affecter le code qui utilise l'objet. Par exemple, supposons que nous ayons initialement créé une classe `Person` qui a un attribut de nom complet, mais que nous voulions plus tard changer la classe pour avoir des attributs de nom et de famille séparés que nous combinons pour créer un nom complet. Si nous accédons toujours à l'attribut `fullname` via un setter, nous pouvons simplement réécrire le setter, aucun code qui appelle le setter ne devra être modifié.Mais que se passe-t-il si notre code accède directement à l'attribut `fullname` ? Nous pouvons écrire une méthode de nom complet qui renvoie la bonne valeur, mais une méthode doit être appelée. Heureusement, le décorateur @property nous permet de faire en sorte qu'une méthode se comporte comme un attribut :<jupyter_code>class Person:
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    @property
    def fullname(self):
        return "%s %s" % (self.name, self.surname)

jane = Person("Jane", "Smith")
print(jane.fullname) # no brackets!<jupyter_output><empty_output><jupyter_text>Il existe également des décorateurs que nous pouvons utiliser pour définir un setter et un deleter pour notre attribut (un deleter supprimera l'attribut de notre objet). Les méthodes getter, setter et deleter doivent toutes avoir le même nom :<jupyter_code>class Person:
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    @property
    def fullname(self):
        return "%s %s" % (self.name, self.surname)

    @fullname.setter
    def fullname(self, value):
        # this is much more complicated in real life
        name, surname = value.split(" ", 1)
        self.name = name
        self.surname = surname

    @fullname.deleter
    def fullname(self):
        del self.name
        del self.surname

jane = Person("Jane", "Smith")
print(jane.fullname)

jane.fullname = "<NAME>"
print(jane.fullname)
print(jane.name)
print(jane.surname)<jupyter_output><empty_output><jupyter_text>Exercice 4Créez une classe appelée `Numbers`, qui a un seul attribut de classe appelé `MULTIPLIER`, et un constructeur qui prend les paramètres `x` et `y` (ceux-ci doivent tous être des nombres). - 1) Écrivez une méthode appelée `add` qui renvoie la somme des attributs `x` et `y`. - 2) Écrivez une méthode de classe appelée `multiply`, qui prend un seul paramètre numérique `a` et renvoie le produit de `a` et de `MULTIPLIER`. - 3) Écrivez une méthode statique appelée `subtract`, qui prend deux paramètres numériques : `b` et `c`, et renvoie `b - c`. - 4) Écrivez une méthode appelée `value` qui renvoie un tuple contenant les valeurs de `x` et `y`. Transformez cette méthode en propriété et écrivez un setter et un deleter pour manipuler les valeurs de `x` et `y`.  7. Inspecter un objetNous pouvons vérifier quelles propriétés sont définies sur un objet en utilisant la fonction `dir` :<jupyter_code>class Person:
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def fullname(self):
        return "%s %s" % (self.name, self.surname)

jane = Person("Jane", "Smith")

print(dir(jane))<jupyter_output><empty_output><jupyter_text>Maintenant, nous pouvons voir nos attributs et notre méthode. Mais qu'est-ce que c'est que tous ces autres trucs ? Nous discuterons de l'héritage dans le chapitre suivant, mais pour l'instant tout ce que vous devez savoir est que toute classe que vous définissez a un objet comme classe parent même si vous ne le dites pas explicitement, votre classe aura donc beaucoup d'attributs par défaut et les méthodes de tout objet Python.C'est pourquoi vous pouvez simplement omettre la méthode `__init__` de votre classe si vous n'avez aucune initialisation à faire, la valeur par défaut que vous avez héritée de object (qui ne fait rien) sera utilisée à la place. Si vous écrivez votre propre méthode `__init__`, elle remplacera (`override`) la méthode par défaut.De nombreuses méthodes et attributs par défaut que l'on trouve dans les objets Python de base ont des noms qui commencent et se terminent par des traits de soulignement doubles, comme `__init__` ou `__str__`. Ces noms indiquent que ces propriétés ont une signification particulière, vous ne devez pas créer vos propres méthodes ou attributs avec les mêmes noms, à moins que vous ne vouliez les surcharger. Ces propriétés sont généralement des méthodes, et elles sont parfois appelées méthodes magiques.Nous pouvons utiliser `dir` sur n'importe quel objet. Vous pouvez essayer de l'utiliser sur toutes sortes d'objets que nous avons déjà vus auparavant, comme des nombres, des listes, des chaînes et des fonctions, pour voir quelles propriétés intégrées ces objets ont en commun.Voici quelques exemples de propriétés d'objets spéciaux : - `__init__` : la méthode d'initialisation d'un objet, qui est appelée lors de la création de l'objet. - `__str__` : la méthode de représentation sous forme de chaîne d'un objet, qui est appelée lorsque vous utilisez la fonction `str` pour convertir cet objet en une chaîne. - `__class__` : un attribut qui stocke la classe (ou le type) d'un objet, c'est ce qui est renvoyé lorsque vous utilisez la fonction `type` sur l'objet. - `__eq__` : une méthode qui détermine si cet objet est égal à un autre. Il existe également d'autres méthodes pour déterminer si ce n'est pas égal, inférieur à, etc. Ces méthodes sont utilisées dans les comparaisons d'objets, par exemple lorsque nous utilisons l'opérateur d'égalité == pour vérifier si deux objets sont égaux. - `__add__` est une méthode qui permet d'ajouter cet objet à un autre objet. Il existe des méthodes équivalentes pour tous les autres opérateurs arithmétiques. Tous les objets ne prennent pas en charge toutes les opérations arithmétiques, les nombres ont toutes ces méthodes définies, mais d'autres objets peuvent n'avoir qu'un sous-ensemble. - `__iter__` : une méthode qui renvoie un itérateur sur l'objet, nous le trouverons sur des chaînes, des listes et d'autres itérables. Il est exécuté lorsque nous utilisons la fonction iter sur l'objet. - `__len__` : une méthode qui calcule la longueur d'un objet, on la retrouvera sur des séquences. Il est exécuté lorsque nous utilisons la fonction `len` d'un objet. - `__dict__` : un dictionnaire qui contient tous les attributs d'instance d'un objet, avec leurs noms comme clés. Cela peut être utile si nous voulons itérer sur tous les attributs d'un objet. `__dict__` n'inclut aucune méthode, attribut de classe ou attribut par défaut spécial comme `__class__`.  Exercice 5Créez une instance de la classe `Person` de l'exemple 2. Utilisez la fonction `dir` sur l'instance. Ensuite, utilisez la fonction `dir` sur la classe. - 1) Que se passe-t-il si vous appelez la méthode `__str__` sur l'instance ? Vérifiez que vous obtenez le même résultat si vous appelez la fonction `str` avec l'instance comme paramètre. - 2) Quel est le type de l'instance ? - 3) Quel est le type de classe ? - 4) Écrivez une fonction qui affiche les noms et les valeurs de tous les attributs personnalisés de tout objet transmis en tant que paramètre.  Remplacer les méthodes magiques<jupyter_code>class Person:
    def __init__(self, name, surname):
        self.name = name
        self.surname = surname

    def __str__(self):
        return f"{self.name} {self.surname}"

    def __eq__(self, other): # does self == other?
        return self.name == other.name and self.surname == other.surname

    def __gt__(self, other): # is self > other?
        if self.surname == other.surname:
            return self.name > other.name
        return self.surname > other.surname

    # now we can define all the other methods in terms of the first two

    def __ne__(self, other): # does self != other?
        return not self == other # this calls self.__eq__(other)

    def __le__(self, other): # is self <= other?
        return not self > other # this calls self.__gt__(other)

    def __lt__(self, other): # is self < other?
        return not (self > other or self == other)

    def __ge__(self, other): # is self >= other?
        return not self < other<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06384880180060903, 'fraction_numerical': 0.0025486561631139944, 'mean_word_length': 4.483303085299456, 'pattern_counts': {'":': 0, '<': 98, '<?xml version=': 0, '>': 100, 'https://': 0, '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': '22', 'dir': 'jupyter-structured-clean-dedup', 'id': '250236', 'license': 'Apache-2.0', 'n_tokens_mistral': 9436, 'n_tokens_neox': 9478, 'n_words': 4065, 'path': '_notebooks/2022-01-02-python-intro-gen-6-1-classes.ipynb', 'repo_name': 'AXI-Technologies/AXI-Fastpages', 'size': '30243'}