web-dev-qa-db-fra.com

Comment changer dynamiquement la classe de base des instances lors de l'exécution?

Cet article a un extrait montrant l'utilisation de __bases__ pour modifier dynamiquement la hiérarchie d'héritage de certains codes Python, en ajoutant une classe à une collection de classes existante de classes dont il hérite. Ok, c'est difficile à lire, le code est probablement plus clair:

class Friendly:
    def hello(self):
        print 'Hello'

class Person: pass

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

Autrement dit, Person n'hérite pas de Friendly au niveau de la source, mais plutôt cette relation d'héritage est ajoutée dynamiquement au moment de l'exécution par la modification de __bases__attribut de la classe Person. Cependant, si vous changez Friendly et Person pour être de nouvelles classes de style (en héritant de l'objet), vous obtenez l'erreur suivante:

TypeError: __bases__ assignment: 'Friendly' deallocator differs from 'object'

Un peu de recherche sur Google semble indiquer certaines incompatibilités entre les classes de style nouveau et ancien en ce qui concerne la modification de la hiérarchie d'héritage à l'exécution. Plus précisément: "Les objets de classe de nouveau style ne prennent pas en charge l'affectation à leur base attribut" .

Ma question, est-il possible de faire fonctionner l'exemple Friendly/Person ci-dessus en utilisant des classes de nouveau style dans Python 2.7+, éventuellement en utilisant le __mro__ attribut?

Clause de non-responsabilité: je me rends pleinement compte qu'il s'agit d'un code obscur. Je me rends pleinement compte que dans de vrais trucs de code de production comme celui-ci, ils ont tendance à être illisibles, il s'agit purement d'une expérience de réflexion, et pour les drôles d'apprendre quelque chose sur la façon dont Python traite les problèmes liés à l'héritage multiple.

67
Adam Parkin

Ok, encore une fois, ce n'est pas quelque chose que vous devriez normalement faire, c'est à titre informatif uniquement.

Où Python recherche une méthode sur un objet instance est déterminé par l'attribut __mro__ De la classe qui définit cet objet (le [~ # ~] m [~ # ~] ethod [~ # ~] r [~ # ~] esolution [~ # ~] o [~ # ~] attribut rder). Ainsi, si nous pouvions modifier le __mro__ de Person, nous obtiendrions le comportement souhaité. Quelque chose comme:

setattr(Person, '__mro__', (Person, Friendly, object))

Le problème est que __mro__ Est un attribut en lecture seule, et donc setattr ne fonctionnera pas. Peut-être que si vous êtes un Python il y a un moyen de contourner cela, mais je suis clairement en deçà du statut de gourou car je ne peux pas y penser.

Une solution de contournement possible consiste à simplement redéfinir la classe:

def modify_Person_to_be_friendly():
    # so that we're modifying the global identifier 'Person'
    global Person

    # now just redefine the class using type(), specifying that the new
    # class should inherit from Friendly and have all attributes from
    # our old Person class
    Person = type('Person', (Friendly,), dict(Person.__dict__)) 

def main():
    modify_Person_to_be_friendly()
    p = Person()
    p.hello()  # works!

Ce que cela ne fait pas, c'est de modifier toutes les instances Person précédemment créées pour avoir la méthode hello(). Par exemple (il suffit de modifier main()):

def main():
    oldperson = Person()
    ModifyPersonToBeFriendly()
    p = Person()
    p.hello()  
    # works!  But:
    oldperson.hello()
    # does not

Si les détails de l'appel type ne sont pas clairs, lisez excellente réponse d'e-satis sur 'Qu'est-ce qu'une métaclasse en Python?' .

33
Adam Parkin

J'ai également eu du mal avec cela et j'ai été intrigué par votre solution, mais Python 3 nous l'enlève:

AttributeError: attribute '__dict__' of 'type' objects is not writable

