¿Cómo verificar si un objeto se crea con la statement `with`?

Me gustaría asegurarme de que la clase solo sea instanciada dentro de una statement “con”.

es decir, este está bien:

with X() as x: ... 

y esto no es

 x = X() 

¿Cómo puedo asegurar tal funcionalidad?

Todas las respuestas hasta ahora no proporcionan lo que (creo) OP quiere directamente .
(Creo) OP quiere algo como esto:

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

Esto es lo que se me ocurre, puede que no sea muy robusto, pero creo que es lo más cercano a la intención de 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 

Esto dará exactamente el mismo resultado que mostré anteriormente, siempre y cuando esté en la misma línea con X() cuando use el administrador de contexto.

No hay un camino directo, que yo sepa. Pero, puede tener una bandera booleana, para verificar si se invocó __enter__ , antes de __enter__ los métodos reales en los objetos.

 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" 

Cuando lo usas with ,

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

conseguirás

 Entered I don't know what I am doing Exited 

Pero, cuando creas manualmente un objeto e do_something ,

 x = MyContextManager() x.do_something() 

conseguirás

 Traceback (most recent call last): File "/home/thefourtheye/Desktop/Test.py", line 22, in  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: Esta no es una solución sólida. Alguien puede invocar directamente el método __enter__ solo, antes de llamar a cualquier otro método y el método __exit__ nunca se puede llamar en ese caso.

Si no desea repetir ese control en cada función, puede convertirlo en un decorador, como este

 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" 

No existe un enfoque infalible para garantizar que una instancia se construya dentro de una cláusula with , pero puede crear una instancia en el método __enter__ y devolverla en lugar de self ; este es el valor que será asignado a x . Por lo tanto, puede considerar X como una fábrica que crea la instancia real en su método __enter__ , algo como:

 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 

Por supuesto, esto todavía es reutilizable, pero cada sentencia with creará una nueva instancia.

Naturalmente, uno puede llamar al __enter__ manualmente, u obtener una referencia a la ActualInstanceClass pero sería más de abuso en lugar de uso.


Para un enfoque aún más oloroso, la X() cuando se llama en realidad crea una instancia de XFactory , en lugar de una instancia de X ; y esto, a su vez, cuando se usa como administrador de contexto, crea la instancia ActualX , que es la subclase de X , por lo que isinstance(x, X) devolverá verdadero.

 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' 

Podría llevar esto más lejos y hacer que XFactory cree una instancia de X real con un argumento de palabra clave especial para __new__ , pero considero que es demasiado magia negra para ser útil.

Desafortunadamente, no puedes muy limpiamente.

Los administradores de contexto requieren tener los métodos __enter__ y __exit__ , por lo que puede usar esto para asignar una variable miembro en la clase para verificar su código.

 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.') 

El enfoque de objetos con estado tiene la ventaja añadida de poder saber si el método __exit__ fue llamado más tarde, o manejar de manera limpia los requisitos del método en llamadas posteriores:

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

Aquí hay un decorador que automatiza asegurarse de que los métodos no sean llamados fuera de un administrador de contexto:

 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 

Y aquí hay un ejemplo de ello en 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:" 

Esto fue escrito como una respuesta a esta pregunta duplicada .