Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Spécialiser une classe lors de sa création

+6 votes

J'ai deux classes A et B. A hérite de B. Un objet de classe A a des caractéristiques que tous les objets de classe B n'ont pas forcement car il est soumis à certaines contraintes supplémentaires.
Est-il possible de faire en sorte que quand on crée un objet de classe B, si cet objet vérifie les contraintes pour être un objet de classe A, alors il devienne un objet de classe A? Si oui quelle est la "bonne" façon de faire cela?

demandé 7-Mar-2015 par sPaz (306 points)
reclassée 9-Mar-2015 par sPaz

Pourquoi ferait-on un obj = B(args) pour obtenir un objet de classe A ? Je ne comprends pas très bien l'intérêt.

Je vais détailler un peu plus ma question.
J'ai créé deux classes. Une classe Polynom qui en particulier gère les opérations entre les polynomes et renvoie alors un objet de classe Polynom. Et une classe Polynom2ndDegre qui hérite les opérations de la classe Polynom mais qui en plus est capable de calculer Delta, son tableau de signe...
Le soucis c'est que j'aimerai pouvoir faire des opérations sur les polynômes et, sans avoir à modifier la classe, calculer le Delta du résultat.

Si possible, peut on voir ce que tu as deja comme classes?

Oui bien sûr!
Alors on peut retrouver
La classe Polynom ici: http://git.poneyworld.net/pyMath/plain/pymath/polynom.py?h=dev
La classe Polynom_deg2 ici: http://git.poneyworld.net/pyMath/plain/pymath/polynomDeg2.py?h=dev

En fait j'ai déjà fait quelque chose de ce genre avec une autre classe (Expression que l'on peut trouver ici: http://git.poneyworld.net/pyMath/plain/pymath/expression.py?h=dev) mais je ne trouve pas a très "beau". Et surtout si je voulais reproduire cette astuce, il faudrait que j'importe Polynom_deg2 dans le fichier polynom.py alors que Polynom est déjà importé dans polynomDeg2...

EDIT: Changement du lien pour expression.py

Le troisième lien (celui sur Expression) ne pointe sur rien d'intéressant.
"Invalid branch: dev)"

Je vais essayer de résumer mon code pour vous éviter d'avoir à tout lire...

polynom.py

class Polynom():
    def __init__(self, coefs):
        self._coefs = coefs
    def __add__(self, autre_polynom):
        somme_des_coefs = ...
        return Polynom(somme_des_coefs)

polynomDeg2.py

from .polynom import Polynom
class PolynomDeg2(Polynom):
    def __init__(self, coefs):
        if len(coefs) != 3:
            raise ErrorValue("Un polynome du 2nd degré doit avoir exactement 3 coefficients")
        Polynom.__init__(self, coefs)
    def delta(self):
        return self._coefs[1]**2 - 4 * self._coefs[2] * self._coefs[0] 

Et ce que j'aimerai pouvoir faire c'est

>>> P = Polynom([1,2,3,4])
>>> Q = Polynom([5,6,7,4])
>>> R = P - Q
>>> R._coefs # R pourrait donc être un polynom du deuxième degré
[-4, -4, -4]
>>> R.delta() # Et donc j'aimerai pouvoir appeler cette methode qui n'existe que pour les polynomes du 2nd degré
-48

J'espère que c'est plus clair.

Le seul truc que je vois, c'est prendre la classe Polynom et faire en sorte que la methode __add__ retourne un objet de type PolynomDeg2 si la liste des coef est egale a 3.

dans la mesure où les méthodes spécifiques aux polynômes du second degrés sont peu nombreuses (de même que pour les degrés ultérieur si besoin), je ne suis pas sûr que je m'embêterai à faire plusieurs classes, juste un test du degré pour l'exécution de certaines méthodes...

Le soucis c'est que cette classe PolynomDeg2 n'a bien entendu pas que la méthode Delta qui lui est propre mais pleins d'autres. De plus, j'envisage de créer une classe PolynomDeg3 avec certaines méthodes qui lui seront propres. Je prefèrerai séparer tout ça.
Plutôt que de faire retourner quand c'est possible un objet de type PolynomDeg2 à la méthode add (ce qu'il faudrait répéter pour les méthodes mul, sub...), je préfère encore faire ce qu j'ai fait dans la classe Expression. Ce qui ressemblerai à ça:

class Polynom():
    def __new__(cls, coefs):
        if len(coefs)==3:
            from .polynomDeg2 import PolynomDeg2
            return PolynomDeg2(coefs)
        else:
            return expression = object.__new__(cls)

    ... # le reste du code

