Как манипулировать исключением в __exit__ контекстного менеджера?

Я знаю, что повторно вызывать исключение из метода __exit__() контекстного менеджера — плохой стиль. Итак, я хотел бы добавить атрибут к экземпляру, который может нести контекстную информацию, которая недоступна, если я позволю исключению просочиться или поймаю его. Это позволит избежать повторного повышения.

Альтернативой привязыванию атрибута к исключению было бы проглатывание исключения, установка некоторого состояния экземпляра, который удваивается как рассматриваемый менеджер контекста, а затем проверка этого состояния. Проблема в том, что это приведет к уловке 22, не так ли? Поскольку исключение означает, что выполнение внутри блока with завершается. Нет другого способа повторить операцию, кроме как снова войти в блок with, верно? Таким образом, экземпляр, в котором я пытаюсь сохранить контекстную информацию, исчезнет после возврата метода __exit__().

Итак, вкратце: как я могу манипулировать фактическим ожидающим исключением (если оно есть, что я буду считать заданным для этого вопроса), находясь в методе __exit__()?


person 0xC0000022L    schedule 13.11.2014    source источник
comment
Кто сказал, что это плохой стиль? Контекстные менеджеры — отличный способ преобразовать исключение.   -  person Martijn Pieters    schedule 13.11.2014
comment
@MartijnPieters: на самом деле в документации говорится, что это плохой стиль, потому что он создает проблемы с вложенными менеджерами контекста. И да, для преобразования и без вложенности, хороший момент.   -  person 0xC0000022L    schedule 13.11.2014
comment
Вы не должны повторно вызывать одно и то же исключение (просто возврат None гарантирует сохранение состояния исключения), но вы можете вызвать новое исключение.   -  person Martijn Pieters    schedule 13.11.2014
comment
@MartijnPieters: действительно. Но с исключением все в порядке, за исключением того, что мне нужно передавать некоторую информацию по цепочке вызовов. Вот почему ре-рейз в моем случае был бы плохим стилем.   -  person 0xC0000022L    schedule 13.11.2014


Ответы (1)


Менеджер контекста не исчезает только потому, что блок завершается. Вы можете сохранить его двумя способами:

  1. Сначала создайте диспетчер контекста, назначьте его переменной, а затем используйте with с этим объектом:

    cm = ContextManager()
    with cm:
        # ....
    
    state = cm.attribute
    
  2. Верните сам менеджер контекста из метода __enter__, используйте with ... as ..., чтобы связать его с локальным именем. Это имя не освобождается, когда with выходит:

    with ContextManager() as cm:
        # ....
    
    state = cm.attribute
    

    где ContextManager.__enter__ использует return self.

Вы также можете установить дополнительные атрибуты для самого исключения; нет необходимости повторно вызывать исключение:

>>> class ContextManager(object):
...     def __enter__(self):
...         return self
...     def __exit__(self, tp, v, tb):
...         if tp is None: return
...         v.extra_attribute = 'foobar'
...         self.other_extra_attribute = 'spam-n-ham'
... 
>>> try:
...     with ContextManager() as cm:
...         raise ValueError('barfoo')
... except ValueError as ex:
...     print vars(ex)
... 
{'extra_attribute': 'foobar'}
>>> vars(cm)
{'other_extra_attribute': 'spam-n-ham'}

Здесь исключению был присвоен дополнительный атрибут, который сохранялся на всем пути к обработчику исключений. В приведенном выше я также показываю, что cm по-прежнему привязан к диспетчеру контекста.

