56 votes

Reclassement d'une instance en Python

J'ai une classe qui m'est fournie par une bibliothèque externe. J'ai créé une sous-classe de cette classe. J'ai aussi une instance de la classe d'origine.

Je souhaite maintenant tourner cette instance dans une instance de mon sous-classe sans modifier les propriétés que l'instance a déjà (à l'exception de ceux que mon sous-classe remplace de toute façon).

La solution suivante qui semble fonctionner.

# This class comes from an external library. I don't (want) to control
# it, and I want to be open to changes that get made to the class
# by the library provider.
class Programmer(object):
    def __init__(self,name):
        self._name = name

    def greet(self):
        print "Hi, my name is %s." % self._name

    def hard_work(self):
        print "The garbage collector will take care of everything."

# This is my subclass.
class C_Programmer(Programmer):
    def __init__(self, *args, **kwargs):
        super(C_Programmer,self).__init__(*args, **kwargs)
        self.learn_C()

    def learn_C(self):
        self._knowledge = ["malloc","free","pointer arithmetic","curly braces"]

    def hard_work(self):
        print "I'll have to remember " + " and ".join(self._knowledge) + "."

    # The questionable thing: Reclassing a programmer.
    @classmethod
    def teach_C(cls, programmer):
        programmer.__class__ = cls # <-- do I really want to do this?
        programmer.learn_C()


joel = C_Programmer("Joel")
joel.greet()
joel.hard_work()
#>Hi, my name is Joel.
#>I'll have to remember malloc and free and pointer arithmetic and curly braces.

jeff = Programmer("Jeff")

# We (or someone else) makes changes to the instance. The reclassing shouldn't
# overwrite these.
jeff._name = "Jeff A" 

jeff.greet()
jeff.hard_work()
#>Hi, my name is Jeff A.
#>The garbage collector will take care of everything.

# Let magic happen.
C_Programmer.teach_C(jeff)

jeff.greet()
jeff.hard_work()
#>Hi, my name is Jeff A.
#>I'll have to remember malloc and free and pointer arithmetic and curly braces.

Cependant, je ne suis pas convaincu que cette solution ne contient pas toutes les mises en garde, je n'ai pas pensé (désolé pour le triple négation, en particulier parce que réaffectation des la magique __class__ ne fonctionne tout simplement pas. Même si cela fonctionne, je ne peux pas aider le sentiment qu'il y devrait être plus pythonic moyen de le faire.

Est-il?


Edit: Merci à tous pour vos réponses. Voici ce que je reçois d'eux:

  • Bien que l'idée de reclassing une instance par l'attribution d' __class__ n'est pas largement utilisé de l'idiome, la plupart des réponses (4 sur 6 au moment de la rédaction) considèrent que c'est une approche valable. Un anwswer (par ojrac) dit qu'il est "assez bizarre au premier coup d'œil," avec qui je suis d'accord (c'était la raison de poser la question). Une seule réponse (par Jason Baker; avec deux commentaires positifs & voix) activement m'a découragé de le faire, mais en se basant sur l'exemple de cas d'utilisation plutôt que sur la technique en général.

  • Aucune réponse, positive ou non, trouve un réel problème technique de cette méthode. Une petite exception est jls qui mentionne à l'attention de la vieille-classes de style, ce qui est probablement vrai, et C extensions. Je suppose que le nouveau-style-classe-aware C les extensions doivent être aussi très bien avec cette méthode comme Python lui-même (à condition que le dernier est vrai), même si vous êtes en désaccord, garder les réponses à venir.

Quant à la question de savoir comment pythonic c'est, il y a eu quelques réponses positives, mais pas de réelle motivation. En regardant le Zen (import this- )), je suppose que la règle la plus importante dans ce cas est "Explicite est mieux qu'implicites." Je ne suis pas sûr, cependant, si cette règle, qui parle pour ou contre reclassing de cette façon.

  • À l'aide de {has,get,set}attr semble plus explicite, comme nous sommes explicitement les modifications de l'objet au lieu d'utiliser la magie.

  • À l'aide de __class__ = newclass semble plus explicite parce que nous avons explicitement de dire "C'est maintenant un objet de la classe 'newclass' attendre à un comportement différent" en silence au lieu de changer les attributs, mais en laissant aux utilisateurs de l'objet à croire qu'ils font affaire avec un objet régulier de la vieille classe.

En résumé: à Partir d'un point de vue technique, la méthode semble ok; le pythonicity question reste sans réponse avec une préférence pour le "oui".

J'ai accepté Martin Geisler réponse, parce que l'Mercurial plugin est un exemple assez fort (et aussi parce qu'elle répondait à une question que je n'avais même pas demandé à moi-même encore). Cependant, s'il y a des arguments sur la pythonicity question, je voudrais encore entendre. Merci à tous jusqu'à présent.

P. S. Le réel de cas d'utilisation est une INTERFACE de contrôle des données de l'objet qui a besoin de croître fonctionnalité supplémentaire au moment de l'exécution. Cependant, la question est destinée à être très général.

19voto

Martin Geisler Points 44779

Reclassing des cas comme cela se fait dans Mercurial a distributed revision control system) lorsque des extensions (plugins) que vous voulez modifier l'objet qui représentent le référentiel local. L'objet est appelé repo et est, au départ, localrepo de l'instance. Il est transmis à chaque extension, à son tour, et, si nécessaire, des extensions de définir une nouvelle classe qui est une sous-classe de repo.__class__ et de changer la classe de l' repo de cette nouvelle sous-classe!

Il semble comme ceci dans le code:

