2015-02-14 8 views
7

Mi piacerebbe assicurarmi che la classe sia istanziata solo all'interno di un'istruzione "con".Come verificare se un oggetto viene creato con l'istruzione `with`?

vale a dire questo è ok:

with X() as x: 
... 

e questo non è:

x = X() 

Come posso garantire tale funzionalità?

+4

E ** perché in nome del cielo ** vorresti mai farlo? 'x = X()', 'con x come result_of_entering:' (creando il CM e usandolo su due linee separate) improvvisamente non è più un caso d'uso valido? Cosa succede se volevo memorizzare i gestori di contesto in una mappatura per selezionarne uno in modo dinamico? –

+1

E se volessi usare un ['contextlib.ExitStack()'] (https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack)? combinare più gestori di contesto? Esistono diversi buoni casi di utilizzo in cui viene creato un gestore di contesto al di fuori di un'istruzione 'with' che si sta tentando di impedire. Non cercare di correggere tutti i possibili errori del programmatore a scapito di rendere la vita più difficile per quei programmatori che sanno cosa stanno facendo. –

risposta

2

Tutte le risposte finora non fornire ciò che (penso) OP vuole direttamente.
(credo) OP vuole qualcosa di simile:

>>> with X() as x: 
... # ok 

>>> x = X() # ERROR 

Traceback (most recent call last): 
    File "run.py", line 18, in <module> 
    x = X() 
    File "run.py", line 9, in __init__ 
    raise Exception("Should only be used with `with`") 
Exception: Should only be used with `with` 

Questo è ciò che mi è venuta in mente, potrebbe non essere molto robusto, ma penso che sia più vicina alla volontà di OP.

import inspect 
import linecache 

class X(): 

    def __init__(self): 
     if not linecache.getline(__file__, 
      inspect.getlineno(inspect.currentframe().f_back) 
     ).startswith("with "): 
      raise Exception("Should only be used with `with`") 

    def __enter__(self): 
     return self 

    def __exit__(self, *exc_info): 
     pass 

Questo darà la stessa uscita esatta come mostrato sopra finché with è nella stessa linea con X() utilizzando gestore contesto.

+0

Sì, questo è quello che volevo. Grazie. – vpas

+9

Ed è per questo che non possiamo avere cose belle. –

+1

Funziona solo con 'with' che è al livello di modulo –

9

Non c'è un modo semplice, per quanto ne so. Ma puoi avere un flag booleano, per verificare se è stato invocato __enter__, prima che venissero chiamati i metodi effettivi negli oggetti.

class MyContextManager(object): 

    def __init__(self): 
     self.__is_context_manager = False 

    def __enter__(self): 
     print "Entered" 
     self.__is_context_manager = True 
     return self 

    def __exit__(self, exc_type, exc_value, traceback): 
     print "Exited" 

    def do_something(self): 
     if not self.__is_context_manager: 
      raise Exception("MyContextManager should be used only with `with`") 

     print "I don't know what I am doing" 

Quando lo si utilizza con with,

with MyContextManager() as y: 
    y.do_something() 

otterrete

Entered 
I don't know what I am doing 
Exited 

Ma, quando si crea manualmente un oggetto, e richiama do_something,

x = MyContextManager() 
x.do_something() 

otterrete

Traceback (most recent call last): 
    File "/home/thefourtheye/Desktop/Test.py", line 22, in <module> 
    x.do_something() 
    File "/home/thefourtheye/Desktop/Test.py", line 16, in do_something 
    raise Exception("MyContextManager should be used only with `with`") 
Exception: MyContextManager should be used only with `with` 

Nota: Questa non è una soluzione solida. Qualcuno può invocare direttamente il metodo __enter__ prima di chiamare qualsiasi altro metodo e il metodo __exit__ non può mai essere chiamato in quel caso.

Se non si vuole ripetere che il check in ogni funzione, si può rendere un decoratore, come questo

class MyContextManager(object): 

    def __init__(self): 
     self.__is_context_manager = False 

    def __enter__(self): 
     print "Entered" 
     self.__is_context_manager = True 
     return self 

    def __exit__(self, exc_type, exc_value, traceback): 
     print "Exited" 

    def ensure_context_manager(func): 
     def inner_function(self, *args, **kwargs): 
      if not self.__is_context_manager: 
       raise Exception("This object should be used only with `with`") 

      return func(self, *args, **kwargs) 
     return inner_function 

    @ensure_context_manager 
    def do_something(self): 
     print "I don't know what I am doing" 
+0

Si potrebbe usare '__getattribute__' per inchiodare veramente tutto per tutto –

7

Non esiste un approccio infallibile per garantire che un caso è costruito all'interno di una clausola with , ma è possibile creare un'istanza nel metodo __enter__ e restituirla invece di self; questo è il valore che verrà assegnato a x. Così si può considerare X come una fabbrica che crea l'istanza effettiva nel suo metodo __enter__, qualcosa di simile:

class ActualInstanceClass(object): 
    def __init__(self, x): 
     self.x = x 

    def destroy(self): 
     print("destroyed") 

