2015-07-23 7 views
26

Ist es überhaupt möglich, den Wert einer @property einer Instanz einer Klasse, die ich nicht kontrolliere?Affe patchen @property

class Foo: 
    @property 
    def bar(self): 
     return here().be['dragons'] 

f = Foo() 
print(f.bar) # baz 
f.bar = 42 # MAGIC! 
print(f.bar) # 42 

Offensichtlich ist die oben würde einen Fehler erzeugen, wenn sie f.bar zuweisen wollen. Ist # MAGIC! in irgendeiner Weise möglich? Die Implementierungsdetails der @property sind eine Blackbox und nicht indirekt Affepatches. Der gesamte Methodenaufruf muss ersetzt werden. Es muss nur eine einzelne Instanz betreffen (das Patching auf Klassenebene ist in Ordnung, wenn es unvermeidlich ist, aber das geänderte Verhalten muss nur eine bestimmte Instanz betreffen, nicht alle Instanzen dieser Klasse).

+0

Haben Sie bedeuten, dass Sie 'f.bar' kein Eigentum mehr (und wenn ja, ohne Auswirkungen auf andere Instanzen?) Machen wollen, beachten, dass Eigenschaften, wie andere Deskriptoren gespeichert sind von der Klasse * * . – jonrsharpe

+0

Ja, ich möchte den Wert beeinflussen, der beim Zugriff auf 'f.bar' nur in dieser Instanz zurückgegeben wird. – deceze

+1

Ich denke, der einzige Weg, dies zu tun wäre, 'f .__ dict__' direkt zu ändern, indem man ein neues' bar'-Attribut bereitstellt, das vor der Eigenschaft nachgeschlagen wird, aber nur für diese einzelne Instanz. – jonrsharpe

Antwort

14

Subclass die Basisklasse (Foo) und einzelne Instanz der Klasse ändern, um die neue Unterklasse mit __class__ Attribut übereinstimmen:

>>> class Foo: 
...  @property 
...  def bar(self): 
...   return 'Foo.bar' 
... 
>>> f = Foo() 
>>> f.bar 
'Foo.bar' 
>>> class _SubFoo(Foo): 
...  bar = 0 
... 
>>> f.__class__ = _SubFoo 
>>> f.bar 
0 
>>> f.bar = 42 
>>> f.bar 
42 
+1

Klingt wie ein Gewinner! – deceze

+1

Wird es besser sein, wenn Sie 'bar = 0 'durch' bar = f.bar' in der '_SubFoo' Deklaration ersetzen, um den vorherigen Wert von' f.bar' zu behalten? – f43d65

+0

@ f43d65 Wenn die Unterklasse nur für eine Instanz verwendet wird, dann ist es sicher. Sie können den Balken sogar direkt auf '42' setzen, wenn das der Wert ist, den Sie sowieso wollen. Wenn mehrere Instanzen dieselbe Klasse verwenden müssen (um das gleiche Attribut zu überschreiben), ist es besser, einen einfachen Standardwert wie "0" anzugeben. –

1

Idee: Ersetze Eigenschaftendeskriptor, um die Einstellung bestimmter Objekte zu ermöglichen. Wenn ein Wert nicht explizit auf diese Weise festgelegt wird, wird der ursprüngliche Eigenschaftengetter aufgerufen.

Das Problem besteht darin, wie die explizit festgelegten Werte gespeichert werden. Wir können keinen dict Schlüssel verwenden, der durch gepatchte Objekte getastet wird, da 1) sie nicht notwendigerweise durch Identität vergleichbar sind; 2) Dies verhindert, dass gepatchte Objekte von Müll gesammelt werden. Für 1) könnten wir eine Handle schreiben, die Objekte umschließt und die Vergleichssemantik durch Identität überschreibt und für 2) könnten wir verwenden. Allerdings konnte ich diese beiden nicht zusammen arbeiten lassen.