person Martijn Pieters    schedule 13.11.2014
comment
Я предполагал, что ваш v.extra_attribute = 'foobar' и мой setattr(exc_value, 'extra_attribute', 'foobar') будут функционально эквивалентны. Однако его использование дает AttributeError: 'str' object has no attribute 'extra_attribute'. Почему значение, которое я получаю, чтобы увидеть экземпляр str вместо экземпляра исключения? Это внутри метода __exit__(). - person 0xC0000022L; 13.11.2014
comment
Вы вызываете исключение string? Это очень не рекомендуется. Используйте только исключения объектов (так что подклассы BaseException). - person Martijn Pieters; 13.11.2014
comment
Если у вас нет контроля над тем, какие исключения возбуждаются, вы не можете устанавливать атрибуты для тех исключений, которые являются строковыми исключениями старого стиля. Стандартная библиотека, конечно, больше их не поднимает, но я не знаю, какую систему вы здесь используете. :-) - person Martijn Pieters; 13.11.2014
comment
И последнее, но не менее важное: поддержка строковых исключений была полностью удалена в Python 2.6, поэтому, если вы видите это в 2.6 или 2.7, что-то еще не так. Не могли бы вы показать мне суть или лепешку, демонстрирующую проблему, которую вы видите? Я, конечно, не могу воспроизвести какие-либо проблемы с setattr() в Python 2.7. - person Martijn Pieters; 13.11.2014
comment
Я создаю любые исключения только на основе BaseException себя. Но код библиотеки, который я вызываю, может вызвать что-то еще. Это на Python 2.6 — у меня нет возможности использовать что-то более новое на машинах, на которых это работает (CentOS с 2.6, без 2.7 в стандартной установке). Я попытаюсь придумать кусок кода, демонстрирующий это, да. Дай мне немного времени. - person 0xC0000022L; 13.11.2014
comment
@ 0xC0000022L: в версии 2.6 вам не будут передаваться строковые исключения, нет. Можете ли вы распечатать repr() первых двух аргументов __exit__? - person Martijn Pieters; 13.11.2014
comment
Оказывается, 2.6 передает мне строковое исключение. Строковое исключение, которое я вижу, это "'Download' object has no attribute 'purge'", и, поскольку я не тот, кто преобразует тип исключения в строку, я должен предположить, что это передается мне Python. Конечно, теперь я могу пойти и устранить причину. Но это было неожиданно. - person 0xC0000022L; 18.11.2014
comment
@ 0xC0000022L: метод __exit__ получает 3 аргумента; тип, значение и трассировка. Какой тип в данном случае? - person Martijn Pieters; 18.11.2014
comment
Это str. Возможно ли, что CentOS делает что-то подобное с Python, как и с ядром Linux? т.е. смешивание и сопоставление по мере необходимости, так что номер версии на самом деле не говорит правду? python -V сообщает Python 2.6.6. - person 0xC0000022L; 18.11.2014
comment
@ 0xC0000022L: странно, потому что попытка вызвать строковое исключение в версии 2.6 вызывает ошибку TypeError. Посмотрите, что происходит с менеджером контекста в версии 2.6 на основе разных стилей создания исключений. - person Martijn Pieters; 18.11.2014
comment
@ 0xC0000022L: у меня была гипотеза, которая неверна. Возможно, CentOS повторно включила строковые исключения в своей сборке 2.6, а не исправила код, вызывающий строковые исключения? Что делает «raise foobar», когда вы вводите его в свой Python? - person Martijn Pieters; 18.11.2014
comment
Python 2.6 на CentOS дает мне описанное вами TypeError. - person 0xC0000022L; 18.11.2014
comment
Странно тогда; в отладчике, когда вашему диспетчеру контекста передается строковое исключение, знаете ли вы, было ли это действительное исключение или что-то еще, называемое __exit__ вручную? Это значение выглядит как сообщение AttributeError. - person Martijn Pieters; 18.11.2014
comment
Мне придется попытаться создать минимальный пример, который имитирует общую структуру. Надеюсь, что смогу воспроизвести это таким образом. И да, это похоже на AttributeError. Ничто в моем коде не вызывает явно __exit__(), по крайней мере. - person 0xC0000022L; 18.11.2014
comment
@ 0xC0000022L: похоже, что это где-то проблема с Python 2.6, см. параметр `exc_value` из `__exit__()` (менеджер контекста) представляет собой строку, а не Исключение (Python 2.6) - person Martijn Pieters; 03.05.2015
comment
@ 0xC0000022L: ошибка обнаружена; это проблема с реализацией Python 2.6: bugs.python.org/issue7853 - person Martijn Pieters; 03.05.2015