How to check if an object is created with `with` s

2020-03-01 18:18发布

I would like to ensure that the class is only instantiated within a "with" statement.

i.e. this one is ok:

with X() as x:
 ...

and this is not:

x = X()

How can I ensure such functionality?

5条回答
冷血范
2楼-- · 2020-03-01 18:34

Unfortunately, you can't very cleanly.

Context managers require having __enter__ and __exit__ methods, so you can use this to assign a member variable on the class to check in your code.

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

The stateful object approach has the nice added benefit of being able to tell if the __exit__ method was called later, or to cleanly handle method requirements in later calls:

def walk_through(self):
    if self.state == 'closed':
        self.__enter__
    walk()
查看更多
该账号已被封号
3楼-- · 2020-03-01 18:35

Here is a decorator that automates making sure methods aren't called outside of a context 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

And here is an example of it in use:

@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 was written as an answer to this duplicate question.

查看更多
干净又极端
4楼-- · 2020-03-01 18:37

There is no straight forward way, as far as I know. But, you can have a boolean flag, to check if __enter__ was invoked, before the actual methods in the objects were called.

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"

When you use it with with,

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

you will get

Entered
I don't know what I am doing
Exited

But, when you manually create an object, and invoke do_something,

x = MyContextManager()
x.do_something()

you will get

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`

Note: This is not a solid solution. Somebody can directly invoke __enter__ method alone, before calling any other methods and the __exit__ method may never be called in that case.

If you don't want to repeat that check in every function, you can make it a decorator, like this

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"
查看更多
We Are One
5楼-- · 2020-03-01 18:46

All answers so far do not provide what (I think) OP wants directly.
(I think) OP wants something like this:

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

This is what I come up with, it may not be very robust, but I think it's closest to OP's intention.

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

This will give the exact same output as I showed above as long as with is in the same line with X() when using context manager.

查看更多
甜甜的少女心
6楼-- · 2020-03-01 18:51

There is no foolproof approach to ensure that an instance is constructed within a with clause, but you can create an instance in the __enter__ method and return that instead of self; this is the value that will be assigned into x. Thus you can consider X as a factory that creates the actual instance in its __enter__ method, something like:

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

Of course this is still reusable, but every with statement would create a new instance.

Naturally one can call the __enter__ manually, or get a reference to the ActualInstanceClass but it would be more of abuse instead of use.


For an even smellier approach, the X() when called does actually create a XFactory instance, instead of an X instance; and this in turn when used as a context manager, creates the ActualX instance which is the subclass of X, thus isinstance(x, X) will return 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'

You could take this further and have XFactory create an actual X instance with a special keyword argument to __new__, but I consider it to be too black magic to be useful.

查看更多
登录 后发表回答