def reposetup(ui, repo):
    # ...

    class bookmark_repo(repo.__class__): 
        def rollback(self):
            if os.path.exists(self.join('undo.bookmarks')):
                util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
            return super(bookmark_repo, self).rollback() 

        # ...

    repo.__class__ = bookmark_repo

L'extension (j'ai pris le code de l'signets extension) définit un module de niveau de la fonction appelée reposetup. Mercurial appellerons ce lors de l'initialisation de l'extension et de passer un ui (interface utilisateur) et repo (référentiel) de l'argument.

La fonction définit une sous-classe, quelle que soit la classe repo de passe. Il ne serait pas suffisant de simplement sous-classe localrepo depuis les extensions doivent être en mesure d'étendre les uns des autres. Ainsi, si la première extension de change repo.__class__ de foo_repo, la prochaine extension devrait changer en repo.__class__ d'une sous-classe de foo_repo , et pas seulement une sous-classe de localrepo. Enfin, les changements de la fonction de la instanceø de la classe, tout comme vous l'avez fait dans votre code.

J'espère que ce code peut afficher une légitime l'utilisation de cette fonctionnalité du langage. Je pense que c'est le seul endroit où je l'ai vu utilisé dans la nature.

11voto

Jason Baker Points 56682

Je ne suis pas sûr que l'utilisation de l'héritage qui est le mieux dans ce cas (au moins en ce qui concerne "reclassing"). Il semble que vous êtes sur la bonne voie, mais il semble que la composition ou d'agrégation qui serait le mieux pour cela. Voici un exemple de ce que je suis en train de penser (dans pas testé, pseudo-esque code):

from copy import copy

# As long as none of these attributes are defined in the base class,
# this should be safe
class SkilledProgrammer(Programmer):
    def __init__(self, *skillsets):
        super(SkilledProgrammer, self).__init__()
        self.skillsets = set(skillsets)

def teach(programmer, other_programmer):
    """If other_programmer has skillsets, append this programmer's
       skillsets.  Otherwise, create a new skillset that is a copy
       of this programmer's"""
    if hasattr(other_programmer, skillsets) and other_programmer.skillsets:
        other_programmer.skillsets.union(programmer.skillsets)
    else:
        other_programmer.skillsets = copy(programmer.skillsets)
def has_skill(programmer, skill):
    for skillset in programmer.skillsets:
        if skill in skillset.skills
            return True
    return False
def has_skillset(programmer, skillset):
    return skillset in programmer.skillsets


class SkillSet(object):
    def __init__(self, *skills):
        self.skills = set(skills)

C = SkillSet("malloc","free","pointer arithmetic","curly braces")
SQL = SkillSet("SELECT", "INSERT", "DELETE", "UPDATE")

Bob = SkilledProgrammer(C)
Jill = Programmer()

teach(Bob, Jill)          #teaches Jill C
has_skill(Jill, "malloc") #should return True
has_skillset(Jill, SQL)   #should return False

Vous pouvez lire plus au sujet de jeux et arbitraire des listes d'arguments si vous n'êtes pas familier avec eux pour obtenir cet exemple.

3voto

jamesls Points 902

C'est bon. J'ai utilisé cet idiome plusieurs fois. Une chose à garder à l’esprit cependant, c’est que cette idée ne fonctionne pas bien avec les classes de style ancien et les diverses extensions C. Normalement, cela ne devrait pas être un problème, mais puisque vous utilisez une bibliothèque externe, vous devez simplement vous assurer que vous n'avez pas affaire à des classes de style ancien ou à des extensions C.

3voto

pillmuncher Points 4726

"L'État Modèle permet à un objet de modifier son comportement quand son état interne change. L'objet apparaîtra à changer de classe." - La Tête La Première, Modèle De Conception. Quelque chose de très similaire à écrire Gamma et.al. dans leur livre "Design Patterns". (Je l'ai à mon autre endroit, donc pas de citation). Je pense que c'est le point entier de ce modèle de conception. Mais si je peux changer la classe d'un objet au moment de l'exécution, la plupart du temps je n'ai pas besoin de motif (il ya des cas où les etats ne sont plus que de simuler un changement de classe).

Aussi, changer de classe au moment de l'exécution ne fonctionne pas toujours:

class A(object):
    def __init__(self, val):
        self.val = val
    def get_val(self):
        return self.val

class B(A):
    def __init__(self, val1, val2):
        A.__init__(self, val1)
        self.val2 = val2
    def get_val(self):
        return self.val + self.val2


a = A(3)
b = B(4, 6)

print a.get_val()
print b.get_val()

a.__class__ = B

print a.get_val() # oops!

En dehors de cela, je envisager de changer de classe à l'exécution Pythonic et de l'utiliser de temps à autre.

1voto

ojrac Points 6897

Heheh, amusant exemple.

"Reclassing" est assez bizarre, au premier coup d'œil. Ce sujet de la " constructeur de copie approche? Vous pouvez faire cela avec la Réflexion-comme hasattr, getattr et setattr. Ce code permet de copier tout un objet à un autre, à moins qu'il existe déjà. Si vous ne voulez pas les méthodes de copie, vous pouvez les exclure; voir le commentaire if.

class Foo(object):
    def __init__(self):
        self.cow = 2
        self.moose = 6

class Bar(object):
    def __init__(self):
        self.cat = 2
        self.cow = 11

    def from_foo(foo):
        bar = Bar()
        attributes = dir(foo)
        for attr in attributes:
            if (hasattr(bar, attr)):
                break
            value = getattr(foo, attr)
            # if hasattr(value, '__call__'):
            #     break # skip callables (i.e. functions)
            setattr(bar, attr, value)

        return bar

Toute cette réflexion n'est pas assez, mais parfois vous avez besoin d'un vilain reflet de la machine à faire des trucs cool se produire. ;)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X