IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Python Discussion :

décorer des classes, et un peu plus.


Sujet :

Python

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert
    Homme Profil pro
    Inscrit en
    Avril 2004
    Messages
    1 107
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Avril 2004
    Messages : 1 107
    Par défaut décorer des classes, et un peu plus.
    Bonjour,

    j'ai récemment apris qu'il est possible de décorer des classes (merci fred1599) et j'ai l'intuition que ça peu m'aider.
    Voici, jusqu'ici j'avais en schématisant:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class A:
        def draw():
            ...
        def drawWrapper():
            ...
     
    class B(A):
        @A.drawWrapper
        def draw():
            ...
    y aurait pas moyen de faire un truc genre:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A:
        def draw():
            ...
        def drawWrapper():
            ...
     
    @undecorateurpourA
    class B():
     
        def draw():
            ...
    qui ferait magiquement hériter de A et décorer B.draw ?

    Merci.

  2. #2
    Expert confirmé
    Homme Profil pro
    Responsable Données
    Inscrit en
    Janvier 2009
    Messages
    5 559
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Responsable Données

    Informations forums :
    Inscription : Janvier 2009
    Messages : 5 559
    Par défaut
    Bonjour,
    De ce que je comprends dans les documentations sur l'héritage en python que j'ai trouvé, ce code devrait ressembler à ceci :
    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     
    class A:
        def draw():
            ...
        def drawWrapper():
            ...
     
    class B(A):
        def draw():
            ...
    Si tu crées une instance de B, l'invocation de la méthode draw() utilisera l'implémentation de B, puisqu'il n'y a pas de surcharge, tandis que l'invocation de drawWrapper() utilisera celle de A.
    A moins que j'ai raté quelque chose...

    Tatayo.

  3. #3
    Membre Expert
    Profil pro
    Inscrit en
    Septembre 2010
    Messages
    1 769
    Détails du profil
    Informations personnelles :
    Âge : 47
    Localisation : France

    Informations forums :
    Inscription : Septembre 2010
    Messages : 1 769
    Par défaut
    Si je ne dis pas trop de bêtises, tu peux définir toi même ta fonction décoratrice (tu peux même lui définir des paramètres pour la rendre plus générique comme la classe à hériter, la fonction à "décorer/wrapper" et la fonction à utiliser pour cela), donc dans l'absolue c'est faisable.
    Si tu veux que toutes les sous-classes de A qui redéfinissent draw aient le décorateur du wrapper, il existe la "méthode" __init_subclass__ qui pourrait permettre d'appliquer le wrapper (de ton choix) directement aux sous-classes de A (fonction à définir dans la classe A donc) et ainsi, il n'y a même plus de décorateur à ajouter, c'est la classe A qui s'en charge.

    exemple (issu de l'IA)
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class A:
        @staticmethod
        def drawWrapper(func):
            def wrapper(self, *args, **kwargs):
                print(">> Début automatique")
                res = func(self, *args, **kwargs)
                print(">> Fin automatique")
                return res
            return wrapper
     
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            # Si la sous-classe redéfinit 'draw', on la décore automatiquement !
            if "draw" in cls.__dict__:
                cls.draw = A.drawWrapper(cls.draw)
     
     
    class B(A):  # Héritage explicite, mais le décorateur s'applique tout seul
        def draw(self):
            print("Dessin de B")
     
     
    b = B()
    b.draw()

  4. #4
    Membre Expert
    Homme Profil pro
    Inscrit en
    Avril 2004
    Messages
    1 107
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Avril 2004
    Messages : 1 107
    Par défaut
    umfred,
    j'étais en train de faire des tests et je suis tombé plus ou moins sur un code similaire avant même de lire ta réponse. Ca me laisse penser que j'ai encore de bons restes

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    class A:
     
        def draw(self):
            print('draw A')
     
     
        def drawWrapper(func):
            def draw(self):
                print('drawWrapper A')
                func(self)
            return draw
     
        @classmethod
        def classWrapper(cls,scls):
            bases = (cls,) + scls.__bases__
            scls.draw = A.drawWrapper(scls.draw)
            return type(scls.__name__,bases,scls.__dict__.copy())
     
     
    @A.classWrapper
    class B():
     
        def draw(self):
            print('draw B')
     
    foo = B()
    print(type(foo))
    foo.draw()
    Cependant ton exemple est beaucoup plus propre. Je ne connaissais pas __init_subclass__.

    Merci.

  5. #5
    Membre Expert
    Profil pro
    Inscrit en
    Septembre 2010
    Messages
    1 769
    Détails du profil
    Informations personnelles :
    Âge : 47
    Localisation : France

    Informations forums :
    Inscription : Septembre 2010
    Messages : 1 769
    Par défaut
    Je te mets la fonction décoratrice générique que l'IA m'a généré
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    def auto_inherit_and_wrap(parent_cls, method_name, wrapper_name):
        """
        Décorateur générique qui :
        - Fait hériter la classe décorée de `parent_cls`
        - Applique la méthode `wrapper_name` de `parent_cls` sur `method_name`
        """
        def decorator(cls):
            # 1. Ajout dynamique de la classe parente
            if parent_cls not in cls.__bases__:
                cls.__bases__ = (parent_cls,) + cls.__bases__
     
            # 2. Récupération du décorateur depuis la classe parente
            wrapper = getattr(parent_cls, wrapper_name, None)
     
            # 3. Application du décorateur si la méthode cible existe dans la classe
            if wrapper and method_name in cls.__dict__:
                original_method = getattr(cls, method_name)
                setattr(cls, method_name, wrapper(original_method))
     
            return cls
        return decorator 
     
    # --- EXEMPLE D'UTILISATION ---
     
    class A:
        def draw(self):
            print("Dessin de A")
     
        @staticmethod
        def drawWrapper(func):
            def wrapper(self, *args, **kwargs):
                print(">> Début du dessin")
                res = func(self, *args, **kwargs)
                print(">> Fin du dessin")
                return res
            return wrapper
     
     
    # On passe la classe parente, le nom de la méthode et le nom du wrapper
    @auto_inherit_and_wrap(parent_cls=A, method_name="draw", wrapper_name="drawWrapper")
    class B:
        def draw(self):
            print("Dessin de B")
     
     
    b = B()
    print(f"Hérite de A ? {isinstance(b, A)}")  # True
    b.draw()

  6. #6
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 962
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 962
    Par défaut
    @josmiley,

    Oui c'est possible d'utiliser @undecorateurpourA sans problème, pour fonctionner comme demandé, cette solution doit recréer la classe à la volée via la fonction type() pour lui injecter A dans ses classes de bases (__bases__), puis remplacer les méthodes, ça donnerait ceci,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def undecorateurpourA(cls):
        # 1. On force l'héritage de A (en recréant la classe dynamiquement avec type)
        cls_bases = (A,) + cls.__bases__
        nouveau_cls = type(cls.__name__, cls_bases, dict(cls.__dict__))
     
        # 2. On décore la méthode 'draw' si elle existe dans la classe fille
        if 'draw' in nouveau_cls.__dict__:
            method_originale = nouveau_cls.__dict__['draw']
     
            # Note: selon que drawWrapper prend self ou non, l'adaptation peut varier
            def draw_wrap(self, *args, **kwargs):
                print("Avant draw (wrapper)")
                result = method_originale(self, *args, **kwargs)
                print("Après draw (wrapper)")
                return result
     
            setattr(nouveau_cls, 'draw', draw_wrap)
     
        return nouveau_cls
    Une seconde solution est d'utiliser depuis la version 3.6 __init_subclass__.

    Je pense que c'est le plus pythonique car on défini la logique dans A pour qu'elle s'applique automatiquement à tous ses enfants (attention que depuis 3.6 comme tu dois avoir des legacy comme moi je suppose...).

    Ça donnerait ce type de code,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    class A:
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            # Dès que quelqu'un hérite de A, on vérifie s'il a défini "draw"
            if 'draw' in cls.__dict__:
                original_draw = cls.__dict__['draw']
     
                # On le décore dynamiquement
                def drawWrapper(self, *args, **kwargs):
                    print("-- Début wrapper --")
                    res = original_draw(self, *args, **kwargs)
                    print("-- Fin wrapper --")
                    return res
     
                setattr(cls, 'draw', drawWrapper)
     
        def draw(self):
            pass
     
    class B(A):
        def draw(self):
            print("Draw de B")
     
    obj = B()
    obj.draw()
    Tu peux te renseigner du côté de la PEP 487 pour plus d'infos sur cette méthode __init_subclass__.

    Avec ta solution, chaque fois qu'un développeur crée une classe C, D ou E, il doit absolument se souvenir d'ajouter le décorateur @undecorateurpourA. S'il l'oublie, le code plantera. Avec la solution __init_subclass__, il suffit d'hériter de A (ce qui est naturel en POO), et la décoration se fait de manière transparente.

    Et puis ta solution est en python, alors que __init_subclass__ est implémenté en CPython...

    Aussi ta solution est en réalité un "hack". Forcer un héritage à la volée en injectant A dans le tuple cls.__bases__ = (A,) + cls.__bases__ modifie l'ordre de résolution des méthodes (MRO) de manière inattendue. Si la classe B a déjà d'autres parents, modifier l'arbre d'héritage dynamiquement dans un décorateur est extrêmement périlleux et risque de faire planter les appels à super() ou de générer l'erreur : TypeError: Cannot create a consistent method resolution order (MRO). Alors que l'autre solution en Cpython utilise l'héritage standard de Python, le compilateur garantit donc que le MRO sera toujours valide et prévisible.

    Peu important si tu n'utilises pas les analyses statiques style mypy, mais les IDE ne gèrent pas très bien les mutations de classes générées par des appels dynamiques à type() ou des décorateurs qui modifient la structure de la classe. Avec ta solution, ton IDE risque de ne pas comprendre que B hérite de A. Avec la solution standard, l'autocomplétion, le linting et l'analyse statique de type fonctionneront parfaitement dès le départ.

    EDIT: J'avais pas regardé les réponses, mais ce que dit @umfred est exactement ce que je dis avec quelques petites précisions supplémentaires...
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

Discussions similaires

  1. Réponses: 0
    Dernier message: 05/04/2018, 23h48
  2. Liste des classes les plus utilisées
    Par mayer_450 dans le forum Android
    Réponses: 2
    Dernier message: 20/06/2014, 11h46
  3. Réponses: 2
    Dernier message: 27/04/2011, 01h19
  4. Bug Visual studio, plus accès à des classes de App_code
    Par tortuegenie dans le forum ASP.NET
    Réponses: 1
    Dernier message: 20/05/2008, 09h26
  5. [Classloader]mon progr ne marche plus après modif des *.class
    Par Melchisedec dans le forum API standards et tierces
    Réponses: 16
    Dernier message: 30/04/2006, 20h48

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo