Décoration automatique de classes

a marqué ce sujet comme résolu.

Bonjour,

Je vais essayer d'expliciter au mieux mon problème, mais c'est pas gagné :)

Je travaille sur un projet où je cherche à dissocier l'interface graphique du cœur. En fait, pour chaque classe qui constituerait un composant du programme, j'aimerais pouvoir greffer une interface de façon facultative et transparente.

Disons que je dispose d'une telle classe :

1
2
3
class Menu:
    def __init__(self, *choices):
        self.choices = choices

La classe est utilisable en l'état. Mais si on imagine que j'ai chargé un autre module mettant en place une interface graphique, la classe Menu devra alors être spécialisée pour cette interface (placement des éléments, etc.) en une classe GUIMenu. Ça c'est pour le côté facultatif.

Au niveau de la transparence, le module contenant Menu pourrait contenir d'autres classes qui référencent Menu. Je ne voudrais cependant pas accéder à la classe Menu de base, mais à GUIMenu, sans avoir à en changer le nom.

J'ai trouvé deux solutions qui s'offraient à moi, que je présente ci-dessous.

Décorateur

La première serait l'utilisation d'un décorateur autour de chaque classe pouvant être spécialisée par l'interface, par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> class GUIMenu:
...     foo = 1
...
>>> def gui(cls):
...     return type(cls.__name__, (GUIMenu, cls), {})
...
>>> @gui
... class Menu: pass
...
>>> Menu.foo
1
>>> Menu.mro()
[<class '__main__.Menu'>, <class '__main__.GUIMenu'>, <class '__main__.Menu'>, <class 'object'>]

Ça fonctionne correctement, la transparence est totale. Seul problème, ça nécessite de décorer toutes les classes actuelles et futures, je trouve ça un peu lourd.

Je n'ai rien trouvé permettant de décorer automatiquement toutes les classes d'un module. Les seules solutions que j'ai lues interviennent après le chargement du module, et non après la création de chaque classe, ce qui fait qu'une classe référencée dans le module pointera vers sa version non décorée.

Métaclasse

Cette fois-ci, je sors l'artillerie lourde. Plutôt que de décorer ma classe, je lui applique une métaclasse qui créera d'elle-même la classe spécialisée. Ainsi, ma classe originale ne sera jamais référéncée dans le module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> class GUIMenu:
...     foo = 1
...
>>> class Meta(type):
...     def __new__(cls, name, bases, dict):
...         c = super().__new__(cls, name, bases, dict)
...         if issubclass(c, GUIMenu):
...             return c
...         return type(name, (GUIMenu, c), {})
...
>>> class Menu(metaclass=Meta): pass
...
>>> Menu.foo
1
>>> Menu.mro()
[<class '__main__.Menu'>, <class '__main__.GUIMenu'>, <class '__main__.Menu'>, <class 'object'>]

Cette solution me plaisait davantage, car toutes mes classes héritant déjà d'une classe commune, je peux aisément régler la métaclasse de tous mes composants.

Seulement, j'ai rencontré plus tard un aspect plus fourbe. Contrairement au décorateur, puisque la métaclasse intervient pendant la création, à l'intérieur même de celle-ci, __class__ ne référence pas la classe d'origine mais la nouvelle classe, ce qui m'empêche d'y utiliser super convenablement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> class GUIMenu:
...     def __init__(self):
...         super().__init__()
...
>>> class Menu(metaclass=Meta):
...     def __init__(self):
...         super().__init__()
...
>>> Menu() # BOUM
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  [...]
  File "<stdin>", line 3, in __init__
RecursionError: maximum recursion depth exceeded while calling a Python object

Bref, c'est assez gênant de devoir utiliser chaque fois explicitement le nom de la classe parente, et ça peut surtout être cause de nombreux bugs que je n'ai aucune envie de rencontrer.

Donc ?

Maintenant que mon/mes soucis sont présentés, j'en viens à ma question, à savoir quelle méthode préférer ? Et surtout, s'il existait une solution pour réunir les avantages de l'une et de l'autre :

  • Ne pas trop alourdir la syntaxe de définition des classes ;
  • Ne pas exploser à cause de super.

J'aimerais bien un décorateur qui s'applique à toutes les classes d'un module, ou une métaclasse se comportant comme un décorateur, mais je doute que ça existe.

Vos avis ?

