2011-08-18 8 views
8

Ho scritto un'interfaccia Python in un sistema di distribuzione del lavoro incentrato sui processi che stiamo sviluppando/utilizzando internamente sul mio posto di lavoro. Mentre i programmatori ragionevolmente qualificati, le persone primarie che usano questa interfaccia sono scienziati di ricerca, non sviluppatori di software, quindi la facilità d'uso e mantenere l'interfaccia al massimo grado possibile è fondamentale.Estrazione di una funzione in un contesto diverso in Python

La mia libreria srotola una sequenza di input in una sequenza di file pickle su un file server condiviso, quindi genera processi che caricano tali input, eseguono il calcolo, mettono in sottaceti i risultati e escono; lo script client quindi recupera e produce un generatore che carica e produce i risultati (o ripensa qualsiasi eccezione la funzione di calcolo ha fatto).

Ciò è utile solo perché la funzione di calcolo è di per sé uno degli ingressi serializzati. cPickle è abbastanza contenuto per decollare i riferimenti alle funzioni, ma richiede che la funzione decapata sia reimportabile nello stesso contesto. Questo è problematico. Ho già risolto il problema di trovare il modulo per reimportarlo, ma nella stragrande maggioranza del tempo, si tratta di una funzione di livello superiore che viene decapata e, quindi, non ha un percorso del modulo. L'unica strategia che ho trovato per essere in grado di deserializzare tale funzione sui nodi di calcolo è questo approccio poco nauseante verso simulando l'ambiente originale in cui la funzione è stata decapato prima deserializzazione esso:

... 
# At this point, we've identified the source of the target function. 
# A string by its name lives in "modname". 
# In the real code, there is significant try/except work here. 

targetModule = __import__(modname) 
globalRef = globals() 
for thingie in dir(targetModule): 
    if thingie not in globalRef: 
     globalRef[thingie] = targetModule.__dict__[thingie] 

# sys.argv[2]: the path to the pickle file common to all jobs, which contains 
# any data in common to all invocations of the target function, then the 
# target function itself 
commonFile = open(sys.argv[2], "rb") 
commonUnpickle = cPickle.Unpickler(commonFile) 
commonData = commonUnpickle.load() 
# the actual function unpack I'm having trouble with: 
doIt = commonUnpickle.load() 

la linea finale è il più importante qui - è dove il mio modulo sta rilevando la funzione che dovrebbe effettivamente essere in esecuzione. Questo codice, come scritto, funziona come desiderato, ma manipolare direttamente le tabelle dei simboli come questo è sconvolgente.

Come posso fare questo, o qualcosa di molto simile a questo, che non costringe gli scienziati della ricerca a separare i loro script di calcolo in una struttura di classe appropriata (usano Python come il più eccellente calcolatore grafico di sempre e mi piacerebbe continuare lasciare che lo facciano) il modo in cui Pickle desidera disperatamente, senza la spiacevole, insicura e semplicemente spaventosa manipolazione __dict__ e globals() che sto usando sopra? Credo fermamente che ci sia un modo migliore, ma exec "from {0} import *".format("modname") non lo ha fatto, diversi tentativi di iniettare il carico di pickle nel riferimento targetModule non lo hanno fatto, e eval("commonUnpickle.load()", targetModule.__dict__, locals()) non lo ha fatto. Tutti questi errori non riescono con AttributeError di Unpickle a non riuscire a trovare la funzione in <module>.

Che cos'è un modo migliore?

+0

