【问题标题】:How to check if an object is created with `with` statement?如何检查一个对象是否是用`with`语句创建的?
【发布时间】:2015-02-14 15:41:58
【问题描述】:

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

即这个没问题:

with X() as x:
 ...

这不是:

x = X()

我怎样才能确保这样的功能?

【问题讨论】:

  • 天哪你会想要这样做吗? x = X()with x as result_of_entering:(创建 CM 并在两个单独的行上使用它)是一个有效的用例!如果我想将 CM 存储在映射中以动态选择一个,或者使用contextlib.ExitStack(),该怎么办?合并多个 CM?在许多用例中,CM 在您将阻止的 with 语句之外创建。不要以让那些知道自己在做什么的人更难为代价来尝试修复所有可能的错误。
  • 这正是我想弄清楚的问题,而且——老实说,@MartijnPieters——虽然我知道你对你的回答充满热情,但我不能我的生活到底是什么,你是如此坚定,我做而不是
  • @LaurentStanevich 你想阻止x = X() 工作吗?你能告诉我为什么你认为这是必要的吗? Python 不认为对象的分配是需要被阻止或特殊的。 x = X()with X() as x: 中的 X() 调用表达式在 Python 中被完全相同地处理,两者都将结果放入堆栈,以便下一条指令(赋值或 with 语句块设置)可以使用该对象。阻止分配在这里没有意义,并且会积极破坏重要的用例。
  • @LaurentStanevich:不管怎样,这再次强调了 OP 和你都没有告诉我们为什么你认为你需要这个。这具有XY problem 的所有特征,试图解决更大的问题。如果你退后一步,回到你认为这个问题背后的想法可以解决问题的原始问题,那么我可能会提供帮助。
  • @LaurentStanevich:对于我能想到的任何情况,对于这个问题所代表的 Y 来说,这将是一个合理的 X,正确的答案是由Antti Haapala 编写:通过实现从__enter__ 返回的不同对象类型,因此在以下三行x = X()/# do stuff with target 中分配给target 的东西。那么x = X() 是“无害的”,因为它与target 的对象类型不同。

标签: python with-statement


【解决方案1】:

据我所知,没有直接的方法。但是,您可以有一个布尔标志,以在调用对象中的实际方法之前检查是否调用了 __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"

【讨论】:

  • 您可以使用__getattribute__ 真正确定一切
  • 我不会说这不是一个可靠的解决方案。任何更多的东西都会过度杀伤/有害,因为能够直接调用进入/退出有时非常重要(例如 contextlib.ExitStack)。
  • 函数装饰器确保状态是你想要的样子,看起来非常干净和pythonic。不错。
  • 为什么要经历所有的状态切换?更好的方法是不从__enter__ 返回self。返回一个不同的对象,并让该对象实现do_something()。所以MyContextManager() 根本没有do_something() 方法。从__enter__ 返回的对象确实如此。
  • @MartijnPieters 我猜“状态切换”方法和您的“返回_Different 对象”方法之间的区别在于,后者在技术上不会阻止调用者直接初始化_Different 对象.但我明白你的意思。 :-)
【解决方案2】:

没有万无一失的方法来确保实例是在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__ 提供一个特殊的关键字参数,但我认为这太黑魔法了。

【讨论】:

  • 聪明!这个解决方案有点味道(因为它默默地返回一个不同的对象),但它比我想出的要干净。
  • 为什么是“气味”?上下文管理器协议专门设计以从__enter__ 返回任意对象,因此您可以生成一个新的特殊对象以在您刚刚输入的上下文中使用。这就是许多数据库连接所做的,例如:为上下文返回事务或游标。
  • 不幸的是,OP 从未阐明为什么他们认为需要阻止分配上下文管理器实例,但如果我们将问题视为 XY 问题,X 为:人们错误地使用了上下文管理器,他们在没有进入上下文的情况下对对象进行操作,那么那个问题的正确答案是为上下文返回一个不同的对象,这就是从__enter__ 返回东西的目的
  • @MartijnPieters 虽然我很确定 user559633 不会再继续这个讨论了:D
【解决方案3】:

到目前为止,所有答案都没有提供(我认为)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)).lstrip(
        ).startswith("with "):
            raise Exception("Should only be used with `with`")

    def __enter__(self):
        return self
    
    def __exit__(self, *exc_info):
        pass

只要在使用上下文管理器时withX() 在同一行,这将给出与我上面显示的完全相同的输出。

【讨论】:

  • 是的,这就是我想要的。谢谢。
  • 这就是为什么我们不能拥有美好的事物。
  • 仅适用于模块级别的with
  • 我知道你不喜欢它,我也不喜欢这个解决方案,但我仍然在这里发布它,因为这正是 OP 想要的。
  • 太棒了,不熟悉linecache和朋友。我会在.startswith("with ") 之前添加.strip(),以防它不是顶级
【解决方案4】:

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

上下文管理器需要__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()

【讨论】:

  • 这个答案完全忘记了__self__的返回值。
【解决方案5】:

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

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 的回复。

【讨论】:

  • 这真是错误的做法。只需从 __enter__ 返回一个不同的对象,而不是 self,该对象实现您想要在上下文中公开的方法。
【解决方案6】:

OP 的问题was believed to be an XY problemcurrent chosen answer 确实(也是?)hacky。

我真的不知道 OP 最初的“X 问题”,但我认为动机并不是真的要“阻止 x = X() ASSIGNMENT 工作”。相反,它可能会强制 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."""
        ...

毕竟,我们这里都是成年人……

【讨论】:

  • @MartijnPieters 评论? :-)
猜你喜欢
  • 1970-01-01
  • 2021-08-22
  • 2021-06-08
  • 2023-01-13
  • 2021-12-24
  • 2021-06-25
  • 2015-11-12
  • 2013-06-03
  • 1970-01-01
相关资源
最近更新 更多