@sPaz Oui, si tu peux eviter de dupliquer du code, c'est top. Je repondais uniquement a la problematique:

>>> R = P - Q
>>> R._coefs # R pourrait donc être un polynom du deuxième degré
[-4, -4, -4]
>>> R.delta() # appeler delta qui n'existe que pour les polynomes du 2nd degré
-48

Sur le fond, on est d'accord, retourner un objet different en fonction du nombre de coef. Pour la forme, je te laisse gerer.

3 Réponses

+4 votes
 
Meilleure réponse

Créer un décorateur "polynom_specializer", qui récupère le résultat de la fonction décorée et effectue les actions suivantes :
- Si c'est autre chose qu'un polynome, ne rien faire et renvoyer ce résultat tel quel.
- Sinon, inspecter le polynome et renvoyer un polynome de degré 2, 3 ou autre le cas échéant.

Appliquer ce décorateur à toutes les méthodes de la classe Polynom. Avec un bout de code dans ce genre :

import inspect
import types

class Polynom:
    def foo(self): return Polynom(1,2)
    # etc...

for name, fn in inspect.getmembers(Polynom):
    if isinstance(fn, types.UnboundMethodType):
        setattr(Polynom, name, polynom_specializer(fn))

Inspiré de cette question : http://stackoverflow.com/questions/3467526/attaching-a-decorator-to-all-functions-within-a-class

Ne pas oublier d'appliquer ce décorateur sur les méthodes des classes héritées, ainsi que sur tout autre fonction qui renverrait un polynome.

répondu 10-Mar-2015 par recher (380 points)
edité 14-Mar-2015 par recher

J'ai oublié les "import inspect" et "import types" au début. C'est des libs battery-included, à priori.

tu peux éditer ta réponse pour les ajouter :)

cela veut alors dire qu'il faut bien définir les classes Polynomes, et en particulier la possibilité de passer de l'une à l'autre. Par exemple de pouvoir faire

>>> P = Polynom([1, 2, 3, 4])
>>> Q = PolynomDeg2([1, 2, 3])
>>> R = Polynom(Q)
>>> S = PolynomDeg2([1, 2, 3, 0 ,0 ,0 ,0])
>>> T = Q + R

Oui effectivement, je n'avais pas penser à faire un décorateur pour renvoyer le bon type de polyôme! Après je prefère appliquer à chacune des fonctions qui renvoie un polynôme ce décorateur. Ce sera plus explicite.
Merci beaucoup!

+4 votes

Une solution valide et propre serait une troisième classe C qui encapsule ce traitement n'ayant rien à faire ni dans A, ni dans B.
Pour causer patterns, c'est une factory.
Pour causer fonctionnel, c'est une fonction qui retourne un nouvel objet selon des conditions d'entrées. Ça porte sûrement un nom.

En clair :

class B() :
def __init__(self, a, b, c):
    pass

class A(B) : 
def __init__(self, a, b):
    pass

def factory(context, a, b, c):
    if context.validate(a, b, c):
        return A(a, b, c)
    else:
        return B(a, b)

C'est tout à fait plaçable comme méthode de classe de A, avec le décorateur @staticmethod et, quoiqu'il arrive, une bonne docstring.

EDIT: La réponse de recher est vachement plus classe et pythonique.

répondu 10-Mar-2015 par lucas (2,340 points)
edité 11-Mar-2015 par lucas
+1 vote

Pour répondre à la question, c'est possible techniquement en jouant avec __new__, mais je ne dirais pas que c'est une bonne pratique.

Exemple en python 3 :

class Polynom:
    def __new__(cls, coeff):
        if len(coeff) <= 2:
            ret = super().__new__(Polynom2d)
            ret.__init__(coeff)
            return ret
        return super().__new__(cls)
    def __init__(self, coeff):
        self.coeff = coeff

class Polynom2d(Polynom):
    pass

x = Polynom([5.,0.])      # instance de Polynom2d
y = Polynom([2.,1.0,3.])  # instance de Polynom
répondu 9-Aou-2015 par yoch (2,514 points)

Ne faudrait il pas rajouter un @classmethod ici ? Ou un truc du genre ?

Je profite pour mettre un lien sur un question que j'avais posé:
http://indexerror.net/1855/recherche-cas-dusage-pertinent-de-%40staticmethod?show=1859#a1859

Non, __new__ est en quelque sorte une méthode de classe par définition, puisqu'elle ne prend pas une instance mais la classe elle même. la raison est que c'est une méthode qui est appelée avant la création de l'instance.

Ici, je me sers de __new__ pour faire une sorte de factory dans la classe parent.

Voir par exemple ici pour d'autres usages de __new__.

...