как питонический способ унаследовать контекстный менеджер

Python учит нас выполнять очистку объектов с помощью __enter__ и __exit__. Что делать, если мне нужно создать объект, который использует объекты, которые должны использовать диспетчеры контекста? Представьте себе это:

from database1 import DB1
from database2 import DB2

Обычно они используются как таковые:

with DB1() as db1, DB2() as db2:
    db1.do_stuff()
    db2.do_other_stuff()

Что бы ни случилось, db1 и db2 запустят свою функцию __exit__ и очистят соединение, сбросят и т. д.

Когда я поместил бы все это в класс, как бы я это сделал? Это правильно? Очевидно, что это неправильно, менеджер контекста для db1 и db2 запускается в конце блока, как указано в комментариях.

class MyApp(object):
    def __enter__(self):
        with DB1() as self.db1, DB2() as self.db2:
            return self
    def __exit__(self, type, value, traceback):
        self.db1.__exit__(self, type, value, traceback)
        self.db2.__exit__(self, type, value, traceback)

Я даже думал сделать что-то вроде этого: На самом деле это выглядит хорошей идеей (после некоторой очистки):

class MyApp(object):
    def __init__(self):
        self.db1 = DB1()
        self.db2 = DB2()
    def __enter__(self):
        self.db1.__enter__()
        self.db2.__enter__()
        return self
    def __exit__(self, type, value, traceback):
        try:
            self.db1.__exit__(self, type, value, traceback)
        except:
            pass
        try:
            self.db2.__exit__(self, type, value, traceback)
        except:
            pass

РЕДАКТИРОВАТЬ: Исправлен код.


person user37203    schedule 02.07.2015    source источник
comment
Ваша первая попытка не имеет смысла — блок with DB1() ... заканчивается до завершения MyApp.__enter__; self.db1 уже будет __exit__ed задолго до начала MyApp.__exit__.   -  person jonrsharpe    schedule 02.07.2015
comment
Второй вариант почти хорош, и будет в порядке, если вы добавите обработку исключений (чтобы в случае сбоя второго __enter__ или первого __exit__ другое соединение не зависало. При этом рассмотрите contextmanager.   -  person bereal    schedule 02.07.2015
comment
@jonrsharpe, я знаю, поэтому я и спросил в первую очередь.   -  person user37203    schedule 02.07.2015
comment
Вы читали напр. stackoverflow.com/q/8720179/3001761?   -  person jonrsharpe    schedule 02.07.2015


Ответы (3)


Я бы выбрал второе решение, но также обрабатывал ошибки базы данных:

import sys

class MyApp(object):
    def __init__(self):
        self.db1 = DB1()
        self.db2 = DB2()
    def __enter__(self):
        self.db1.__enter__()
        try:
            self.db2.__enter__()
        except:
            self.db1.__exit__(None, None, None) # I am not sure with None
            raise
        return self
    def __exit__(self, type, value, traceback):
        try:
            self.db1.__exit__(self, type, value, traceback)
        finally:
            self.db2.__exit__(self, type, value, traceback)

Первый вызывает __exit__ в __enter__ из-за with - значит, не работает.

EDIT: Также ознакомьтесь с ответом @Ming. Во многих случаях он короче.

person User    schedule 02.07.2015
comment
разве вы не должны настроить переменные так, чтобы они не были в классе? - person John Ruddell; 02.07.2015
comment
Ваша __enter__ функция не return self. - person user37203; 02.07.2015
comment
@ user37203 Спасибо, исправил. @ JohnRuddell, не могли бы вы объяснить, почему? - person User; 02.07.2015
comment
в контексте сначала запускается __init__, затем __enter__. Может быть, путаница, связанная с порядком выполнения? - person user37203; 02.07.2015

Большинство контекстных менеджеров можно просто написать с помощью декоратора @contextmanager. Вы пишете функцию с одним выходом, до того, как выход будет вашей функцией «ввода», а после выхода — вашей функцией «выход». Из-за того, как реализованы генераторы, если оператор yield находится в операторе with, то оператор with не завершается при выходе.

eg.

from contextlib import contextmanager

class SomeContextManager:
    def __init__(self, name):
        self.name = name
    def __enter__(self):
        print("enter", self.name)
        return self
    def __exit__(self, ex_type, value, traceback):
        print("exit", self.name)

class SomeContextManagerWrapper:
    def __init__(self, *context_managers):
        self.context_managers = context_managers
    @property
    def names(self):
        return [cm.name for cm in self.context_managers]

@contextmanager
def context_manager_combiner():
    print("context_manager_combiner entering")
    with SomeContextManager("first") as a, SomeContextManager("second") as b:
        yield SomeContextManagerWrapper(a, b)
    print("context_manager_combiner exiting")

with context_manager_combiner() as wrapper:
    print("in with statement with:", wrapper.names)

выходы:

context_manager_combiner entering
enter first
enter second
in with statement with: ['first', 'second']
exit second
exit first
context_manager_combiner exiting
person Dunes    schedule 02.07.2015
comment
Это лучший ответ, но мне нужно что-то, что я могу применить при создании экземпляра класса. Я не совсем уверен, как это сделать с contextmanager. - person user37203; 02.07.2015
comment
Вы можете вернуть класс в операторе yield, который обертывает вашу базу данных, который может иметь связанные методы для управления базами данных. - person Dunes; 02.07.2015

Зависит от того, чего вы пытаетесь достичь в целом. Одна из возможностей состоит в том, чтобы создать отдельные менеджеры контекста, а затем объединить их с стандартной библиотеки. contextlib.nested. Это даст вам один объект, который ведет себя как ваш пример MyApp, но использует существующую стандартную библиотеку DRY (не повторяйтесь).

person Ming    schedule 02.07.2015
comment
Это приятно! Не могли бы вы добавить исходный код? Можно ли использовать класс с этим? - person User; 02.07.2015