Autour du codeDevelopper toujours mieux
Posté le

Les classes mixins

Les mixins sont des classes vouées à être assemblées entre elles pour créer d'autre classe par héritage. Cela peut paraitre simple, mais ça peut vite être piégeux.

Premier piège, ma mixin à besoin d'attributs d'instance pour fonctionner.

class FooMixin:
    def foo(self):
        self.foo_attr.append(33)
        print(self.foo_attr)

class BarMixin:
    def bar(self):
        self.bar_attr.append(21)
        print(self.bar_attr)


class FooBar(FooMixin, BarMixin):
    """
    FooBar class
    """

FooBar().foo() # AttributeError: 'FooBar' object has no attribute 'foo_attr'

On pourrait créer un constructeur dans FooMixin et BarMixin pour initialiser foo_attr et bar_attr, mais cela obligerai l'utilisateur des mixins à les appeler dans sa classe FooBar

class FooBar(FooMixin, BarMixin):
    def __init__(self):
        FooMixin.__init__(self)
        BarMixin.__init__(self)

Une solution pour simplifier la vie de notre utilisateur et de regarder si l'attribut existe et s'il n'existe pas on, le crée.

class FooMixin:
    def foo(self):
        if not hasattr(self, 'foo_attr'):
            self.foo_attr = []
        self.foo_attr.append(33)
        print(self.foo_attr)

class BarMixin:
    def bar(self):
        if not hasattr(self, 'bar_attr'):
            self.bar_attr = []
        self.bar_attr.append(21)
        print(self.bar_attr)

class FooBar(FooMixin, BarMixin):
    pass

f = FooBar()
f.foo() # [33]
f.foo() # [33, 33]

Une dernière chose... Si vous souhaitez éviter ceci:

class FooBar(FooMixin, BarMixin):
    foo_attr = 1

f = FooBar()
f.foo() # AttributeError: 'int' object has no attribute 'append'

Et que vous ne souhaitez pas que l'utilisateur puisse initialiser l'attribut il suffit de le préfixer de deux underscores.

class FooMixin:
    def foo(self):
        if not hasattr(self, '_FooMixin__foo_attr'):
            self.__foo_attr = []
        self.__foo_attr.append(33)
        print(self.__foo_attr)

Pour finir, voyons un cas plus complexe, le conflit entre les noms des méthodes. Imaginions que l'on souhaite faire un jeu avec des combats entre créatures. Les créatures ont des points de vie et de force.Il y a une mixin vampire la créature regagne de la vie quand elle attaque, puis une mixin Apprenti, a chaque attaque la créature gagne en force.

class Apprenti:
    def attaquer(self, other):
        if not hasattr(self, "_Croissant__extra"):
            self.__extra = 0
            other.vie -= (self.force + self.__extra)
        self.__extra += 2


class Vampire:
    def attaquer(self, other):
        vie_avant = other.vie
        other.vie -= self.force
        self.vie += (vie_avant - other.vie) // 2

Le problème et que si l'on compose une créature avec c'est deux classes, c'est soie la méthode attaquer de Vampier soie celle d'Apprenti qui sera appelée. Or on veut une créature qui fait les deux.

class ApprentiVampire(Apprenti, Vampire):
    "Se comporte comme un Apprenti"

class ApprentiVampire(Vampire, Apprenti):
    "Se comporte comme un Vampire"

La solution et que la mixin regarde dans les classes parents si elle trouve une autre méthode attaquer et de l'appeler au bon moment, par contre on va se retrouver avec des appels sans fin si l'on fait ça n'importe comment. Voici comment on peut s'y prendre:

class Apprenti:
    def attaquer(self, other):
        if not hasattr(self, "_Apprenti__extra"):
            self.__extra = 0

        # Je prends le tuple des classes parents
        parents = type(self).__bases__
        # Je ne regarde que les classes après moi dans le tuple
        i = parents.index(Apprenti) + 1
        for parent in parents[i:]:
            # Si une classe a une méthode attaquée
            # j'exécute sa méthode.
            if hasattr(parent, 'attaquer'):
                self.force += self.__extra
                parent.attaquer(self, other)
                self.force -= self.__extra
                break
        else:
            other.vie -= (self.force + self.__extra)
        self.__extra += 2

class Vampire:
    def attaquer(self, other):
        vie_avant = other.vie
        parents = type(self).__bases__
        i = parents.index(Vampire) + 1
        for parent in parents[i:]:
            if hasattr(parent, 'attaquer'):
                parent.attaquer(self, other)
                break
        else:
            other.vie -= self.force

        self.vie += (vie_avant - other.vie) // 2

class ApprentiVampire(Apprenti, Vampire):

    def __init__(self):
        self.force = 10
        self.vie = 20

    def __str__(self):
        return "force:%s vie:%s" % (self.force, self.vie)

class SimpleCreature:
    def __init__(self):
        self.vie = 30


vampire = ApprentiVampire()
creature = SimpleCreature()

print(vampire.vie, creature.vie) # 20 30
vampire.attaquer(creature)
print(vampire.vie, creature.vie) # 25 20
vampire.attaquer(creature)
print(vampire.vie, creature.vie) # 31 8
vampire.attaquer(creature)
print(vampire.vie, creature.vie) # 38 -6

Pour conclure, les mixin peuvent vous permette de créer une libraire sympa où l'utilisateur aura juste à assembler des classes pour avoir une classe sur-mesure. L'inconvénient c'est que c'est pas mal de taf en amont et que c'est statique. Si les comportements ont besoin d'évoluer dynamiquement, il vaut mieux envisager le design pattern décorateur.

J'espère que ça vous a plu alors à vos IDE et bon code ! smile