Wie kann die Basisklasse von Instanzen zur Laufzeit dynamisch geändert werden?


76

Dieser Artikel enthält einen Ausschnitt, der die Verwendung von __bases__zum dynamischen Ändern der Vererbungshierarchie von Python-Code zeigt, indem eine Klasse zu einer vorhandenen Klassensammlung von Klassen hinzugefügt wird, von denen er erbt. Ok, das ist schwer zu lesen, Code ist wahrscheinlich klarer:

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

class Person: pass

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

Das heißt, Personerbt nicht Friendlyauf Quellenebene, sondern diese Vererbungsbeziehung wird zur Laufzeit dynamisch hinzugefügt, indem das __bases__Attribut der Person-Klasse geändert wird. Wenn Sie jedoch ändern Friendlyund Personneue Stilklassen werden (indem Sie vom Objekt erben), wird der folgende Fehler angezeigt:

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

Ein bisschen Googeln scheint auf einige Inkompatibilitäten zwischen Klassen neuen und alten Stils hinsichtlich der Änderung der Vererbungshierarchie zur Laufzeit hinzuweisen . Insbesondere: "Klassenobjekte neuen Stils unterstützen keine Zuordnung zu ihrem Basisattribut " .

Meine Frage, ist es möglich, das obige Friendly / Person-Beispiel mithilfe von Klassen neuen Stils in Python 2.7+ zum Laufen zu bringen, möglicherweise mithilfe des __mro__Attributs?

Haftungsausschluss: Mir ist völlig klar, dass dies obskurer Code ist. Mir ist völlig klar, dass in echten Produktionscodes solche Tricks eher unlesbar sind. Dies ist ein reines Gedankenexperiment, und für Funzies ist es wichtig, etwas darüber zu lernen, wie Python mit Problemen im Zusammenhang mit Mehrfachvererbung umgeht.


Es ist auch schön für Lernende, dies zu lesen, wenn sie nicht mit Metaklasse, Typ (), ... vertraut sind: slidehare.net/gwiener/metaclasses-in-python :)
hkoosha

Hier ist mein Anwendungsfall. Ich importiere eine Bibliothek, deren Klasse B von Klasse A erbt.
FutureNerd

2
Hier ist mein aktueller Anwendungsfall. Ich importiere eine Bibliothek, deren Klasse B von Klasse A erbt. Ich möchte mit new_A_method () New_A erstellen, das von A erbt. Jetzt möchte ich New_B erstellen, das von ... erbt, von B, als ob B von New_A geerbt hätte, damit die Methoden von B, die Methoden von A und new_A_method () für Instanzen von New_B verfügbar sind. Wie kann ich dies tun, ohne die vorhandene Klasse A mit Affen zu patchen?
FutureNerd

Könnten Sie nicht New_Bvon beiden Bund erben New_A? Denken Sie daran, Python unterstützt die Mehrfachvererbung.
Adam Parkin

2
Nach ein wenig googeln schien der folgende Python-Fehlerbericht relevant zu sein ... bugs.python.org/issue672115
mgilson

Antworten:


39

Ok, auch dies sollten Sie normalerweise nicht tun, dies dient nur zu Informationszwecken.

Wo Python sucht nach einer Methode eines Instanz - Objekt wird durch das bestimmt __mro__Attribut der Klasse , die das Objekt definiert (die M ethode R eSolution O rder Attributs). Wenn wir also das ändern könnten __mro__von Person, würden wir das gewünschte Verhalten bekommen. Etwas wie:

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

Das Problem ist, dass __mro__es sich um ein schreibgeschütztes Attribut handelt und setattr daher nicht funktioniert. Wenn Sie ein Python-Guru sind, gibt es vielleicht einen Ausweg, aber ich verfehle eindeutig den Guru-Status, da mir keiner einfällt.

Eine mögliche Problemumgehung besteht darin, die Klasse einfach neu zu definieren:

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!

Dies ändert keine zuvor erstellten PersonInstanzen, um die hello()Methode zu erhalten. Zum Beispiel (nur modifizieren main()):

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

Wenn die Details des typeAnrufs nicht klar sind, lesen Sie die ausgezeichnete Antwort von e-satis unter "Was ist eine Metaklasse in Python?". .


-1: Warum sollte die neue Klasse gezwungen werden, nur dann von Frielndly zu erben, wenn Sie das ursprüngliche `__mro__` genauso gut beibehalten können, indem Sie Folgendes aufrufen: type('Person', (Friendly) + Person.__mro__, dict(Person.__dict__)) (und noch besser, fügen Sie Schutzmaßnahmen hinzu, damit Friendly dort nicht zweimal endet.) Andere Probleme hier - wo die "Person" -Klasse tatsächlich definiert und verwendet wird: Ihre Funktion ändert sie nur für das aktuelle Modul - andere Module, in denen Person ausgeführt wird, sind nicht betroffen - Sie sollten besser einen Monkeypatch für das Modul durchführen, für das Person definiert ist. (und selbst dort gibt es Probleme)
jsbueno

10
-1Sie haben den Grund für die Ausnahme überhaupt nicht übersehen. Sie können Person.__class__.__bases__in Python 2 und 3 problemlos Änderungen vornehmen, sofern Persondiese nicht object direkt von ihnen erben . Siehe die Antworten von akaRem und Sam Gulve unten. Diese Problemumgehung umgeht nur Ihr eigenes Missverständnis des Problems.
Carl Smith

