2015-05-05 11 views
5

Ich muss einen Aufruf einer Funktion in einer anderen Funktion während der Laufzeit ändern.Ändern der Verweis auf Funktion in Laufzeit in Python

Betrachten Sie den folgenden Code ein:

def now(): 
    print "Hello World!" 

class Sim: 
    def __init__(self, arg, msg): 
     self.msg = msg 
     self.func = arg 
     self.patch(self.func) 

    def now(self): 
     print self.msg 

    def run(self): 
     self.func() 

    def patch(self, func): 
     # Any references to the global now() in func 
     # are replaced with the self.now() method. 

def myfunc(): 
    now() 

Then ...

>>> a = Sim(myfunc, "Hello Locals #1") 
>>> b = Sim(myfunc, "Hello Locals #2") 
>>> b.run() 
Hello Locals #2 
>>> a.run() 
Hello Locals #1 

Ein Benutzer hat Code geschrieben, myfunc(), die einen Aufruf zu einer global definierte Funktion macht now() und ich kann es nicht ändern . Aber ich möchte, dass es stattdessen die Methode der Sim Instanz aufruft. Also müsste ich die myfunc() Funktion in Laufzeit "patchen".

Wie könnte ich darüber gehen?

Eine mögliche Lösung ist, den Bytecode wie hier getan zu bearbeiten: http://www.jonathon-vogel.com/posts/patching_function_bytecode_with_python/ aber ich frage mich, ob es einen einfacheren Weg gibt.

+0

In Ihrem tatsächlichen Anwendungsfall sind 'now' und' myfunc' im selben Modul definiert? Interessiert es dich, wenn andere Nutzungen von "jetzt" betroffen sind? Wenn sie in verschiedenen Modulen sind, wie greift 'myfunc' dann auf die' now' Funktion zu? Der erforderliche Ansatz könnte ein bisschen anders sein, wenn er 'somemodule import now ... now()' vs 'importiert somemodule ... somemodule.now()'. – Weeble

+0

Ja, es ist wichtig. Ich habe das Beispiel aktualisiert, um dies zu berücksichtigen. Der Endbenutzer würde die globalen Namen '' now'' und '' Sim'' aus einer Bibliothek in ihrem Code importieren. Ich könnte das globale '' now'' modifizieren, aber ich kann mir kein kostspieliges Nachschlagen innerhalb des globalen ''now'' leisten, um eine lokale Version zu finden. – jpcgt

Antwort

2

Das ist kniffliger als es scheint. Um es zu machen, müssen Sie:

  • erstellen dict Unterklasse mit einer __missing__ Methode, um Ihren neuen Wert von now zu halten. Die __missing__ Methode greift dann alle Elemente, die nicht im Wörterbuch enthalten sind, von der üblichen globals() dict.

  • Erstellen Sie ein neues Funktionsobjekt aus der vorhandenen myfunc()-Funktion, wobei Sie das Codeobjekt beibehalten, jedoch das neue Wörterbuch verwenden, das Sie für globale Variablen erstellt haben.

  • Weisen Sie die neue Funktion mithilfe ihres Funktionsnamens in globals zurück.

Hier ist, wie:

def now(): 
    print "Hello World!" 

class NowGlobals(dict): 
    def __init__(self, now, globals): 
     self["now"] = now 
     self.globals = globals 
    def __missing__(self, key): 
     return self.globals[key] 

class Sim(object): 
    def __init__(self, func): 
     func = self.patch(func) 
     self.func = func 
    def now(self): 
     print "Hello locals!" 
    def patch(self, func): 
     funcname = func.__name__ 
     nowglobals = NowGlobals(self.now, func.func_globals) 
     func = type(func)(func.func_code, nowglobals) 
     globals()[funcname] = func 
     return func 

def myfunc(): 
    now() 

sim = Sim(myfunc) 
myfunc() 

Es gibt wirklich keine Notwendigkeit, es in einer Klasse zu haben, aber ich habe es so gehalten, da das ist so, wie Sie es ursprünglich geschrieben.

Wenn myfunc in einem anderen Modul ist, müssen Sie Sim.patch() umschreiben, um in diesen Namespace, z. module.myfunc = sim.patch(module.myfunc)

+0

Funktioniert dies an 2 verschiedenen Instanzen von '' Sim() '' wie im aktualisierten Code? Es sieht so aus, als würde es das Original '' myfunc'' modifizieren und das vorherige Patching modifizieren. Vielleicht könnte das tiefe Kopieren von '' myfunc'' zuerst und dann das Patchen den Trick machen? – jpcgt

+0

Sollte mit mehreren Instanzen arbeiten. Offensichtlich kann nur eine Version von 'myfunc' gleichzeitig in den Globals sein, aber wenn Sie sie über die' Sim'-Instanz aufrufen (entweder machen Sie 'Sim' aufrufbar, indem Sie eine' _call__' Methode hinzufügen oder einfach 'sim. func() ') verwendet jeweils eine eigene Version von' myfunc'. Sie können das Patchen auch aufschieben, bis Sie die Instanz aufrufen. – kindall

