如何使用`with`语句检查对象是否已创建?

时间:2015-02-14 15:41:59

标签: python with-statement

我想确保只在“with”语句中实例化该类。

即。这个没关系:

with X() as x:
 ...

而这不是:

x = X()

我如何确保此类功能?

6 个答案:

答案 0 :(得分:12)

据我所知,没有直接的方法。但是,在调用对象中的实际方法之前,您可以使用布尔标志来检查是否调用了__enter__

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"

with一起使用时,

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

你会得到

Entered
I don't know what I am doing
Exited

但是,当您手动创建对象并调用do_something时,

x = MyContextManager()
x.do_something()

你会得到

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`

注意:这不是一个可靠的解决方案。在调用任何其他方法之前,有人可以单独直接调用__enter__方法,在这种情况下可能永远不会调用__exit__方法。

如果您不想在每个功能中重复检查,您可以将它设为装饰器,就像这样

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"

答案 1 :(得分:8)

确保在with子句中构造实例没有万无一失的方法,但您可以在__enter__方法中创建一个实例并返回该实例而不是self;这是将分配给x的值。因此,您可以将X视为在__enter__方法中创建实际实例的工厂,例如:

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

当然这仍然是可重用的,但每个with语句都会创建一个新实例。

当然,您可以手动调用__enter__,或者获取对ActualInstanceClass的引用,但更多的是滥用而不是使用。


对于更有气味的方法,X()在调用时确实会创建XFactory实例,而不是X实例;反过来,当用作上下文管理器时,创建ActualX实例,它是X的子类,因此isinstance(x, X)将返回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'

您可以更进一步,让XFactory使用X的特殊关键字参数创建一个实际的__new__实例,但我认为它太黑魔法无法使用。< / p>

答案 2 :(得分:3)

不幸的是,你不能很干净。

上下文管理器需要使用__enter____exit__方法,因此您可以使用此方法在类上分配成员变量以检入代码。

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

有状态对象方法有一个很好的附加好处,即能够判断以后调用__exit__方法,或者在以后的调用中干净地处理方法要求:

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

答案 3 :(得分:2)

到目前为止,所有答案都没有提供(我认为)OP希望直接 (我认为)OP想要这样的事情:

>>> 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`

这是我提出的,它可能不是很强大,但我认为它最接近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

只要with在使用上下文管理器时与X()位于同一行,这将提供与上面显示的完全相同的输出。

答案 4 :(得分:1)

这是一个装饰器,可以自动确保方法不在上下文管理器之外调用:

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

这是一个使用它的例子:

@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:"

这是作为this duplicate question的答案而写的。

答案 5 :(得分:0)

OP的问题was believed to be an XY problemcurrent chosen answer确实(太过分)了。

我真的不知道OP最初的“ X问题”,但我认为动机并不是从字面上阻止“ x = X()分配工作”。相反,这可能会迫使API用户始终使用x作为上下文管理器,以便始终触发其__exit__(...),这就是将class X设计为首先要成为上下文管理员。至少,这就是把我带到这个问答环节的原因。

class Holder(object):
    def __init__(self, **kwargs):
        self._data = allocate(...)  # Say, it allocates 1 GB of memory, or a long-lived connection, etc.
    def do_something(self):
        do_something_with(self._data)
    def tear_down(self):
        unallocate(self._data)

    def __enter__(self):
        return self
    def __exit__(self, *args):
        self.tear_down()

# This is desirable
with Holder(...) as holder:
    holder.do_something()

# This might not free the resource immediately, if at all
def foo():
    holder = Holder(...)
    holder.do_something()

也就是说,在学习了这里的所有对话之后,我最终还是保持了Holder类的原样,好了,我只是为tear_down()添加了一个文档字符串:

    def tear_down(self):
        """You are expect to call this eventually; or you can simply use this class as a context manager."""
        ...

毕竟,我们都同意这里的成年人...