Daher verwenden wir einen anderen Ansatz zum Speichern der explizit gesetzten Werte auf dem Objekt selbst, mit einem "sehr unwahrscheinlich Attributnamen". Es ist natürlich immer noch möglich, dass dieser Name mit etwas kollidiert, aber das ist in Sprachen wie Python ziemlich inhärent.

Dies funktioniert nicht bei Objekten, die keinen -Steckplatz haben. Ein ähnliches Problem würde sich jedoch für schwache Referenzen ergeben.

class Foo: 
    @property 
    def bar (self): 
     return 'original' 

class Handle: 
    def __init__(self, obj): 
     self._obj = obj 

    def __eq__(self, other): 
     return self._obj is other._obj 

    def __hash__(self): 
     return id (self._obj) 


_monkey_patch_index = 0 
_not_set   = object() 
def monkey_patch (prop): 
    global _monkey_patch_index, _not_set 
    special_attr = '$_prop_monkey_patch_{}'.format (_monkey_patch_index) 
    _monkey_patch_index += 1 

    def getter (self): 
     value = getattr (self, special_attr, _not_set) 
     return prop.fget (self) if value is _not_set else value 

    def setter (self, value): 
     setattr (self, special_attr, value) 

    return property (getter, setter) 

Foo.bar = monkey_patch (Foo.bar) 

f = Foo() 
print (Foo.bar.fset) 
print(f.bar) # baz 
f.bar = 42 # MAGIC! 
print(f.bar) # 42 
+0

Interessanter Ansatz. Im Wesentlichen muss die Klasse gepatcht werden, nur die Instanz kann nicht sein? – deceze

+1

@deceze dies funktioniert nur für die explizite Einstellung * einer * Instanz, nicht sicher, ob das wichtig ist – davidism

+0

@davidism: Sie haben Recht. Eine richtige Lösung würde ein "dict" von gepatchten Objekten zu neuen Werten haben, aber dies erzeugt einen Speicherverlust ... Erinnere dich nicht an den Status von schwachen Referenzen in Python. – doublep

1

Es sieht aus wie Sie von Eigenschaften zu den Bereichen Daten bewegen müssen Deskriptoren und Nicht-Datendeskriptoren. Eigenschaften sind nur eine spezielle Version von Datendeskriptoren. Funktionen sind ein Beispiel für Nicht-Datendeskriptoren - wenn Sie sie von einer Instanz abrufen, geben sie eine Methode und nicht die Funktion selbst zurück.

Ein Nicht-Datendeskriptor ist nur eine Instanz einer Klasse mit einer -Methode. Der einzige Unterschied zu einem Datendeskriptor besteht darin, dass auch eine __set__-Methode verwendet wird. Eigenschaften haben zunächst eine __set__-Methode, die einen Fehler auslöst, sofern Sie keine Setter-Funktion angeben.

Sie können wirklich erreichen, was Sie wollen, einfach indem Sie Ihren eigenen trivialen Nicht-Datendeskriptor schreiben.

class nondatadescriptor: 
    """generic nondata descriptor decorator to replace @property with""" 
    def __init__(self, func): 
     self.func = func 

    def __get__(self, obj, objclass): 
     if obj is not None: 
      # instance based access 
      return self.func(obj) 
     else: 
      # class based access 
      return self 

class Foo: 
    @nondatadescriptor 
    def bar(self): 
     return "baz" 

foo = Foo() 
another_foo = Foo() 

assert foo.bar == "baz" 
foo.bar = 42 
assert foo.bar == 42 
assert another_foo.bar == "baz" 
del foo.bar 
assert foo.bar == "baz" 
print(Foo.bar) 

Was diese ganze Arbeit macht, ist, dass die Logik unter der Haube __getattribute__. Ich kann nicht in der entsprechenden Dokumentation im Moment zu finden, aber um des Abrufs ist:

  1. Daten-Deskriptoren für die Klasse definiert gegeben werden, um die höchste Priorität (Objekte mit beiden __get__ und __set__) und deren __get__ Methode aufgerufen .
  2. Alle Attribute des Objekts selbst.
  3. Nicht-Datendekriptoren, die für die Klasse definiert sind (Objekte mit nur einer -Methode).
  4. Alle anderen für die Klasse definierten Attribute.
  5. Schließlich wird die __getattr__ Methode des Objekts als letzter Ausweg aufgerufen (falls definiert).
