2016-02-11 15 views
9

In Python, ci sono molte funzioni che funzionano sia come funzioni standard che come gestori di contesto. Per esempio open() può essere chiamato sia come:Python: funzione standard e gestore contesto?

my_file=open(filename,'w') 

o

with open(filename,'w') as my_file: 

Entrambi dando un oggetto my_file che può essere utilizzato per fare tutto ciò che è necessario. In generale, la successiva è preferibile, ma ci sono momenti in cui si potrebbe voler fare anche la prima.

Sono stato in grado di capire come scrivere un gestore di contesto, sia con la creazione di una classe con __enter__ e __exit__ funzioni o utilizzando il @contextlib.contextmanager decoratore su una funzione e yield piuttosto che return. Tuttavia, quando lo faccio non riesco più a usare la funzione direttamente - utilizzando il decoratore, ad esempio, ottengo un oggetto _GeneratorContextManager anziché il risultato desiderato. Ovviamente, se l'avessi fatto come una classe, avrei appena ottenuto un'istanza della classe del generatore, che presumo sia essenzialmente la stessa cosa.

Quindi, come è possibile progettare una funzione (o classe) che funzioni come una funzione, restituendo un oggetto o un gestore di contesto, restituendo un valore _GeneratorContextManager o simile?

edit:

Ad esempio, dire ho una funzione simile alla seguente (questo è altamente semplificata):

def my_func(arg_1,arg_2): 
    result=arg_1+arg_2 
    return my_class(result) 

Quindi la funzione accetta un numero di argomenti, fa cose con loro, e usa il risultato di quella roba per inizializzare una classe, che poi restituisce. Il risultato finale è che ho un'istanza di my_class, proprio come avrei un oggetto file se avessi chiamato open. Se voglio essere in grado di utilizzare questa funzione come un contesto manager, posso modificarlo in questo modo:

@contextlib.contextmanager 
def my_func(arg_1,arg_2): 
    result=arg_1+arg_2 # This is roughly equivalent to the __enter__ function 
    yield my_class(result) 
    <do some other stuff here> # This is roughly equivalent to the __exit__function 

Il che funziona bene quando si chiama come un contesto manager, ma non ottengo più un'istanza di my_class quando chiamare come una funzione diritta. Forse sto solo facendo qualcosa di sbagliato?

Edit 2:

Nota che io ho il pieno controllo su my_class, tra cui la possibilità di aggiungere funzioni ad esso. Dalla risposta accettata di seguito, sono stato in grado di dedurre che la mia difficoltà derivava da un malinteso fondamentale: stavo pensando che qualsiasi cosa avessi chiamato (my_func nell'esempio precedente) necessitava delle funzioni __exit__ e __enter__. Questo non è corretto. In effetti, è solo ciò che la funzione restituisce (my_class nell'esempio precedente) che richiede le funzioni per funzionare come gestore di contesto.

+2

'open' è una funzione che restituisce un'istanza di una classe. Sia che tu usi 'myfile = open (filename)' o 'con open (filename) come myfile', rimarrà comunque un'istanza della stessa classe. Niente è cambiato. – zondo

+0

@zondo Vero, come la funzione che sto cercando di scrivere. Tuttavia, quando avvolgo la funzione nel decoratore '@ contextlib.contextmanager' e la chiamo come una funzione standard, la classe che ottengo NON è la classe I" resa "dalla funzione. Solo quando lo chiamo come gestore di contesto ottengo quella classe. Aggiungerò un semplice esempio – ibrewster

+1

Definisci un metodo '__call__' nella tua classe. –

risposta

1

La difficoltà che stai andando a correre in è che per una funzione per essere utilizzato sia come un gestore di contesto (with foo() as x) e una funzione regolare (x = foo()), l'oggetto restituito dalla funzione deve avere sia __enter__ e __exit__ metodi ... e non c'è un ottimo modo - nel caso generale - per aggiungere metodi a un oggetto esistente.

Un approccio potrebbe essere quello di creare una classe wrapper che utilizza __getattr__ passare metodi e attributi per l'oggetto originale:

class ContextWrapper(object): 
    def __init__(self, obj): 
     self.__obj = obj 

    def __enter__(self): 
     return self 

    def __exit__(self, *exc): 
     ... handle __exit__ ... 

    def __getattr__(self, attr): 
     return getattr(self.__obj, attr) 

Ma questo causerà problemi sottili perché non è esattamente lo stesso l'oggetto restituito dalla funzione originale (ad esempio, i test isinstance non verranno eseguiti, alcuni builtin come iter(obj) non funzioneranno come previsto, ecc.).

Si potrebbe anche sottoclasse dinamicamente l'oggetto restituito come dimostrato qui: https://stackoverflow.com/a/1445289/71522:

class ObjectWrapper(BaseClass): 
    def __init__(self, obj): 
     self.__class__ = type(
      obj.__class__.__name__, 
      (self.__class__, obj.__class__), 
      {}, 
     ) 
     self.__dict__ = obj.__dict__ 

    def __enter__(self): 
     return self 

    def __exit__(self, *exc): 
     ... handle __exit__ ... 

Ma questo approccio crei problemi di troppo (come indicato nel post linkato), ed è un livello di magia io personalmente wouldn' t essere comodo introducendo senza giustificazione forte.

io in genere preferisco sia l'aggiunta di esplicite __enter__ e __exit__ metodi, o utilizzando un aiutante come contextlib.closing:

with closing(my_func()) as my_obj: 
    … do stuff … 
+0

Ah, la chiave che mi mancava era che l'oggetto * restituito * necessitava delle funzioni '__enter__' e' __exit__' - non necessariamente l'oggetto (funzione in questo caso) * chiamato *.Quindi, aggiungendo queste funzioni alla classe restituita dalla funzione, con la funzione '__enter__' che semplicemente restituisce' self', funziona! – ibrewster

1

Solo per chiarezza: se si è in grado di cambiare my_class, si sarebbe naturalmente aggiungere le __enter__/__exit__ descrittori quella classe.

Se non si è in grado di cambiare my_class (che ho dedotto dalla tua domanda), questa è la soluzione mi riferivo a:

class my_class(object): 

    def __init__(self, result): 
     print("this works", result) 

class manage_me(object): 

    def __init__(self, callback): 
     self.callback = callback 

    def __enter__(self): 
     return self 

    def __exit__(self, ex_typ, ex_val, traceback): 
     return True 

    def __call__(self, *args, **kwargs): 
     return self.callback(*args, **kwargs) 


def my_func(arg_1,arg_2): 
    result=arg_1+arg_2 
    return my_class(result) 


my_func_object = manage_me(my_func) 

my_func_object(1, 1) 
with my_func_object as mf: 
    mf(1, 2) 

come decoratore:

@manage_me 
def my_decorated_func(arg_1, arg_2): 
    result = arg_1 + arg_2 
    return my_class(result) 

my_decorated_func(1, 3) 
with my_decorated_func as mf: 
    mf(1, 4) 
+1

Sono abbastanza in grado di cambiare my_class. La mia confusione derivava dal fatto che non sto chiamando (o istanziando) my_class direttamente - piuttosto, chiamo una funzione che restituisce un'istanza di my_class, e volevo essere in grado di usare quella funzione sia come funzione che come contesto manager - proprio come si può con la funzione open(). Non mi rendevo conto che era l'oggetto * restituito * che doveva comportarsi come un gestore di contesto: pensavo che la funzione che * chiamavo * fosse un gestore di contesto. – ibrewster

Problemi correlati