19

基本上有三种使用 with 语句的方法:

使用现有的上下文管理器:

with manager:
    pass

创建一个上下文管理器并将其结果绑定到一个变量:

with Manager() as result:
    pass

创建一个上下文管理器并丢弃它的返回值:

with Manager():
    pass

如果我们get_manager()在上面的三个 with 块中放置了一个函数,是否有任何实现可以返回封闭的上下文管理器,或者至少是它们的__exit__函数?

在第一种情况下显然很容易,但我想不出一种方法让它在其他两种情况下工作。我怀疑是否有可能获得整个上下文管理器,因为值堆栈在SETUP_WITH操作码之后立即弹出。但是,由于__exit__函数存储在块堆栈中SETUP_WITH,有什么方法可以访问它吗?

4

5 回答 5

8

不幸的是,正如评论中所讨论的,这并非在所有情况下都是可能的。创建上下文管理器时,将运行以下代码(至少在 cPython 2.7 中。我无法评论其他实现):

    case SETUP_WITH:
    {
        static PyObject *exit, *enter;
        w = TOP();
        x = special_lookup(w, "__exit__", &exit);
        if (!x)
            break;
        SET_TOP(x);
        /* more code follows... */
    }

__exit__方法使用宏被压入堆栈,SET_TOP定义为

#define SET_TOP(v)        (stack_pointer[-1] = (v))

反过来,堆栈指针在帧 eval开始时设置到帧的值堆栈的顶部:

stack_pointer = f->f_stacktop;

其中 f 是在frameobject.h中定义的框架对象。对我们来说不幸的是,这是小径停止的地方。python 可访问框架对象仅使用以下方法定义:

static PyMemberDef frame_memberlist[] = {
    {"f_back",          T_OBJECT,       OFF(f_back),    RO},
    {"f_code",          T_OBJECT,       OFF(f_code),    RO},
    {"f_builtins",      T_OBJECT,       OFF(f_builtins),RO},
    {"f_globals",       T_OBJECT,       OFF(f_globals), RO},
    {"f_lasti",         T_INT,          OFF(f_lasti),   RO},
    {NULL}      /* Sentinel */
};

不幸的是,其中不包括f_valuestack我们需要的。这是有道理的,因为f_valuestack它是 type PyObject **,它需要被包装在一个对象中,才能以任何方式从 python 访问。

TL;DR:__exit__我们正在寻找的方法只位于一个地方,即框架对象的值堆栈,而 cPython 不会使值堆栈可供 python 代码访问。

于 2014-01-07T16:56:39.073 回答
4

这种情况与类似情况的区别在于super,这里没有封闭框架可供查看。with语句不是新范围。sys._getframe(0)(或者,如果您将代码放入函数中, )会正常工作,但它会返回与语句sys._getframe(1)前后完全相同的帧。with

您可以做到这一点的唯一方法是检查字节码。但即使这样也无济于事。例如,试试这个:

from contextlib import contextmanager

@contextmanager
def silly():
    yield

with silly():
    fr = sys._getframe(0)

dis.dis(fr.f_code)

显然,正如SETUP_WITH解释的那样,该方法确实会被查找并压入堆栈WITH_CLEANUP以供以后使用。因此,即使POP_TOP删除了 的返回值silly(),它__exit__仍然在堆栈中。

但是没有办法从 Python 中得到它。除非你想开始修改字节码,或者用什么来挖掘堆栈,ctypes否则它可能不存在。

于 2013-12-25T00:07:34.857 回答
4

如果上下文管理器是一个类并且只有一个实例,那么您可以在堆上找到它:

import gc

class ConMan(object):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        print "enter %s" % self.name

    def found(self):
        print "You found %s!" % self.name

    def __exit__(self, *args):
        print "exit %s" % self.name


def find_single(typ):
    single = None
    for obj in gc.get_objects():
        if isinstance(obj, typ):
            if single is not None:
                raise ValueError("Found more than one")
            single = obj
    return single



def foo():
    conman = find_single(ConMan)
    conman.found()

with ConMan('the-context-manager'):
    foo()

(免责声明:不要这样做)

于 2013-12-25T00:33:26.390 回答
0

如果您愿意接受一个 hacky 解决方案,我会为您带来一个受此启发的解决方案

让上下文管理器编辑本地命名空间。

class Context(object):
    def __init__(self, locals_reference):
        self.prev_context = locals_reference.get('context', None)
        self.locals_reference = locals_reference

    def __enter__(self):
        self.locals_reference['context'] = self

    def __exit__(self, exception_type, exception_value, traceback):
        if self.prev_context is not None:
            self.locals_reference['context'] = self.prev_context
        else:
            del self.locals_reference['context']

然后,您可以使用上下文变量获取上下文

with Context(locals()):
    print(context)

此实现也适用于嵌套上下文

with Context(locals()):
    c_context = context
    with Context(locals()):
        print(c_context == context)
    print(c_context == context)

但是,这是特定于实现的,因为 的返回值locals可能是命名空间的副本。在 CPython 3.10 上测试。

编辑:

上面的实现在其他模块的函数中不起作用(我想知道为什么),所以这里有一个获取上下文的函数:

def get_current_context(cls) -> "Context | None":
    try:
        if context is not None:
            return context
    except NameError:
        pass

    i = 0
    while True:
        try:
            c = sys._getframe(i).f_locals.get('context',None)
        except ValueError:
            return None

        if c is not None:
            return c
        i += 1

我会将其classmethod设为上下文管理器类。

于 2021-12-15T17:27:24.250 回答
0

许多年后,在 OP 的 3 个案例中的第二个案例中,这是一种直接的方法,with ... as用于将上下文管理器__enter__方法的输出绑定到变量。您可以让该__enter__方法返回上下文管理器本身,或者__exit__如果您只对它感兴趣,则返回它的方法。

class Manager:
     def __enter__(self): return self
     def __exit__(self, *args): print("Doing exit method stuff!")

with Manager() as manager:
    print("Doing some stuff before manually calling the exit method...")
    manager.__exit__() # call the exit method
    print("Doing some more stuff before exiting for real...")

当然,这会干扰使用with ... as从 绑定其他一些返回值__enter__,但是__enter__返回一个由其普通返回值和管理器组成的元组会很简单,就像你可以让一个函数返回多个值一样。

正如 OP 所指出的,在第一种情况下调用该__exit__方法也很简单,其中上下文管理器已经预先分配给一个变量。所以唯一真正棘手的情况是第三种情况,其中上下文管理器只是通过创建with Manager():但从未分配给变量。我的建议是:如果您以后想要引用管理器(或其方法),那么(1)预先为其分配一个名称,(2)让它的__enter__方法返回对它的引用以with ... as捕获为我在上面做了,但是(3)不要在没有存储任何引用的情况下创建它!

于 2021-12-27T17:38:10.053 回答