+0

Das ist nett und alles, aber ich versuche wirklich, eine 3rd-Party-Bibliothek hier zu entführen. ;) – deceze

+0

Du kannst dann einfach die Klasseneigenschaft patchen. z.B. 'Foo.bar = Nondatadeskriptor (Foo.bar.fget)'. Alle anderen Eigenschaften bleiben erhalten, aber Sie können jetzt 'foo.bar' einstellen. – Dunes

+0

Also im Wesentlichen [was er sagte] (http://stackoverflow.com/a/31590289/476)? :) – deceze

-1

ich gerade diesen Q & A gefunden, weil PyCharm über „‚bar‘Eigenschaft kann nicht festgelegt werden“ beschwerte, aber das ist für mich tatsächlich gearbeitet:

>>> class Foo: 
...  @property 
...  def bar(self): 
...    return 'Foo.bar' 
... 
>>> f = Foo() 
>>> f.bar 
'Foo.bar' 
>>> @property 
... def foo_bar(self): 
...  return 42 
... 
>>> Foo.bar = foo_bar 
>>> f.bar 
42 
>>> 

Ich weiß, dass dies nicht die erfüllen kann einzelne Instanz Zustand, aber wenn meine Antwort hier schon war es wäre mir etwas Zeit sparen;)

3
from module import ClassToPatch 

def get_foo(self): 
    return 'foo' 

setattr(ClassToPatch, 'foo', property(get_foo)) 
+2

Sie scheinen Mitglied mit hohem Ansehen zu sein, tut mir leid, dass ich Ihnen das gebrochen habe. Ich bin mir ziemlich sicher, dass Sie verstehen, dass es nett und vorteilhaft für jeden wäre, wenn Sie nur mehr auf das Was/Wie/Warum des Algorithmus, den Sie geschrieben haben, eingehen könnten. – Roylee

+0

Perfekt! Ich habe dies verwendet, um die Basisklasse für ein Framework zu patchen, um ein dringend benötigtes Feature zu unterstützen. Bei weitem der einfachste Weg, es zu tun, obwohl ich denke, dass die Beschreibung hier ein wenig besser sein könnte :) Wenn Sie Ihr .py dann wissen, macht nur der Bitcode genug Sinn. –

1

Um Affen, der eine Eigenschaft Patch gibt es eine noch einfachere Art und Weise:

from module import ClassToPatch 

def get_foo(self): 
    return 'foo' 

ClassToPatch.foo = property(get_foo) 
+0

Die Frage war, wie man eine vorhandene Eigenschaft überschreibt. Diese Antwort hat damit nichts zu tun. – MarredCheese

+0

Ich bin mir nicht sicher, ob ich Ihren Kommentar verstehe? In meinem Verständnis wird es in der Tat eine bestehende Eigenschaft überschreiben (die Frage war nicht über Unterklassen, sondern Affepatching). – fralau

+0

Entschuldigung, mein Kommentar war vage und basierte darauf, dass ich deine Antwort falsch interpretierte. Ich dachte, du würdest 'instance.foo = ...' machen, was fehlschlagen würde, wenn 'foo' eigentlich eine existierende Eigenschaft dieser Klasse wäre, aber du hast' class.foo = ... 'geschrieben, was gut funktioniert . Also habe ich mich geirrt. Aber während wir dabei sind, gibt es noch zwei weitere Probleme: 1) Der Fragesteller hat versucht, eine Instanz zu patchen, keine Klasse und 2) sie haben versucht, ihn mit einem statischen Wert zu patchen, nicht mit einer anderen Eigenschaft. Egal, ich denke, deine Antwort bringt noch einen gewissen Mehrwert. – MarredCheese