+0

Das hat super funktioniert. Ich würde nur darauf hinweisen, dass '' globals() [funkname] = name'' das Original '' myfunc'' ändert, daher sollte es abhängig vom gewünschten Ergebnis optional sein. Ich persönlich habe es einfach kommentiert und mache jetzt, was ich wollte. – jpcgt

-1

Funktionen haben ein func_globals Attribut. In diesem Fall implementieren Sie patch wie so:

def now(): 
    print "Hello World!" 


class Sim: 
    def __init__(self, arg): 
     self.func = arg 
     self.patch(self.func) 
     self.func() 

    def now(self): 
     print "Hello Locals!" 

    def patch(self, func): 
     # Any references to the global now() in func 
     # are replaced with the self.now() method. 
     func.func_globals['now'] = self.now 


def myfunc(): 
    now() 


Sim(myfunc) 

Welche druckt:

Hello World! 
Hello Locals! 
+0

Dadurch werden * alle * Anrufe zu 'now' ersetzt, nicht nur Anrufe von' myfunc' zu 'now'. – BrenBarn

+0

Hoppla. Verpasst das. Bearbeiten meiner Antwort. –

+0

Dies mischt die Funktion, die Sie patchen möchten, und die Funktion, die die andere Funktion verwenden soll. Außerdem ersetzt es immer noch Aufrufe von anderen Funktionen im Modul. – user2357112

0

Nicht sehr elegant, aber Sie können Ihre func mit einer anderen Funktion wickeln, die nur die globale Umwelt ändert, während func läuft.

def now(): 
    print "Hello World!" 

class Sim: 
    def __init__(self, arg, msg): 
     self.msg = msg 
    self.func = arg 
    self.patch(self.func) 

    def now(self): 
    print self.msg 

    def run(self): 
    self.func() 

    def patch(self, func): 
    # Any references to the global now() in func 
    # are replaced with the self.now() method. 

    def newfunc(): 
     global now 
     tmp = now 
     now = self.now 
     func() 
     now = tmp 

    self.func = newfunc 


def myfunc(): 
    now() 
1

Diese Antwort ist nur zur Unterhaltung gedacht. Bitte tu das niemals.

Es kann durch das Ersetzen myfunc mit einer Wrapper-Funktion erfolgen, die eine Kopie der globalen Variablen Wörterbuch macht, ersetzt den säumigen 'now' Wert, macht eine neue Funktion mit dem Codeobjekt der ursprünglichen myfunc und die neuen globalen Wörterbuch, und ruft dann diese neue Funktion anstelle des ursprünglichen myfunc auf. Wie folgt aus:

import types 
def now(): 
    print("Hello World!") 

class Sim: 
    def __init__(self, arg): 
     self.func = arg 
     self.patch(self.func) 
     self.func() 

    def now(self): 
     print("Hello Locals!") 

    def patch(self, func): 

     def wrapper(*args, **kw): 
      globalcopy = globals().copy() 
      globalcopy['now'] = self.now 
      newfunc = types.FunctionType(func.__code__, globalcopy, func.__name__) 
      return newfunc(*args, **kw) 
     globals()[func.__name__] = wrapper 

def myfunc(): 
    now() 

def otherfunc(): 
    now() 

Dann:

>>> myfunc() 
Hello World! 
>>> otherfunc() 
Hello World! 
>>> Sim(myfunc) 
Hello World! 
<__main__.Sim instance at 0x0000000002AD96C8> 
>>> myfunc() 
Hello Locals! 
>>> otherfunc() 
Hello World! 

Es gibt viele Gründe, dies nicht zu tun. Es funktioniert nicht, wenn die Funktion now, auf die myfunc zugreift, nicht im selben Modul wie Sim ist. Es könnte andere Dinge brechen. Auch eine einfache Klasseninstanziierung wie Sim(myfunc) globalen Zustand auf diese Weise ändern ist wirklich böse.

0

Sie können das Paket mock verwenden, das sich bereits um einen Großteil der harten Arbeit beim Patchen eines Funktionsaufrufs kümmert. Es ist eine Installation von Drittanbietern in Python 2 (und frühen Versionen von 3?), Aber Teil der Python 3-Standardbibliothek als unittest.mock. Es ist hauptsächlich zum Testen gedacht, aber Sie könnten es hier verwenden.

import mock 

def now(): 
    print "Hello World!" 

class Sim: 
    # Your code from above here 

def myfunc(): 
    now() 

myfunc() # Outputs Hello World! 

s = Sim(myfunc, "Hello Locals #1") 
mock.patch('__main__.now', wraps=s.now).start() 

myfunc() # Now outputs Hello Locals #1