Merci d'avance.

Salut,

Je ne comprends pas vraiment ce que tu fais dans Meta.__new__. D'après ce que je lis, tu regardes si la classe en cours de création hérite déjà de la classe GUIMenu, si c'est le cas tu renvoie la classe, jusqu'ici pas de problème. C'est quand la classe n'hérite pas de GUIMenu que j'ai plus d mal à comprendre. Déjà le truc bizarre c'est que tu ne renvoie pas une instance de Meta mais de type, mais ce qui me choque le plus c'est que la classe renvoyée hérite de la classe en cours de création.

Peut-être qu'il y a une subtilité qui m'échappe mais ça me paraît étrange.

AZ.

En fait, comme la métaclasse se propage aux classes filles, l'appel à type fait lui-même appel à Meta. (ce qui explique que je doive vérifier si ma classe n'hérite pas déjà de GUIMenu, sinon je serais bon là aussi pour une récursion infinie).

Le comportement est peut-être plus simple à comprendre avec un peu de debug.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> class Meta(type):
...     def __new__(cls, name, bases, dict):
...         print(name, bases, dict)
...         c = super().__new__(cls, name, bases, dict)
...         if issubclass(c, GUIMenu):
...             return c
...         return type(name, (GUIMenu, c), {})
... 
>>> class Menu(metaclass=Meta): pass
... 
Menu () {'__qualname__': 'Menu', '__module__': '__main__'}
Menu (<class '__main__.GUIMenu'>, <class '__main__.Menu'>) {}

On passe dans la métaclasse une première fois pour la classe en cours de création, et une seconde fois pour la création de la classe décorée.

Le fait que je renvoie une classe qui hérite de l'actuelle est le comportement voulu. Elle se comporte ainsi de la même manière, et hérite aussi de la classe GUIMenu, ce qui permet d'étendre ses fonctionnalités.

Oui mais du coup une classe qui hérite d'elle même c'est étrange, non ? Pourquoi pas juste prendre l'environnement de Menu au lieu de passer par l'héritage ?

Tous ces trucs de métaclasses c'est pas simple à expliquer, c'est plus simple avec du code. J'envisagerai un truc comme ça plutôt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class GUIMeta(type):
    def __new__(mcs, name, bases, attr):
        if GUIMenu in bases:
            return super().__new__(mcs, name, bases, attr)

        return super().__new__(mcs, name, (GUIMenu, *bases), attr)


class GUIMenu:
    def foo(self):
        print('foo')


class Menu(metaclass=GUIMeta):
    pass

# >>> m = Menu()
# >>> m.foo()
# foo
# >>> Menu.mro()
# [<class '__main__.Menu'>, <class '__main__.GUIMenu'>, <class 'object'>]

J'ai testé ça fonctionne, après je n'ai peut-être pas compris ton problème :euh:

+0 -0

Oui mais du coup une classe qui hérite d'elle même c'est étrange, non ? Pourquoi pas juste prendre l'environnement de Menu au lieu de passer par l'héritage ?

AlphaZeta

La classe n'hérite pas vraiment d'elle-même. On crée une classe Menu', qui hérite de Menu et la remplace. C'est confus parce que les deux classes ont le même nom, mais elles sont différentes l'une de l'autre.

Je vais réfléchir à ta solution, mais en l'état ça me gêne parce que l'ordre d'appel des méthodes est inversé.

Edit: Ça cause aussi des problèmes de MRO dès que l'on tente d'hériter de Menu. La solution est alors de placer GUIMenu tout à droite dans l'héritage, mais ça risque de poser d'autres problèmes.

Edit2: Des soucis comme le fait de ne pas pouvoir utiliser super sans savoir s'il existe une classe mère qui implémente la méthode.

Ça y est je crois que j'ai compris. J'ai essayé de surcharger la méthode mro dans GUIMeta et ça a l'air de marcher:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class GUIMeta(type):
    def __new__(mcs, name, bases, attr):
        if GUIMenu not in bases:
            cls = super().__new__(mcs, name, (GUIMenu,) + bases, attr)
        else:
            cls = super().__new__(mcs, name, bases, attr)

        return cls

    def mro(cls):
        default = super().mro()
        default.remove(GUIMenu)
        default.insert(0, GUIMenu)
        return default


class GUIMenu:
    def foo(self):
        print('foo')