2
Dies ist eine sehr fehlerhafte "Lösung". Unter den anderen Problemen mit diesem Code wird das __dict__Attribut der resultierenden Objekte zerstört .
Benutzer2357112 unterstützt Monica

24

Ich habe auch damit zu kämpfen und war fasziniert von Ihrer Lösung, aber Python 3 nimmt es uns weg:

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

Ich habe tatsächlich ein berechtigtes Bedürfnis nach einem Dekorateur, der die (einzelne) Oberklasse der dekorierten Klasse ersetzt. Es würde eine zu lange Beschreibung erfordern, um sie hier aufzunehmen (ich habe es versucht, konnte sie aber nicht auf eine vernünftige Länge und begrenzte Komplexität bringen - sie entstand im Zusammenhang mit der Verwendung eines Python-basierten Unternehmensservers durch viele Python-Anwendungen, bei denen Unterschiedliche Anwendungen erforderten leicht unterschiedliche Variationen eines Teils des Codes.)

Die Diskussion auf dieser und ähnlichen Seiten lieferte Hinweise darauf, dass das Problem der Zuweisung __bases__nur für Klassen auftritt, für die keine Oberklasse definiert ist (dh deren einzige Oberklasse das Objekt ist). Ich konnte dieses Problem (sowohl für Python 2.7 als auch für Python 2.2) lösen, indem ich die Klassen, deren Oberklasse ich ersetzen musste, als Unterklassen einer trivialen Klasse definierte:

## 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'

6

Ich kann nicht für die Konsequenzen bürgen, aber dass dieser Code macht, was Sie auf py2.7.2 wollen.

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"

Wir wissen, dass dies möglich ist. Cool. Aber wir werden es niemals benutzen!


3

Rechts von der Fledermaus sind alle Vorbehalte des dynamischen Spielens mit der Klassenhierarchie wirksam.

Aber wenn es dann getan werden muss, gibt es anscheinend einen Hack, der sich um "deallocator differs from 'object" issue when modifying the __bases__ attributedie neuen Stilklassen dreht.

Sie können ein Klassenobjekt definieren

class Object(object): pass

Was eine Klasse von der eingebauten Metaklasse ableitet type. Das war's, jetzt können Ihre neuen Stilklassen das __bases__problemlos ändern .

In meinen Tests funktionierte dies tatsächlich sehr gut, da alle vorhandenen Instanzen (vor dem Ändern der Vererbung) und die daraus abgeleiteten Klassen den Effekt der Änderung einschließlich ihrer mroAktualisierung spürten .


2

Ich brauchte eine Lösung dafür, die:

  • Funktioniert sowohl mit Python 2 (> = 2.7) als auch mit Python 3 (> = 3.2).
  • Ermöglicht das Ändern der Klassenbasen nach dem dynamischen Importieren einer Abhängigkeit.
  • Ermöglicht das Ändern der Klassenbasen gegenüber dem Unit-Test-Code.
  • Funktioniert mit Typen mit einer benutzerdefinierten Metaklasse.
  • Ermöglicht weiterhin unittest.mock.patch, wie erwartet zu funktionieren.

Folgendes habe ich mir ausgedacht:

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

Wird in der Anwendung folgendermaßen verwendet:

# 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)

Verwenden Sie diese Option innerhalb des Unit-Test-Codes:

# 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()

0

Die obigen Antworten sind gut, wenn Sie eine vorhandene Klasse zur Laufzeit ändern müssen. Wenn Sie jedoch nur eine neue Klasse erstellen möchten, die von einer anderen Klasse geerbt wird, gibt es eine viel sauberere Lösung. Ich habe diese Idee von https://stackoverflow.com/a/21060094/3533440 erhalten , aber ich denke, das folgende Beispiel veranschaulicht einen legitimen Anwendungsfall besser.

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'

Korrigieren Sie mich, wenn ich falsch liege, aber diese Strategie scheint mir sehr lesbar zu sein, und ich würde sie im Produktionscode verwenden. Dies ist sehr ähnlich zu Funktoren in OCaml.


1
Ich bin mir nicht sicher, was dies mit der Frage zu tun hat, da es wirklich darum ging, Basisklassen zur Laufzeit dynamisch zu ändern. Was ist auf jeden Fall der Vorteil davon, nur Mapdirekt von direkten Methoden zu erben und diese nach Bedarf zu überschreiben (ähnlich wie es der Standard collections.defaultdicttut)? So wie es aussieht, make_defaultkann immer nur eine Art von Dingen zurückgegeben werden. Warum also nicht einfach DefaultMapdie Kennung der obersten Ebene erstellen, anstatt aufrufen make_defaultzu müssen, damit die Klasse instanziiert wird?
Adam Parkin

Danke für die Rückmeldung! (1) Ich bin hier gelandet, als ich versucht habe, eine dynamische Vererbung durchzuführen, also dachte ich, ich könnte jemandem helfen, der meinem gleichen Weg folgt. (2) Ich glaube, man könnte make_defaultdamit eine Standardversion einer anderen Art von wörterbuchähnlicher Klasse erstellen ( Map). Sie können nicht Mapdirekt von erben, da Mapdies erst zur Laufzeit definiert wird. In diesem Fall möchten Sie dictdirekt erben , aber wir stellen uns vor, dass es einen Fall geben könnte, in dem Sie bis zur Laufzeit nicht wissen, von welcher wörterbuchähnlichen Klasse Sie erben sollen.
Fredcallaway
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.