Avete considerato [ 'marshal'] (http://docs.python.org/library/marshal.html), invece? È il solito modo per mantenere gli "oggetti codice" (anche se per quanto ne so tu potresti avere tutti gli stessi problemi che hai con 'cPickle'). – Cameron

+1

'marshal' non può comprimere tipi di dati definiti dall'utente, come l'oggetto attorno alla funzione se la mia funzione di destinazione è in realtà una funzione di istanza, uno scenario che funziona correttamente qui. I suggerimenti su come impacchettare il '__code__' sono utili (e mi aiutano a risolvere un altro problema che ho da qualche altra parte), ma non risolvono il problema" e il tuo piccolo contesto! _evil cackle_ "lo spazio problematico a cui sto mirando. –

risposta

1

Per un modulo che deve essere riconosciuto come caricato, penso che debba essere inserito in sys.modules, non solo nel suo contenuto importato nel proprio spazio dei nomi globale/locale. Prova a eseguire tutto, quindi ottieni il risultato da un ambiente artificiale.

env = {"fn": sys.argv[2]} 
code = """\ 
import %s # maybe more 
import cPickle 
commonFile = open(fn, "rb") 
commonUnpickle = cPickle.Unpickler(commonFile) 
commonData = commonUnpickle.load() 
doIt = commonUnpickle.load() 
""" 
exec code in env 
return env["doIt"] 
+0

Questa non è un'espressione, sono due affermazioni - la valutazione funziona solo su singole espressioni.Ho provato il codice equivalente usando 'exec' e, sfortunatamente, non ho ottenuto nulla –

+0

Esatto, eval non funzionerà.Tuttavia ho fatto qualcosa di simile una volta in una Esempio di aggiornamento di prova: –

2

Le funzioni di decapaggio possono essere piuttosto fastidiose se si cerca di spostarle in un contesto diverso. Se la funzione non fa riferimento a nulla dal modulo in cui si trova e ai moduli (se presenti) che sono garantiti per l'importazione, è possibile controllare un codice da uno Rudimentary Database Engine trovato nel Cookbook Python.

Per supportare le visualizzazioni, il modulo accademico acquisisce il codice dal callable durante il prelievo della query. Quando arriva il momento di annullare la visualizzazione, viene creata un'istanza LambdaType con l'oggetto codice e un riferimento a uno spazio dei nomi contenente tutti i moduli importati. La soluzione ha dei limiti ma ha funzionato abbastanza bene per l'esercizio.


Esempio Visualizzazioni

class _View: 

    def __init__(self, database, query, *name_changes): 
     "Initializes _View instance with details of saved query." 
     self.__database = database 
     self.__query = query 
     self.__name_changes = name_changes 

    def __getstate__(self): 
     "Returns everything needed to pickle _View instance." 
     return self.__database, self.__query.__code__, self.__name_changes 

    def __setstate__(self, state): 
     "Sets the state of the _View instance when unpickled." 
     database, query, name_changes = state 
     self.__database = database 
     self.__query = types.LambdaType(query, sys.modules) 
     self.__name_changes = name_changes 

è a volte sembra necessario apportare modifiche ai moduli registrati disponibili nel sistema. Se ad esempio è necessario fare riferimento al primo modulo (__main__), potrebbe essere necessario creare un nuovo modulo con lo spazio dei nomi disponibile caricato in un nuovo oggetto modulo. Il same recipe ha utilizzato la seguente tecnica.


Esempio per moduli

def test_northwind(): 
    "Loads and runs some test on the sample Northwind database." 
    import os, imp 
    # Patch the module namespace to recognize this file. 
    name = os.path.splitext(os.path.basename(sys.argv[0]))[0] 
    module = imp.new_module(name) 
    vars(module).update(globals()) 
    sys.modules[name] = module 

+1

Grazie per il suggerimento su come imballare il '__code__' e per lavorare con quello- non ne ero a conoscenza affatto.Sfortunatamente, mi piacerebbe essere in grado di gestire una funzione che è in realtà parte di un'istanza di classe (se, per esempio, è una funzione del tipo di archiviazione dati primaria), che Pickle esegue in modo trasparente, ma questo fallirebbe a. È comunque utile, e lo terrò a mente. –

0

Mentre le funzioni sono pubblicizzati come oggetti di prima classe in Python, questo è un caso in cui si può vedere che sono veramente seconda classe oggetti. È il riferimento al callable, non l'oggetto stesso, che viene messo in salamoia. (Non è possibile pickle direttamente un'espressione lambda.)

C'è un uso alternativo di __import__ che si potrebbe preferire:

def importer(modulename, symbols=None): 
    u"importer('foo') returns module foo; importer('foo', ['bar']) returns {'bar': object}" 
    if modulename in sys.modules: module = sys.modules[modulename] 
    else: module = __import__(modulename, fromlist=['*']) 
    if symbols == None: return module 
    else: return dict(zip(symbols, map(partial(getattr, module), symbols))) 

Quindi questi sarebbero tutti sostanzialmente equivalenti:

from mymodule.mysubmodule import myfunction 
myfunction = importer('mymodule.mysubmodule').myfunction 
globals()['myfunction'] = importer('mymodule.mysubmodule', ['myfunction'])['myfunction'] 
0

La tua domanda Era lungo, e ero troppo drogato per affrontare la tua lunga domanda ... Tuttavia, penso che stai cercando di fare qualcosa che già esiste già una buona soluzione. C'è un fork della libreria parallel python (ovvero pp) che prende funzioni e oggetti e li serializza, li invia a server diversi, quindi li decodifica e li esegue. La forcella vive all'interno del pacchetto pathos, ma è possibile scaricarlo autonomamente qui:

http://danse.cacr.caltech.edu/packages/dev_danse_us

L ' "altro contesto" in questo caso è un altro server ... e gli oggetti vengono trasportati convertendo gli oggetti da codice sorgente e quindi di nuovo agli oggetti.

Se stai cercando di utilizzare decapaggio, tanto nel modo che si sta facendo già, c'è un'estensione mpi4py che serializza argomenti e funzioni, e torna in salamoia valori di ritorno ... Il pacchetto si chiama pyina, ed è comunemente usato per la spedizione codice e oggetti per i nodi del cluster in coordinamento con uno scheduler del cluster.

Sia pathos e pyina fornire map astrazioni (e pipe), e cercare di nascondere tutti i dettagli di calcolo parallelo dietro le astrazioni, così gli scienziati non hanno bisogno di imparare nulla, se non come programmare pitone di serie normale. Utilizzano solo una delle funzioni map o pipe e ottengono il calcolo parallelo o distribuito.

Oh, ho quasi dimenticato. Il serializzatore dill include le funzioni dump_session e load_session che consentono all'utente di serializzare facilmente l'intera sessione dell'interprete e inviarlo a un altro computer (o semplicemente salvarlo per un uso successivo).È molto utile per cambiare i contesti, in un senso diverso.

Get dill, pathos, e pyina qui: https://github.com/uqfoundation