class Menu(metaclass=GUIMeta):
    def foo(self):
        print('bar')


if __name__ == '__main__':
    m = Menu()
    print(m)
    print(Menu.mro())
    m.foo()

Ça me donne ceci:

1
2
3
<__main__.Menu object at 0x7f0f323b3198>
[<class '__main__.GUIMenu'>, <class '__main__.Menu'>, <class 'object'>]
foo

EDIT: j'ai pas encore essayé avec super

EDIT 2: C'est assez étrange que Python ne regarde pas plutôt l'attribut __mro__, étant donné qu'il est protégé. Je me demande si une volonté des devs Python ou pas.

EDIT 3: Tiens non en fait __mro__ est aussi modifié.

+0 -0

Je n'ai pas tout compris et qui puis est le plus important, l'intérêt, mais je tente ma chance, car en relisant, j'ai l'impression que tu cherches à créer une classe abstraite et ses méthodes, mais je me suis sans doute trompé, tellement je suis peu sûr de ce que tu veux.

Voici une approche en code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from abc import ABCMeta, abstractmethod

class Menu(metaclass=ABCMeta):

    @abstractmethod
    def __init__(self):
        pass

class GuiMenu(Menu):
    def __init__(self):
        self.foo = 1

Je n'ai pas tout compris et qui puis est le plus important, l'intérêt, mais je tente ma chance, car en relisant, j'ai l'impression que tu cherches à créer une classe abstraite et ses méthodes, mais je me suis sans doute trompé, tellement je suis peu sûr de ce que tu veux.

fred1599

Ça ne correspond pas vraiment à une classe abstraite, non.

Je vais expliciter mon problème autrement en prenant l'exemple d'un jeu, mais qui serait totalement abstrait de l'interface graphique. On pourrait alors spécialiser les classes du jeu pour afficher les objets dans un environnement OpenGL, ou une projection plus simple genre 2D isométrique avec une autre bibliothèque graphique. Le cœur du jeu n'aurait pas à connaître les détails de l'interface, et fonctionnerait de la même manière qu'on utilise l'une ou l'autre (voire aucune).

Si j'ai une classe qui représente un personnage, celle-ci contiendra naturellement sa position dans l'espace. Mais l'interface 2D ajoutera par exemple un sprite, la position de ce sprite sur la fenêtre, et autres éléments qui lui seraient utiles. Ainsi, quand on interagirait avec un personnage pour le déplacer, la méthode de déplacement de la classe de base et de la classe GUI devront toutes deux être appelées (la première pour gérer les collisions et mettre à jour la position, la seconde pour rafraîchir le sprite).

J'espère que c'est plus clair comme ça.

Pourquoi pas faire un modèle MVC dans le cas du jeu ? Ainsi, les classes représentant les entités du jeu seraient les modèles, le cœur du jeu serait le contrôleur, et les classes de l'interface graphiques encapsuleraient les modèles.

A priori si décorer toutes les classes de ton projet est trop fastidieux, alors je pense qu'il est sera encore plus long de créer une vue pour chacune des classes, mais je dis ça comme ça.

Il me semble que dans ton projet tu souhaites fusionner les classes de vue et les classes de modèle pour reprendre l'image du pattern MVC.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class View:
    def display(self):
        if isinstance(self, Player):
            print(self.name)


class ModelMeta(type):
    def __new__(mcs, name, bases, attrs):
        if View not in bases:
            bases += (View,)

        return super().__new__(mcs, name, bases, attrs)


class Model(metaclass=ModelMeta):
    pass


class Player(Model):
    def __init__(self, name):
        self.name = name


if __name__ == '__main__':
    player = Player('Patrick')
    player.display()

Encore une fois c'est assez difficile de voir l'étendue du problème lorsqu'on a pas l'architecture du projet en face.

Oui, je pense que ça correspond assez bien au MVC, c'est en tout cas ce que j'ai essayé de reproduire.

Mais oui, je souhaite que tout en étant séparées, les modèles et les vues se trouvent réunis dans une même classe (pas vraiment fusionnés, puisqu'il ne s'agit que de créer une nouvelle classe héritant du modèle et de la vue).

Avec vues et modèles séparés, je devrais maintenir une hiérarchie de vues semblable à celle des modèles, faire en sorte que les vues soient mises à jour à chaque changement sur les modèles, etc. Ça me paraît difficilement gérable.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte