Можно ли получить доступ к контекстному менеджеру?
Существует три способа использования инструкции with:
Использовать существующий менеджер контекста:
with manager:
pass
Создайте контекстный менеджер и привяжите его результат к переменной:
with Manager() as result:
pass
Создайте менеджер контекста и отбросьте его возвращаемое значение:
with Manager():
pass
Если мы помещаем функцию get_manager()
внутри трех с вышеприведенными блоками, есть ли какая-либо реализация, которая может возвращать вложенный менеджер контекста или, по крайней мере, их функцию __exit__
?
В первом случае это явно легко, но я не могу придумать способ заставить его работать в двух других. Я сомневаюсь, что можно получить весь менеджер контекста, так как стек значений выставляется сразу после кода операции SETUP_WITH
. Однако, поскольку функция __exit__
хранится в стеке блоков SETUP_WITH
, есть ли способ получить к ней доступ?
Ответы
Ответ 1
К сожалению, как обсуждалось в комментариях, это невозможно во всех случаях. Когда создается менеджер контекста, выполняется следующий код (по крайней мере, в 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))
Указатель стека, в свою очередь, устанавливается в начало стека значений фрейма в начале frame 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
имеет тип PyObject **
, который нужно будет обернуть в объект, доступный из python любым способом.
TL; DR: метод __exit__
, который мы ищем, находится только в одном месте, стек значений объекта фрейма, а cPython не делает стек значений доступным для кода python.
Ответ 2
Если менеджер контекста является классом и имеет только один экземпляр, вы можете найти его в куче:
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()
(Отказ от ответственности: не делайте этого)
Ответ 3
Разница между этим случаем и похожими случаями, такими как 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
или что-то еще, оно также не может существовать.