composition – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le guide ultime et définitif sur la programmation orientée objet en Python à l’usage des débutants qui sont rassurés par les textes détaillés qui prennent le temps de tout expliquer. Partie 5. http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-5/ http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-5/#comments Mon, 11 Feb 2013 17:38:28 +0000 http://sametmax.com/?p=4495 Maestro, musique !

Prérequis :

La POO, c’est comme les poupées russes. Une fois qu’on a maîtrisé un concept, paf, y en a un autre qui se ramène derrière.

J’ai plusieurs mamans

En ces temps de polémique sur l’autorisation du mariage entre êtres amoureux et consentants (par opposition à celui qu’on fait par convention sociale depuis des siècles), je vous propose de vous rappeler qu’une fois de plus les informaticiens sont en avance sur les mœurs.

D’abord parce qu’une communauté qui fait autant de links vers Never Gona Give You Up est forcément pro gay par essence. Ensuite parce qu’une classe fille peut avoir plusieurs classes mères sans que ça choque personne.

Prenez ces deux classes qui passaient par là sans rien demander :

class MouMoutte(object):

    type = 'top' # c'est tip top moumoutte

class Raoul(object):

    trop = 'cool'

Hé merde, j’ai écris ça, et maintenant j’ai aucune idée de comment faire un code cohérent à partir de cet exemple à la con.

On annule tout.

On recommence.

Vous faites un jeu vidéo. Ça, ça parle bien. Et dedans vous avez des protections et des armes.

class Arme(object):

    def __init__(self, nom, degat):

        self.nom = nom
        self.degat = degat

    def attaque(self, cible): # on retire les degâts de l'épee des points de vie
        cible.vie -= self.degat


class Protection(object):

    def __init__(self, nom, armure):

        self.nom = nom
        self.armure = armure

    def defend(self, degat): # on diminue les degâts, voire on les annule

        degat = degat - self.armure
        if degat < 0:
            return 0

        return degat

>>> epee = Arme('Epée Mana', degat=999)
>>> casque = Protection('Casque de Balduran', armure=1)

C’est simpliste, mais vous voyez le tableau. Maintenant un connard de client arrive et vous sort une idée trop cool : il faudrait ajouter un barbare dans le jeu. Qui tape aussi avec son bouclier, parce que la concurrence le fait et qu’ils veulent pas se faire mettre un vent par Blizzard.

Enfer et Rutabaga ! Comment allons nous nous sortir de cette situation ?

Il y a moult manières de faire, mais l’une d’elle est d’utiliser l’héritage multiple, c’est-à-dire de créer une classe qui hérite des deux classes en même temps.

class ProtectionOffensive(Arme, Protection):

    def __init__(self, nom, degat, armure):

        Arme.__init__(self, nom, degat) # appelle le __init__ de arme
        Protection.__init__(self, nom, armure) # appelle le __init de protection

        # comme on a appelé les deux __init__, on va avoir les attributs
        # settés dans les deux __init__ attachés à cette classe

Nous avons alors une classe qui possède les méthodes des deux classes parentes :

>>> bouclier = ProtectionOffensive('Bouclier du dragon', degat=10, armure=100)
>>> bouclier.degat
10
>>> bouclier.armure
100
>>> bouclier.defend(10)
0

Ne cherchez pas compliqué, ça fait exactement ce que ça à l’air de faire : “copier” (oui bon, entre guillemets) le code de chaque parent dans l’enfant.

Néanmoins vous avez vu qu’il y a quelques subtilités, notamment la partie __init__.

Posez-vous deux minutes. Respirez. Concentrez-vous. Prêt ?

Les deux classes parentes ont une méthode __init__, mais Python ne peut en “copier” qu’une seule dans l’enfant. Il copie donc la première qu’il trouve. Il va prendre la liste des parents (ici: Arme, Protection), et la lire de gauche à droite. Il va regarder chaque parent, et si la méthode existe, il va la “copier” dans l’enfant.

Si il retrouve une méthode de même nom dans un des parents suivants, il l’ignore. (Je dis un DES parents suivants car vous pouvez avoir 10 parents si vous voulez).

Donc dans notre exemple, si je fais :

