使用with语句基本上有三种方法:
使用现有的上下文管理器:
with manager:
pass
创建一个上下文管理器并将其结果绑定到一个变量:
with Manager() as result:
pass
创建一个上下文管理器并丢弃其返回值:
with Manager():
pass
如果我们在上面的三个块中放置了一个函数get_manager()
,是否有任何实现可以返回封闭的上下文管理器,或者至少它们的__exit__
函数?
在第一种情况下,这显然很容易,但我想不出让其在另外两种情况下工作的方法。我怀疑是否有可能获得整个上下文管理器,因为值堆栈会在SETUP_WITH
操作码之后立即弹出。但是,由于__exit__
函数由SETUP_WITH
存储在块堆栈中,是否有某种方法可以访问它?
答案 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__
或其他东西挖掘堆栈,否则它可能不存在。