Подтвердить что ты не робот

Можно ли получить доступ к контекстному менеджеру?

Существует три способа использования инструкции with:

Использовать существующий менеджер контекста:

with manager:
    pass

Создайте контекстный менеджер и привяжите его результат к переменной:

with Manager() as result:
    pass

Создайте менеджер контекста и отбросьте его возвращаемое значение:

with Manager():
    pass

Если мы помещаем функцию get_manager() внутри трех с вышеприведенными блоками, есть ли какая-либо реализация, которая может возвращать вложенный менеджер контекста или, по крайней мере, их функцию __exit__?

В первом случае это явно легко, но я не могу придумать способ заставить его работать в двух других. Я сомневаюсь, что можно получить весь менеджер контекста, так как стек значений выставляется сразу после кода операции SETUP_WITH. Однако, поскольку функция __exit__ хранится в стеке блоков SETUP_WITH, есть ли способ получить к ней доступ?

4b9b3361

Ответ 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 или что-то еще, оно также не может существовать.