2015-05-05 17 views
5

Ho bisogno di cambiare una chiamata a una funzione all'interno di un'altra funzione durante l'esecuzione.Cambia riferimento alla funzione in fase di esecuzione in Python

Si consideri il seguente codice:

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

Poi ...

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

Un utente ha scritto il codice, myfunc(), che effettua una chiamata a una funzione definita a livello globale now() e non posso modificarlo . Ma voglio invece chiamare il metodo dell'istanza Sim. Quindi avrei bisogno di "patchare" la funzione myfunc() in fase di esecuzione.

Come posso andare su questo?

Una possibile soluzione è di modificare il bytecode come fatto qui: http://www.jonathon-vogel.com/posts/patching_function_bytecode_with_python/ ma mi chiedo se c'è un modo più semplice.

+0

Nel vostro caso d'uso effettivo, sono 'now' e' myfunc' definiti nello stesso modulo l'uno dell'altro? Ti importa se altri usi di "now" sono interessati? Se sono in moduli diversi, in che modo 'myfunc' accede alla funzione' now'? L'approccio richiesto potrebbe essere un po 'diverso se fosse 'da somemodule import now ... now()' vs 'import somemodule ... somemodule.now()'. – Weeble

+0

Sì, importa. Ho aggiornato l'esempio per tener conto di ciò. L'utente finale importerebbe il globale '' now'' e '' Sim'' da una libreria nel loro codice. Potrei modificare il globale '' now'', ma non posso permettermi una costosa ricerca all'interno del globale '' now'' per trovare una versione locale. – jpcgt

risposta

2

Questo è più difficile di quanto possa sembrare. Per farlo è necessario:

  • creare una sottoclasse dict con un metodo __missing__ per tenere il vostro nuovo valore di now. Il metodo __missing__ quindi prende tutti gli elementi non presenti nel dizionario dal consueto globals() dict.

  • Creare un nuovo oggetto funzione dalla funzione esistente myfunc(), mantenendo il suo oggetto codice ma utilizzando il nuovo dizionario creato per le globali.

  • Assegnare la nuova funzione in globale utilizzando il nome della sua funzione.

Ecco come:

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

non c'è davvero alcun bisogno di avere in una classe, ma ho mantenuto in questo modo dato che questo è il modo in cui lo ha scritto in origine.

Se myfunc è in un altro modulo, è necessario riscrivere Sim.patch() di patch di nuovo in quello spazio dei nomi, ad esempio, module.myfunc = sim.patch(module.myfunc)

+0

Funzionerà su 2 diverse istanze di '' Sim() '' come nel codice aggiornato? Sembra che stia modificando l'originale '' myfunc'' e modificheremo le patch precedenti. Forse copiare in profondità '' myfunc'' prima e poi applicare le patch potrebbe fare il trucco? – jpcgt

+0

Dovrebbe funzionare con più istanze. Ovviamente solo una versione di 'myfunc' può trovarsi nei globali contemporaneamente, ma se la si chiama tramite l'istanza di' Sim' (rendendo 'Sim' callable aggiungendo un metodo' __call__', o semplicemente chiamando 'sim. func() '), ognuno usa la propria versione di' myfunc'. È anche possibile rinviare il patching fino a quando non si chiama l'istanza. – kindall

+0

Questo ha funzionato alla grande. Vorrei solo sottolineare che '' globals() [funcname] = name'' cambierà l'originale '' myfunc'', quindi dovrebbe essere opzionale a seconda del risultato richiesto. Personalmente ho appena commentato e ora fa quello che volevo. – jpcgt

-1

Le funzioni hanno un attributo func_globals. In questo caso si implementa patch in questo modo:

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) 

che stampa:

Hello World! 
Hello Locals! 
+0

Ciò farà sì che * tutte * le chiamate a 'now' vengano sostituite, non solo le chiamate a' now' dall'interno di 'myfunc'. – BrenBarn

+0

Oops. Mi è mancato. Modifica la mia risposta. –

+0

Combina la funzione che si desidera applicare e la funzione che si desidera venga utilizzata dall'altra funzione. Inoltre, sostituisce ancora le chiamate a 'now' da altre funzioni nel modulo. – user2357112

0

Non molto elegante, ma si può avvolgere la func con un'altra funzione che modifica l'ambiente globale solo mentre func è in esecuzione.

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

Questa risposta è destinata esclusivamente al divertimento. Per favore, non farlo mai.

Esso può essere fatto sostituendo myfunc con una funzione wrapper che esegue una copia del dizionario variabile globale, sostituisce il valore offendere 'now', fa una nuova funzione con l'oggetto codice dell'originale myfunc e il nuovo dizionario globale, e quindi chiama quella nuova funzione invece dell'originale myfunc. Come questo:

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

Poi:

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

Ci sono un sacco di ragioni per non farlo. Non funziona se la funzione now degli accessi myfunc non è nello stesso modulo di Sim. Potrebbe rompere altre cose. Inoltre, avere una semplice istanza di classe come Sim(myfunc) cambiare lo stato globale in questo modo è davvero malvagio.

0

È possibile utilizzare il pacchetto mock, che già si occupa di gran parte del duro lavoro di patching di una chiamata di funzione. È un'installazione di terze parti in Python 2 (e nelle prime versioni di 3?), Ma fa parte della libreria standard Python 3 come unittest.mock. È principalmente destinato ai test, ma puoi provare a utilizzarlo qui.

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 
Problemi correlati