Étendre les capacités d'une classe via un décorateur en python

Le problème exposé dans ce sujet a été résolu.

Bonjour !

Voilà, je tente quelque chose, mais je suis pas certain que je m’y prend de la bonne manière, donc je viens demander des avis. Pour la faire courte, je travaille sur une petite librairie perso permettant d’extraire des résultats de fichier log de différents programmes (de chimie quantique, mais osef). Évidement, chaque programme crache ses résultats sous une forme différente, même si il y a certaines résultats communs, et d’autres qui sont spécifiques à un programme donné.

Plutôt que de définir, pour chaque type de fichier, des fonctions get_machin(), je me suis dit que j’allais tenter une approche un peu différente (et qui a le chic de permettre de définir des choses à postériori), c’est de pouvoir, à n’importe quel moment de l’exécution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@define_property(FichierLogDuProgrammeTruc, 'propriétéBidule'):
def get_propriete_bidule(obj):
    truc = obj.faire_machin()  # "obj" est une instance de FichierLogDuProgrammeTruc
    # ... aller chercher l'info, 
    # la transformer dans le bon format, 
    # éventuellement faire un calcul ou deux ...
    return bidule

f = FichierLogDuProgrammeTruc()
f.get_property('propriétéBidule')

Évidement, propriétéBidule peut très bien se retrouver dans différents types de fichier (ou pas), donc il faut que je puisse définir une fonction par type de fichier. J’ai donc testé de mon coté, et je me retrouve avec le POC suivant:

 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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class File:

    properties = {}

    def __init__(self):
        pass

    @classmethod
    def define_property(cls, property, f):
        if cls not in cls.properties:
            cls.properties[cls] = {}
        cls.properties[cls][property] = f

    def get_property(self, property):
        if type(self) in self.properties:
            if property in self.properties[type(self)]:
                return self.properties[type(self)][property](self)
            else:
                raise Exception('no {}'.format(property))
        else:
            raise Exception('not class')


class FileA(File):

    def __init__(self):
        super().__init__()
        self.a = 'a'
        self.c = 'c'


class FileB(File):

    def __init__(self):
        super().__init__()
        self.b = 'b'
        self.c = 'c'


def define_propery(cls, property):
    def wrapper(f):
        cls.define_property(property, f)
    return wrapper


@define_propery(FileA, 'a')
def get_a(obj):
    return obj.a + ' in A'


@define_propery(FileA, 'c')
def get_c(obj):
    return obj.c + ' in A'


@define_propery(FileB, 'b')
def get_b(obj):
    return obj.b + ' in B'


@define_propery(FileB, 'c')
def get_c(obj):
    return obj.c + ' in B'

obj_a = FileA()
print(obj_a.get_property('a'))
print(obj_a.get_property('c'))

obj_b = FileB()
print(obj_b.get_property('b'))
print(obj_b.get_property('c'))
print(obj_b.get_property('a'))   # ça crache!

Ça fait le café, puisqu’on a bien c qui est défini deux fois sans problème et le programme qui se casse la gueule quand on demande a pour FileB (dernière ligne):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
a in A
c in A
b in B
c in B
Traceback (most recent call last):
  File "/home/pbeaujea/Devels/scripts/test.py", line 72, in <module>
    print(obj_b.get_property('a'))
  File "/home/pbeaujea/Devels/scripts/test.py", line 19, in get_property
    raise Exception('no {}'.format(property))
Exception: no a

… mais je suis pas spécialement heureux: déjà, le dictionnaire est copié à la création de tout objet, ce qui est un peu idiot. Ensuite, et c’est lié, il y a un certain bordel qui provient du fait qu’un dictionnaire est immutable (immuable?), ce qui fait qu’on est obligé de définir self.properties[cls], sans quoi on ne peut pas (re)définir une propriété unique à chaque classe.

Une solution serait par exemple de passer par une variable globale pour properties, mais je suis moyennement chaud aussi, parce que c’est toujours un peu pénible avec les import après.

Donc, si vous avez une idée plus intelligente, j’en serais fort heureux !

D’avance merci :)

+0 -0

Salut,

Plutôt que de définir, pour chaque type de fichier, des fonctions get_machin(), je me suis dit que j’allais tenter une approche un peu différente (et qui a le chic de permettre de définir des choses à postériori), c’est de pouvoir, à n’importe quel moment de l’exécution:

pierre_24

Ça a une réel utilité dans ton programme ? Si oui, tu pourrais faire plus simple avec :

1
2
3
4
5
6
class Foo: pass

def get_bar(self): return self
Foo.get_bar = get_bar

Foo().get_bar() # => l'objet __main__.Foo

Je viens de tester, ça modifie aussi les instances précédemment créées :

1
2
3
4
5
6
7
class Foo: pass
foo = Foo()

def get_bar(self): return self
Foo.get_bar = get_bar

foo.get_bar() # => l'objet __main__.Foo

Modifier une classe dynamiquement me fait quand même un peu mal au cœur, c’est vraiment nécessaire ?

Modifier une classe dynamiquement me fait quand même un peu mal au cœur, c’est vraiment nécessaire ?

tleb

Disons que je préfère: ces log contiennent énormément d’informations (on parle de 1 Mio de texte pièce, typiquement), et je voudrait laisser la possibilité de définir des nouveaux attributs à postériori pour les choses que je n’imaginerais même pas récupérer aujourd’hui :)

Je pense que ce tuto pourrait t’intéresser : https://zestedesavoir.com/tutoriels/1226/le-pattern-dispatcher-en-python/

yoch

C’est même clairement ça, idiot que je suis. Ça, c’est parce que j’arrivais pas à formuler exactement ce que je voulais pour l’expliquer à Google, tient, donc un très grand merci :)

+1 -0
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