class ProtectionOffensive(Arme, Protection):
    pass

ProtectionOffensive n’aura que la méthode __init__ de Arme. Or ce n’est pas ce qu’on veut. On va donc overrider la méthode __init__, et dedans appeler la méthode __init__ de Arme ET celle de Protection.

Cette syntaxe : Classe.methode(self, args...) que l’on retrouve dans Arme.__init__(self, nom, degat) est juste un moyen d’appeler spécifiquement la méthode du parent.

Dans la partie précédente, je vous ai montré qu’on pouvait faire cela avec super(). Or super() vous retournera la première méthode du premier parent qu’elle trouve : c’est le but de super(), de faire ça automatiquement sans se soucier de savoir qui est le premier parent à avoir une méthode du bon nom.

C’est utile car parfois c’est le parent du parent du parent qui a la méthode qu’on veut appeler. On ne connaît pas forcément son nom, ou alors on ne veut pas l’écrire en dur. Mais dans notre cas, on veut spécifiquement une méthode d’un parent en particulier, il faut donc l’écrire à la main.

D’une manière générale :

  • Utilisez super() quand vous faites de l’héritage simple où que vous voulez juste appeler la méthode du premier parent venu sans vous soucier de son nom (car il peut être très haut dans la chaîne d’héritage).
  • Utilisez Classe.methode(self, args...) quand vous voulez spécifiquement appeler la méthode d’un parent en particulier.

Faites attention !

Le self n’est pas au même endroit dans super(ClassCourante, self).methode(args...) et ClasseParente.methode(self, args...). Et dans le premier cas, on passe la classe courante (que super() va analyser pour trouver les parents automatiquement), dans le cas suivant, on écrit le nom de la classe parente en dur.

Faites quelques tests avec des scripts bidons pour bien comprendre comment ça marche. Faites ça avec des classes toutes simples. Sinon le jour où vous aurez une classe compliquée, vous allez vous embrouiller.

La composition (POO pour les vrais mecs)

Jusqu’ici c’était un tuto avec des notions de base pour des petits geeks imberbes qui jouent avec des action figures fabriquées en Chine et achetées sur ebay. Mais maintenant nous allons voir la POO pour les vrais hommes, les barbus, ceux qui jouent avec des reals dolls et qui n’ont pas peur de mettre des chaussettes dépareillées.

Voyez-vous, un objet, tout seul, il sert à rien. Il s’emmerde déjà, rien à foutre le samedi, nul part où sortir, tout ça. Mais surtout, il a personne pour prendre l’apéro. Non, dans un programme digne d’un vrai pastis, il faut plusieurs objets qui interagissent entre eux. En fait, plusieurs objets qui s’utilisent les uns les autres.

Retournez à notre exemple de jeu video :

class HeroViril(object):

    # def __init__(self, nom, prenom, groupe_sanguin, signe_astrologique,
    #              couleur_preferee, tendance_sexuelle, culte,
    #              taille_de_la_troisieme_phallange_de_l_index_gauche)
    # TODO : voir le CDC avec le client pour confirmer les attributs du personnage
    def __init__(self, nom, vie, arme=None, protection=None):

        self.nom = nom
        self.vie = vie
        self.arme = arme
        self.protection = protection

    def combattre(self, ennemi):

        print "{} attaque {}".format(self.nom, ennemi.nom)

        while True:

            if self.arme:
                self.arme.attaque(ennemi)

            if ennemi.vie <= 0:
                break

            if ennemi.arme:
                ennemi.arme.attaque(self)

            if self.vie <= 0:
                break

        if self.vie > 0:
            print "Victoire de {}".format(self.nom)
        else:
            print "{} est mort comme une pov' merde".format(self.nom)

Et là vous notez un truc, c’est que nous n’avons pas de méthode attaque() sur notre héros. Nous utilisons la méthode attaque d’un objet arme. Que l’on a en attribut.

C’est cela la composition : un objet, qui en fait est composé de plusieurs sous-objets. Dans notre cas, notre objet héros est aussi composé d’une arme et d’une protection, qui sont ses attributs. Il peut ainsi utiliser le comportement de ses objets pour faire le boulot à sa place : c’est ce qu’on appelle la délégation.

Reprenons notre code des armes, un peu adapté :

# le code de l'armure ne change pas
class Protection(object):

    def __init__(self, nom, armure):

        self.nom = nom
        self.armure = armure

    def defend(self, degat):

        degat = degat - self.armure
        if degat < 0:
            return 0

        return degat


# on change le code de l'arme, si la cible a une protection
# cela diminue les degâts pris
class Arme(object):

    def __init__(self, nom, degat):

        self.nom = nom
        self.degat = degat

    def attaque(self, cible):

        # je mets aussi quelques prints pour le lulz
        if cible.protection:
            degat = cible.protection.defend(self.degat)
            print "{} - {} = {}".format(cible.vie, degat, cible.vie - degat)
            cible.vie -= degat
        else:
            print "{} - {} = {}".format(cible.vie, self.degat, cible.vie - self.degat)
            cible.vie -= self.degat

Maintenant créons deux héros, armons-les, et faisons-les combattre :

>>> gosu = HeroViril("Drizzt Do'Urden", 2000)
>>> gosu.arme = Arme('Lame Vorpale', 10)
>>> gosu.protection = Protection("Maille en Kevlar de mithril doré a l'adamantium", 10)
>>> noob_qui_repop = HeroViril("Bob", 200)
>>> noob_qui_repop.arme = Arme('Cure-dent', 1)
>>> noob_qui_repop.protection = Protection("Slip", 1)
>>> noob_qui_repop.combattre(gosu) # yaaaaaaaaaaaaaaaaaaaaaaa !

Bob attaque Drizzt Do'Urden
2000 - 0 = 2000
200 - 9 = 191
2000 - 0 = 2000
191 - 9 = 182
2000 - 0 = 2000
182 - 9 = 173
2000 - 0 = 2000
173 - 9 = 164
2000 - 0 = 2000
164 - 9 = 155
2000 - 0 = 2000
155 - 9 = 146
2000 - 0 = 2000
146 - 9 = 137
2000 - 0 = 2000
137 - 9 = 128
2000 - 0 = 2000
128 - 9 = 119
2000 - 0 = 2000
119 - 9 = 110
2000 - 0 = 2000
110 - 9 = 101
2000 - 0 = 2000
101 - 9 = 92
2000 - 0 = 2000
92 - 9 = 83
2000 - 0 = 2000
83 - 9 = 74
2000 - 0 = 2000
74 - 9 = 65
2000 - 0 = 2000
65 - 9 = 56
2000 - 0 = 2000
56 - 9 = 47
2000 - 0 = 2000
47 - 9 = 38
2000 - 0 = 2000
38 - 9 = 29
2000 - 0 = 2000
29 - 9 = 20
2000 - 0 = 2000
20 - 9 = 11
2000 - 0 = 2000
11 - 9 = 2
2000 - 0 = 2000
2 - 9 = -7
Bob est mort comme une pov' merde

Ok, j'ai pigé le principe, mais comment ça marche dans le détail ?

Regardons la méthode combattre de plus près :

    # elle attend un ennemi en paramètre, donc UN OBJET HeroViril
    # self est l'objet en cours, donc aussi un objet HeroViril
    def combattre(self, ennemi):

        print "{} attaque {}".format(self.nom, ennemi.nom)

        # une petite boucle infinie. Warning, c'est un tuto. Ne faites pas
        # ça chez vous les enfants.
        # cette boucle loop pour toujours si il n'y a pas d'attribut arme donc
        # ceci n'est qu'un exemple. Hein ? Noté ? Les deux du fond là ?
        while True:

            # on donne le premier coup à la personne qui attaque (l'objet en
            # cours). On vérifie qu'il a une arme. Si c'est le cas,
            # on appelle la méthode de l'arme "attaque()", et on lui passe
            # en paramètre l'ennemi.
            if self.arme:
                self.arme.attaque(ennemi)

            # condition de sortie de la boucle sur la vie du héros qui a pris
            # le coup
            if ennemi.vie <= 0:
                break

            # ensuite on fait pareil à l'envers pour donner une chance à l'autre
            # de répliquer : on vérifie que l'ennemi a une arme, et si c'est
            # le cas, on applique la méthode "attaque" de l'arme à l'objet
            # en cours
            if ennemi.arme:
                ennemi.arme.attaque(self)

            # condition de sortie de la boucle sur la vie du héros qui a pris
            # le coup
            if self.vie <= 0:
                break

        # une fois sorti de la boucle, on vérifie le niveau de vie pour
        # désigner le vainqueur
        if self.vie > 0:
            print "Victoire de {}".format(self.nom)
        else:
            print "{} est mort comme une pov' merde".format(self.nom)

Donc combattre utilise un objet arme, et appelle sa méthode attaque() sur un héros (j'ai viré les prints pour rendre le truc plus clair) :

    # self est l'objet en cours, donc l'arme.
    # cible est un héros, puisqu'on l'a passé en paramètre.
    def attaque(self, cible):

        # si la cible (l'objet héros) a un attribut protection,
        # les dégâts retirés sont diminués (ce calcul est fait par la protection)
        if cible.protection:
            cible.vie -= cible.protection.defend(self.degat)

        # sinon, on retire les dégâts à la vie de la cible (le héros)
        # directement
        else:
            cible.vie -= self.degat

Diantre ! La méthode attaque utilise elle-même la méthode defend() de la protection :

    # self est l'objet en cours, donc la protection.
    # degat est un simple int.
    def defend(self, degat):
        # on retourne les degâts infligés, moins la protection
        return degat - self.armure

Pour comprendre tous ces codes, il faut bien piger deux trucs :

  • Il y a 6 objets. Deux héros, deux armes, et deux protections. Les héros ont les armes / protections comme attributs.
  • On se sert des méthodes des armes pour attaquer. On passe les héros en paramètres à ces méthodes. Les armes se servent des protections des héros pour calculer les dégats.

Ce dernier point est le plus important. Si vous comprenez ça, vous avez maîtrisé le plus important de la POO. Relisez le plusieurs fois :

Les héros ont une référence aux armes. Et ensuite, on passe une référence des héros aux armes. Les armes retirent de la vie à ces héros, non sans calculer les dégâts en fonction de la protection qu'ils portent.

Les objets ont tous des références les uns vers les autres. Ils se manipulent tous les uns les autres.

Cela fait bizarre car dans la vie une épée ne manipule pas un héros (bon, je connais peu d'elfes noirs IRL aussi). On comprend facilement qu'un héros ait un attribut 'épée', mais il est difficile de comprendre qu'une épée ait une méthode, et que le paramètre de cette méthode soit un héros.

C'est un concept purement informatique : la logique des dégâts est codée dans l'arme, pas dans le héros. L'avantage de cette architecture, c'est que si vous changez l'arme, vous changez toute la logique des dégâts. Par exemple, vous rajoutez une arme empoisonnée :

class ArmeMegaEmpoisonnee(Arme):

    def __init__(self, nom, degat, poison=100000):
        super(ArmeMegaEmpoisonnee, self).__init__(nom, degat)
        self.poison = poison


    def attaque(self, cible):

        # on prend les degâts une premiere fois
        super(ArmeMegaEmpoisonnee, self).attaque(cible)

        # puis on prend les dégâts du poison
        cible.vie -= self.poison

Cette arme fait plus de dégâts. Son mécanisme pour faire des dégâts est différent. Il suffit d'équiper un héros avec l'arme (en changeant l'attribut) pour que ce nouveau calcul de dégâts soit pris en compte.

>>> noob_qui_repop.vie = 200 # rePOP !
>>> noob_qui_repop.arme = ArmeMegaEmpoisonnee('Cheat Code', 1)
>>> noob_qui_repop.combattre(gosu) # Vengeance !
Bob attaque Drizzt Do'Urden
2000 - 0 = 2000
Victoire de Bob

Ce qu'il faut retenir : on peut mettre des objets en tant qu'attributs d'objets. Il n'y a pas de limite dans les nombres d'objets à mettre, leur mélange, les niveaux d'imbrications, etc. On peut mettre des objets, dans des objets, dans des objets... C'est la composition.

Et les objets peuvent utiliser les méthodes des autres objets. Et on peut passer des objets comme paramètres à des méthodes. C'est la délégation.

C'est la partouze des objets, quoi.

On peut mettre des objets dans des sets, des dicos, des listes... Par juste dans des attributs. Il y en a des choses à faire !

Choisir entre l'héritage et la composition

Les deux techniques permettent de réutiliser du code, mais pas de la même façon. Aucune règle générale ne tient la route dans tous les cas, mais un bon point de départ est de se dire que :

  • Si vous avez deux objets de même nature et que l'un est une spécialisation de l'autre (Garçon est une spécialisation de Personne, Voiture est une spécialisation de véhicule, Clio est une spécialisation de voiture, Reptincel est une spécialisation de pokemon, foxmask est une spécialisation du correcteur orthographique de Word, etc), alors utilisez l'héritage.
  • Si vous avez deux objets qui échangent des données, qui sont associés ou qui dans la vie réelle sont des 'part de' (un article est une partie d'un blog, Raymond Barre est membre d'un parti, Raymond fait partie d'un bar, le bar et la raie font parti des poissons ah non ça c'est une spécialisation, etc), utilisez la composition.

Il y a aussi, quelque part, dans le lointain pays des enculeurs de mouche, une différence entre l’agrégation (qu'on a pas vu) et la composition. Vous vivrez très bien en considérant que c'est la même chose.

Le design pattern "stratégie"

Vous entendrez parfois parler du motif de conception "stratégie". C'est en fait une mise en application abstraite de la composition.

Normalement la composition s'utilise avec des "part de" concrètes. Vous avez une voiture : elle est composée d'objets pneus, d'un objet moteur, etc.

Le design pattern stratégie est l'extraction d'une part du comportement d'un objet pour le mettre dans un autre objet, mais la nature de l'objet importe peu. Ceci est fait purement pour découpler le comportement de l'objet.

On a vu plus haut que changer l'arme permet de changer le calcul des dégâts. C'est ce type de résultat qu'on vise avec le design pattern strategy.

import os

class ParseurXml(object):
    ...

class ParseurJson(object):
    ...


class ParseurDeFichier(object):

    _strategy = { # les stratégie par défaut
        'json': ParseurXml,
        'xml': ParseurJson
    }

    def __init__(self, fichier, strategy=None):

        self.fichier = fichier
        # on récupère l'extension du fichier
        path, ext = os.path.splitext(fichier)

        # Strategy est une classe de parseur
        # on la récupère depuis les paramètres ou selon
        # l'extension
        Strategy = strategy or self._strategy[ext.lstrip('.')]
        # on instancie notre classe de strategie
        self.strategy = Strategy(fichier)

    def parse(self):
        # on délègue le boulot à la stratégie
        self.strategy.parse()

La ligne la plus importante est :

Strategy = strategy or self._strategy[ext]

Ici on dit récupérer la stratégie de parsing en paramètre, ou sinon, la bonne en fonction de l'extension de fichier. On charge donc une classe dynamiquement, on va créer un objet à partir de cette classe. Et c'est cet objet à qui on va déléguer le comportement du parseur :

    def parse(self):
        self.strategy.parse()

On utilise l'objet dynamiquement pour gérer tout le parsing. On peut ainsi choisir un parseur à la volée.

La pattern strategy mélange donc composition (l'objet strategy est une part de l'objet général), délégation (l'objet général utilise le comportement de l'objet strategy) et d'injection de dépendance (on peut changer l'algo à la volée, il suffit de changer de stratégie).

Bon, ça c'était le mode hard. C'est pas grave si vous finissez pas le niveau toute de suite. Mettez pause. Allez pisser un coup et revenez- plus tard, les continues sont infinis sur le blog.

C'est bon, vous êtes chaud ? Prochaine stations, le monde merveilleux des méthodes magiques.

]]>
http://sametmax.com/le-guide-ultime-et-definitif-sur-la-programmation-orientee-objet-en-python-a-lusage-des-debutants-qui-sont-rassures-par-les-textes-detailles-qui-prennent-le-temps-de-tout-expliquer-partie-5/feed/ 64 4495