是否可以访问封闭的上下文管理器?

时间:2013-12-24 21:42:16

标签: python contextmanager

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

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

with manager:
    pass

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

with Manager() as result:
    pass

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

with Manager():
    pass

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

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

3 个答案:

答案 0 :(得分:5)

不幸的是,正如评论中所讨论的,这在所有情况下都是不可能的。创建上下文管理器时,following code is run(至少在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宏的堆栈,即defined as

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

反过来,堆栈指针设置在frame eval开头的框架值堆栈的顶部:

stack_pointer = f->f_stacktop;

其中f是frameobject.h中定义的帧对象。对我们来说不幸的是,这就是路径停止的地方。 python可访问框架对象使用following methods only

定义
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属于PyObject **类型,需要将其包装在一个对象中,以便能够以任何方式从python中访问。

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

答案 1 :(得分:3)

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

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()

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

答案 2 :(得分:2)

此案例与类似出现的案例(例如super之间的区别在于,此处没有要查看的封闭框架。 with语句不是新范围。 sys._getframe(0)(或者,如果您将代码放入函数中,sys._getframe(1))可以正常工作,但它会返回{{1}之前和之后的完全相同的框架声明。

你能做到的唯一方法就是检查字节码。但即便如此也无济于事。例如,试试这个:

with

显然,正如SETUP_WITH所解释的那样,该方法会被查找并推送到堆栈中以便WITH_CLEANUP稍后使用。因此,即使from contextlib import contextmanager @contextmanager def silly(): yield with silly(): fr = sys._getframe(0) dis.dis(fr.f_code) 删除了POP_TOP的返回值,其silly()仍然在堆栈中。

但是没有办法从Python那里得到它。除非你想开始修改字节码,或者用__exit__或其他东西挖掘堆栈,否则它可能不存在。