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, Person
erbt nicht Friendly
auf Quellenebene, sondern diese Vererbungsbeziehung wird zur Laufzeit dynamisch hinzugefügt, indem das __bases__
Attribut der Person-Klasse geändert wird. Wenn Sie jedoch ändern Friendly
und Person
neue 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.
quelle
New_B
von beidenB
und erbenNew_A
? Denken Sie daran, Python unterstützt die Mehrfachvererbung.Antworten:
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__
vonPerson
, 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
Person
Instanzen, um diehello()
Methode zu erhalten. Zum Beispiel (nur modifizierenmain()
):def main(): oldperson = Person() ModifyPersonToBeFriendly() p = Person() p.hello() # works! But: oldperson.hello() # does not
Wenn die Details des
type
Anrufs nicht klar sind, lesen Sie die ausgezeichnete Antwort von e-satis unter "Was ist eine Metaklasse in Python?". .quelle
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)-1
Sie haben den Grund für die Ausnahme überhaupt nicht übersehen. Sie könnenPerson.__class__.__bases__
in Python 2 und 3 problemlos Änderungen vornehmen, sofernPerson
diese nichtobject
direkt von ihnen erben . Siehe die Antworten von akaRem und Sam Gulve unten. Diese Problemumgehung umgeht nur Ihr eigenes Missverständnis des Problems.__dict__
Attribut der resultierenden Objekte zerstört .Ich habe auch damit zu kämpfen und war fasziniert von Ihrer Lösung, aber Python 3 nimmt es uns weg:
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'
quelle
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!
quelle
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__ attribute
die 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
mro
Aktualisierung spürten .quelle
Ich brauchte eine Lösung dafür, die:
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()
quelle
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.
quelle
Map
direkt von direkten Methoden zu erben und diese nach Bedarf zu überschreiben (ähnlich wie es der Standardcollections.defaultdict
tut)? So wie es aussieht,make_default
kann immer nur eine Art von Dingen zurückgegeben werden. Warum also nicht einfachDefaultMap
die Kennung der obersten Ebene erstellen, anstatt aufrufenmake_default
zu müssen, damit die Klasse instanziiert wird?make_default
damit eine Standardversion einer anderen Art von wörterbuchähnlicher Klasse erstellen (Map
). Sie können nichtMap
direkt von erben, daMap
dies erst zur Laufzeit definiert wird. In diesem Fall möchten Siedict
direkt 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.