Обратный диспетчер контекста

Я нахожусь на среднем уровне с Python, и недавно я играл с менеджерами контекста Python. Я хотел инвертировать порядок, в котором выполняются вход и выход. Итак, я написал этот менеджер контекста:

class ReversibleContextManager(object):

    def __enter__(self, *args):
        print('d')
        return self

    def __exit__(self, *args):
        print('g')
        return self

    def __invert__(self):
        self.__enter__, self.__exit__ = self.__exit__, self.__enter__
        return self

Он отлично работает вперед:

with ContextManager():
    print('o')

d
o
g

Но в обратном порядке мы все равно получаем:

with ~ContextManager():
    print('a')

d
o
g

Если мы вызовем функции входа и выхода явно, как и ожидалось, мы получим:

with ReversibleContextManager() as c:
    c.__enter__()
    print('o')
    c.__exit__()

d
d
o
g
g

НО порядок ОБРАЩАЕТСЯ для метода экземпляра!

with ~ReversibleContextManager() as c:
    c.__enter__()
    print('o')
    c.__exit__()

d
g
o
d
g

Таким образом, похоже, что вызовы оператора with используют метод, привязанный к классу, а не к экземпляру (правильная ли это терминология?). Ожидается ли это?

i.e.

то, что называется:

c = ReversibleContextManager()
c.__invert__()
ReversibleContextManager.__enter__(c)
...in context...
ReversibleContextManager.__exit__(c)

Вместо того, что я ожидаю:

c = ReversibleContextManager()
c.__invert__()
c.__enter__()
...in context...
c.__exit__()

person RichL    schedule 03.12.2015    source источник


Ответы (4)


Таким образом, похоже, что вызовы оператора with используют метод, привязанный к классу, а не к экземпляру (правильная ли это терминология?). Ожидается ли это?

Абсолютно. Вот как Python обычно ищет специальные методы. В основном это так в случаях, когда у вас есть класс Foo, который реализует __str__, print Foo вызывает type(Foo).__str__ вместо Foo.__str__.

person user2357112 supports Monica    schedule 03.12.2015
comment
@AdamSmith: Если бы поиск специального метода сначала проверял экземпляр, то попытка напечатать сам класс Foo вызвала бы метод __str__, предназначенный для строковой обработки экземпляров Foo. - person user2357112 supports Monica; 04.12.2015
comment
Да, я понял это почти сразу после того, как нажал «Добавить комментарий». Ваши дальнейшие правки очень помогли. Спасибо - person Adam Smith; 04.12.2015
comment
Спасибо за ответ, для меня это имеет смысл. Я предполагаю, что мне нужно немного больше времени, чтобы ковыряться в более привередливых частях языка, чтобы полностью понять, почему это требуется. - person RichL; 04.12.2015

В качестве обходного пути вы можете создать свой обратный класс внутри функции __invert__ и вернуть экземпляр нового класса.

class ReversibleContextManager(object):
    def __enter__(self, *args):
        print('enter')
        return self
    def __exit__(self, *args):
        print('exit')
        return self
    def __invert__(self):
        new = type("ReversibleContextManager",
                   (object,),
                   {'__enter__': self.__exit__,
                    '__exit__': self.__enter__})
        return new()

>>> with ReversibleContextManager() as f:
...     print("normal")
enter
normal
exit

>>> with ~ReversibleContextManager() as f:
...     print("reversed")
exit
reversed
enter
person Adam Smith    schedule 03.12.2015
comment
Хорошая работа, я придумал несколько других, но это самое элегантное! - person RichL; 04.12.2015

Более подробный, но довольно явный способ — использовать две вспомогательные функции и просто переключаться между входом и выходом с помощью флага reverse:

class ReversibleContextManager(object):

    def __init__(self, reverse=False):
        self.reverse = reverse

    def _enter(self, *args):
        print('d')
        return self

    def _exit(self, *args):
        print('g')
        return self

    def __enter__(self, *args):
        if self.reverse:
            return self._exit(*args)
        return self._enter(*args)

    def __exit__(self, *args):
        if self.reverse:
            return self._enter(*args)
        return self._exit(*args)

    def __invert__(self):
        self.reverse = True
        return self


>>> with ReversibleContextManager() as r:
    print('o')
d
o
g

>>> with ~ReversibleContextManager() as r:
    print('o')
g
o
d

>>> with ReversibleContextManager(reverse=True) as r:
    print('o')
g
o
d
person Mike Müller    schedule 03.12.2015
comment
Спасибо за это, на самом деле это был один из моих обходных путей! Однако это не идеально, так как я хотел использовать его как абстрактный класс для смешивания с другим менеджером контекста. Таким образом, вам придется перегружать _enter и _exits, а не __enter__ и __exit__, как можно было бы ожидать. - person RichL; 04.12.2015

Другой путь:

class ReversibleContextManager(object):

    inverted = False

    def __init__(self):
        if self.__class__.inverted:
            self.__invert__()
            self.__class__.inverted = False

    def __enter__(self, *args):
        print('d')
        return self

    def __exit__(self, *args):
        print('g')
        return self

    def __invert__(self):
        self.__class__.inverted = True
        self.__class__.__enter__, self.__class__.__exit__ = self.__exit__, self.__enter__
        return self
person Mike Müller    schedule 03.12.2015