Собираем контекстный менеджер Python: загадка

Я озадачен тем, как расположить все, что могут делать менеджеры контекста Python, в соответствующих местах.

Насколько я понимаю, элементы, которые потенциально могут быть использованы при создании диспетчера контекста, включают:

  • A: то, что всегда случается
  • B: Некоторая подготовка необходима для C
  • C: создать и установить объект X, используемый в контексте
  • D: Делайте то, что происходит, используя успешно установленный X до начала контекста.
  • E: вернуть X в контекст (для использования as)
  • F: Завершите знаком X, когда все в порядке, в конце контекста.
  • G: проанализируйте последствия неудачи в C и B, прежде чем входить в контекст
  • H: разбирайтесь с последствиями неудач в контексте

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

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


Я думаю, что в основном понимаю, как ведет себя диспетчер контекста, реализованный через функцию:

from contextlib import contextmanager     
@contextmanager
def log_file_open(oec_data, build_description, log_dir):
    # A: Something that always happens
    try:
        # B: Some stuff needed to make a_thing
        a_thing = establish_thing_in_a_way_that_might_fail() # C
        # D: Some things that happen using a_thing at context start
        yield a_thing # E
        # F: Wrap up with a_thing when all is well
    except:
        # G: Deal the consequences of failure in try or...
        # H: Deal the consequences of failure in context
    finally:
        # Could F go here instead?

Например, чтобы открыть файл, в который следует что-то записать при успешном открытии и закрытии, но который следует очистить, если есть проблема, я мог бы написать

from contextlib import contextmanager     
@contextmanager
def log_file_open(oec_data, build_description, log_dir):
    print('Entering context...')
    try:
        usable_file_name = get_some_name()
        a_thing =  open(usable_file_name, mode='w')
        a_thing.write('Logging context started.')
        yield a_thing
        a_thing.write('Logging context ended.')
    except:
        a_thing.close()
        os.remove(a_thing.name)
        raise

Но я не уверен, что это правильно, и я не понимаю, как это соотносится с использованием __enter()__ и __exit()__ в классах. Это (схематично):

def __init__(self):
    # A: Something that always happens

def __enter__(self):
    try:
        # B: Some stuff needed to make a_thing
        a_thing = establish_thing_in_a_way_that_might_fail() # C
        # D: Some things that happen using a_thing at context start
     except:
        # G: Deal the consequences of failure in try
        a_thing = some_appropriate_blank_value
     finally:
        return a_thing # E

 def __exit__(self, type, value, traceback):
        if type is None:
            # F: Wrap up with a_thing when all is well
            return True
        else:
            # H: Deal the consequences of failure in context
            return False

person orome    schedule 06.01.2014    source источник


Ответы (2)


Вы смешиваете обработку ошибок при генерации значения контекста и обработку ошибок в самом контексте. Лучше написать:

@contextmanager
def fn(...):
    value = ...      # A, B, C, D: setup
    try:
        yield value  # E: pass value to client
    except:          # or better, finally:
        ...          # F, H: cleanup

Таким образом, вы знаете, что имеете дело только с исключениями, возникшими в клиентском коде, и оптимизируете код очистки, поскольку знаете, что установка прошла успешно. Обычно нет смысла пытаться обрабатывать исключения в установочном коде; вы не хотите, чтобы клиентский код обрабатывал None значение контекста. Это означает, что __enter__ просто:

def __enter__(self):
    self.value = ...   # A, B, C, D: setup
    return self.value  # E: pass value to client

Если __enter__ вызывает исключение, то __exit__ не вызывается.

Также обратите внимание, что finally лучше, чем except, если вы не планируете подавлять исключения из клиентского кода, что очень редко бывает полезно. Итак, __exit__ это просто:

def __exit__(self, type, value, traceback):
    ...                # F, H: cleanup
    return False       # don't suppress any exception
person ecatmur    schedule 06.01.2014

Я думаю, что ваше понимание в основном правильное. Диспетчер контекста - это объект, который управляет контекстом с помощью своих методов __enter__ и __exit__. Так что то, что происходит в __init__, остается верным в течение всего срока службы объекта. Давайте посмотрим на конкретный пример:

class CMan(object):
    def __init__(self, *parameters):
        "Creates a new context manager"
        print "Creating object..."

    def __enter__(self):
        "Enters the manager (opening the file)"
        print "Entering context..."
        a_thing = self # Or any other relevant value to be used in this context
        print "Returning %s" % a_thing
        return a_thing

    def __exit__(self, type, value, traceback):
        "Exits the context"
        if type is None:
            print "Exiting with no exception -> Wrapping up"
            return
        print "Exiting with exception %s" % type

Что будет использоваться как это:

>>> with CMan(1,2,3) as x:
...     print 1 + 1
Creating object...
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up

Обратите внимание, что создание объекта "на лету" не обязательно:

>>> mgr = CMan(1,2,3)
Creating object...
>>> with mgr as x:
...     print 1 + 1
Entering context...
Returning <__main__.CMan object at 0x02514F70>
2
Exiting with no exception -> Wrapping up

Наконец, возвращаемое значение __exit__ определяет, следует ли вызывать исключение. Если значение оценивается как False (например, False, 0, _11 _...), любое исключение будет возбуждено. В противном случае это означает, что диспетчер контекста обработал исключение, и его не нужно вызывать. Например:

>>> class Arithmetic(object):
...     def __enter__(self):
...         return self
...     def __exit__(self, type, value, traceback):
...         if type == ZeroDivisionError:
...             print "I dont care -> Ignoring"
...             return True
...         else:
...             print "Unknown error: Panicking !"
...             return False

>>> with Arithmetic() as a:
...     print 1 / 0 # Divide by 0
I dont care -> Ignoring

>>> with Arithmetic() as a:
...     print 1 + "2" # Type error
Unknown error: Panicking !
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

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

>>> with X as x:
...     f(x)

как эквивалент:

>>> x = X.__enter__()
>>> try:
...     exc = None
...     f(x)     
... except Exception as e:
...     exc = e
... finally:
...     handled = X.__exit__(exc)
...     if exc and not handled:
...         raise exc

Конечно, если исключение возникает внутри вашего метода __enter__ или __exit__, оно должно обрабатываться соответствующим образом, например если генерация a_thing может завершиться неудачей. Вы можете найти множество ресурсов в Интернете, выполнив поиск по запросу «Python with statement», как обычно вы относитесь к этому шаблону (хотя диспетчер контекста действительно более правильный)

person val    schedule 06.01.2014