En fait, j'ai un besoin légitime d'un décorateur qui remplace la superclasse (unique) de la classe décorée. Cela nécessiterait une description trop longue à inclure ici (j'ai essayé, mais je n'ai pas pu l'obtenir à une longueur raisonnablement longue et à une complexité limitée - cela est venu dans le contexte de l'utilisation par beaucoup Python applications d'un serveur d'entreprise basé sur Python où différentes applications nécessitaient des variations légèrement différentes d'une partie du code.)

La discussion sur cette page et d'autres comme elle a fourni des indices que le problème de l'attribution à __bases__ ne se produit que pour les classes sans superclasse définie (c'est-à-dire dont la seule superclasse est objet). J'ai pu résoudre ce problème (pour les deux Python 2.7 et 3.2) en définissant les classes dont la superclasse que je devais remplacer comme étant des sous-classes d'une classe triviale:

## T is used so that the other classes are not direct subclasses of object,
## since classes whose base is object don't allow assignment to their __bases__ attribute.

class T: pass

class A(T):
    def __init__(self):
        print('Creating instance of {}'.format(self.__class__.__name__))

## ordinary inheritance
class B(A): pass

## dynamically specified inheritance
class C(T): pass

A()                 # -> Creating instance of A
B()                 # -> Creating instance of B
C.__bases__ = (A,)
C()                 # -> Creating instance of C

## attempt at dynamically specified inheritance starting with a direct subclass
## of object doesn't work
class D: pass

D.__bases__ = (A,)
D()

## Result is:
##     TypeError: __bases__ assignment: 'A' deallocator differs from 'object'
21
Mitchell Model

Je ne peux pas garantir les conséquences, mais que ce code fait ce que vous voulez sur py2.7.2.

class Friendly(object):
    def hello(self):
        print 'Hello'

class Person(object): pass

# we can't change the original classes, so we replace them
class newFriendly: pass
newFriendly.__dict__ = dict(Friendly.__dict__)
Friendly = newFriendly
class newPerson: pass
newPerson.__dict__ = dict(Person.__dict__)
Person = newPerson

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

Nous savons que cela est possible. Cool. Mais nous ne l'utiliserons jamais!

6
akaRem

À droite de la batte, toutes les mises en garde de jouer avec la hiérarchie des classes dynamiquement sont en vigueur.

Mais si cela doit être fait alors, apparemment, il y a un hack qui contourne le "deallocator differs from 'object" issue when modifying the __bases__ attribute pour les nouvelles classes de style.

Vous pouvez définir un objet de classe

class Object(object): pass

Qui dérive une classe de la métaclasse intégrée type. Voilà, maintenant vos nouvelles classes de style peuvent modifier le __bases__ sans aucun probléme.

Dans mes tests, cela a très bien fonctionné car toutes les instances existantes (avant de modifier l'héritage) et ses classes dérivées ont ressenti l'effet du changement, y compris leur mro mis à jour.

2
Sam Gulve

J'avais besoin d'une solution pour cela qui:

  • Fonctionne avec Python 2 (> = 2.7) et Python 3 (> = 3.2)).
  • Permet de modifier les bases de classe après l'importation dynamique d'une dépendance.
  • Permet de changer les bases de classe du code de test unitaire.
  • Fonctionne avec les types qui ont une métaclasse personnalisée.
  • Autorise toujours unittest.mock.patch pour fonctionner comme prévu.

Voici ce que j'ai trouvé:

def ensure_class_bases_begin_with(namespace, class_name, base_class):
    """ Ensure the named class's bases start with the base class.

        :param namespace: The namespace containing the class name.
        :param class_name: The name of the class to alter.
        :param base_class: The type to be the first base class for the
            newly created type.
        :return: ``None``.

        Call this function after ensuring `base_class` is
        available, before using the class named by `class_name`.

        """
    existing_class = namespace[class_name]
    assert isinstance(existing_class, type)

    bases = list(existing_class.__bases__)
    if base_class is bases[0]:
        # Already bound to a type with the right bases.
        return
    bases.insert(0, base_class)

    new_class_namespace = existing_class.__dict__.copy()
    # Type creation will assign the correct ‘__dict__’ attribute.
    del new_class_namespace['__dict__']

    metaclass = existing_class.__metaclass__
    new_class = metaclass(class_name, Tuple(bases), new_class_namespace)

    namespace[class_name] = new_class

Utilisé comme ceci dans l'application:

# foo.py

# Type `Bar` is not available at first, so can't inherit from it yet.
class Foo(object):
    __metaclass__ = type

    def __init__(self):
        self.frob = "spam"

    def __unicode__(self): return "Foo"

# … later …
import bar
ensure_class_bases_begin_with(
        namespace=globals(),
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)

Utilisez comme ceci à partir du code de test unitaire:

# test_foo.py

""" Unit test for `foo` module. """

import unittest
import mock

import foo
import bar

ensure_class_bases_begin_with(
        namespace=foo.__dict__,
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)


class Foo_TestCase(unittest.TestCase):
    """ Test cases for `Foo` class. """

    def setUp(self):
        patcher_unicode = mock.patch.object(
                foo.Foo, '__unicode__')
        patcher_unicode.start()
        self.addCleanup(patcher_unicode.stop)

        self.test_instance = foo.Foo()

        patcher_frob = mock.patch.object(
                self.test_instance, 'frob')
        patcher_frob.start()
        self.addCleanup(patcher_frob.stop)

    def test_instantiate(self):
        """ Should create an instance of `Foo`. """
        instance = foo.Foo()
1
bignose

Les réponses ci-dessus sont bonnes si vous devez modifier une classe existante au moment de l'exécution. Cependant, si vous cherchez simplement à créer une nouvelle classe qui hérite d'une autre classe, il existe une solution beaucoup plus propre. J'ai eu cette idée de https://stackoverflow.com/a/21060094/353344 , mais je pense que l'exemple ci-dessous illustre mieux un cas d'utilisation légitime.

def make_default(Map, default_default=None):
    """Returns a class which behaves identically to the given
    Map class, except it gives a default value for unknown keys."""
    class DefaultMap(Map):
        def __init__(self, default=default_default, **kwargs):
            self._default = default
            super().__init__(**kwargs)

        def __missing__(self, key):
            return self._default

    return DefaultMap

DefaultDict = make_default(dict, default_default='wug')

d = DefaultDict(a=1, b=2)
assert d['a'] is 1
assert d['b'] is 2
assert d['c'] is 'wug'

Corrigez-moi si je me trompe, mais cette stratégie me semble très lisible et je l'utiliserais dans le code de production. C'est très similaire aux foncteurs d'OCaml.

0
fredcallaway