class X(object): 
    instance = None 
    def __enter__(self): 

     # additionally one can here ensure that the 
     # __enter__ is not re-entered, 
     # if self.instance is not None: 
     #  raise Exception("Cannot reenter context manager") 
     self.instance = ActualInstanceClass(self) 

    def __exit__(self, exc_type, exc_value, traceback): 
     self.instance.destroy() 
     return None 

with X() as x: 
    # x is now an instance of the ActualInstanceClass 

Naturalmente questo è ancora riutilizzabile, ma ogni with affermazione potrebbe creare una nuova istanza.

Naturalmente si può chiamare il __enter__ manualmente, oppure ottenere un riferimento alla ActualInstanceClass ma sarebbe più abuso invece di utilizzo.


Per un approccio ancora maleodorante, il X() quando chiamato realtà non creare un'istanza XFactory, invece di un X grado; e questo a sua volta quando viene utilizzato come gestore di contesto, crea l'istanza ActualX che è la sottoclasse di X, pertanto isinstance(x, X) restituirà true.

class XFactory(object): 
    managed = None 
    def __enter__(self): 
     if self.managed: 
      raise Exception("Factory reuse not allowed") 

     self.managed = ActualX() 
     return self.managed 

    def __exit__(self, *exc_info): 
     self.managed.destroy() 
     return 


class X(object): 
    def __new__(cls): 
     if cls == X: 
      return XFactory() 
     return super(X, cls).__new__(cls) 

    def do_foo(self): 
     print("foo") 

    def destroy(self): 
     print("destroyed") 

class ActualX(X): 
    pass 

with X() as x: 
    print(isinstance(x, X)) # yes it is an X instance 
    x.do_foo()    # it can do foo 

# x is destroyed 

newx = X() 
newx.do_foo() # but this can't, 
# AttributeError: 'XFactory' object has no attribute 'do_foo' 

Si potrebbe prendere questo ulteriore e hanno XFactory creare un vero e proprio X esempio con uno speciale argomento chiave di __new__, ma ritengo che sia la magia troppo nero per essere utile.

+0

Intelligente! Questa soluzione ha un po 'di odore (come silenziosamente restituisce un oggetto diverso), ma è più pulito di quello che sono riuscito a fare. –

3

Sfortunatamente, non è molto pulito.

I gestori di contesto richiedono i metodi __enter__ e __exit__, quindi è possibile utilizzarlo per assegnare una variabile membro sulla classe per controllare il codice.

class Door(object): 

    def __init__(self, state='closed'): 
     self.state = state 
     self.called_with_open = False 

    # When being called as a non-context manger object, 
    # __enter__ and __exit__ are not called. 
    def __enter__(self): 
     self.called_with_open = True 
     self.state = 'opened' 

    def __exit__(self, type, value, traceback): 
     self.state = 'closed' 

    def was_context(self): 
     return self.called_with_open 


if __name__ == '__main__': 

    d = Door() 
    if d.was_context(): 
     print("We were born as a contextlib object.") 

    with Door() as d: 
     print('Knock knock.') 

L'approccio object stateful ha il bel vantaggio di essere in grado di dire se il metodo __exit__ è stato chiamato in seguito, o per gestire in modo pulito i requisiti del metodo nelle chiamate successive:

def walk_through(self): 
    if self.state == 'closed': 
     self.__enter__ 
    walk() 
1

Ecco un decoratore che automatizza rendendo metodi sicuri, non sono chiamati al di fuori di un contesto manager:

from functools import wraps 

BLACKLIST = dir(object) + ['__enter__'] 

def context_manager_only(cls): 
    original_init = cls.__init__ 
    def init(self, *args, **kwargs): 
     original_init(self, *args, **kwargs) 
     self._entered = False 
    cls.__init__ = init 
    original_enter = cls.__enter__ 
    def enter(self): 
     self._entered = True 
     return original_enter(self) 
    cls.__enter__ = enter 

    attrs = {name: getattr(cls, name) for name in dir(cls) if name not in BLACKLIST} 
    methods = {name: method for name, method in attrs.items() if callable(method)} 

    for name, method in methods.items(): 
     def make_wrapper(method=method): 
      @wraps(method) 
      def wrapper_method(self, *args, **kwargs): 
       if not self._entered: 
        raise Exception("Didn't get call to __enter__") 
       return method(self, *args, **kwargs) 
      return wrapper_method 
     setattr(cls, name, make_wrapper()) 

    return cls 

E qui è un esempio di esso in uso:

@context_manager_only 
class Foo(object): 
    def func1(self): 
     print "func1" 

    def func2(self): 
     print "func2" 

    def __enter__(self): 
     print "enter" 
     return self 

    def __exit__(self, *args): 
     print "exit" 

try: 
    print "trying func1:" 
    Foo().func1() 
except Exception as e: 
    print e 

print "trying enter:" 
with Foo() as foo: 
    print "trying func1:" 
    foo.func1() 
    print "trying func2:" 
    foo.func2() 
    print "trying exit:" 

Questo è stato scritto come risposta a this duplicate question.