Правильный способ управления несколькими ресурсами с помощью контекстных менеджеров - PullRequest
2 голосов
/ 11 июня 2019

У меня есть некоторые ресурсы, которые я обернул в класс менеджера контекста.

class Resource:
    def __init__(self, res):
        print(f'allocating resource {res}')
        self.res = res

    def __enter__(self):
        return self.res

    def __exit__(self, typ, value, traceback):
        print(f'freed resource {self.res}')

    def __str__(self):
        return f'{self.res}'

Если бы я использовал 2 ресурса напрямую, я мог бы использовать следующий синтаксис:

with Resource('foo') as a, Resource('bar') as b:
    print(f'doing something with resource a({a})')
    print(f'doing something with resource b({b})')

Это работает, как и ожидалось:

allocating resource foo
allocating resource bar
doing something with resource a(foo)
doing something with resource b(bar)
freed resource bar
freed resource foo

Однако я хотел бы обернуть использование этих нескольких ресурсов в класс Task и сделать это самменеджер контекста.

Это моя первая попытка создать класс Task, который управляет 2 ресурсами:

class Task:
    def __init__(self, res1, res2):
        self.a = Resource(res1)
        self.b = Resource(res2)

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.b.__exit__(type, value, traceback)
        self.a.__exit__(type, value, traceback)

    def run(self):
        print(f'running task with resource {self.a} and {self.b}')

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

with Task('foo', 'bar') as t:
    t.run()

И снова это работает, как и ожидалось:

allocating resource foo
allocating resource bar
running task with resource foo and bar
freed resource bar
freed resource foo

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

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

class Resource:
    def __init__(self, res):
        print(f'allocating resource {res}')
        self.res = res

    def __enter__(self):
        return self.res

    def __exit__(self, typ, value, traceback):
        print(f'try free resource {self.res}')
        if self.res == 'bar':
            raise RuntimeError(f'error freeing {self.res} resource')
        print(f'freed resource {self.res}')

    def __str__(self):
        return f'{self.res}'

При предыдущем ручном использовании 2 ресурсов:

try:
    with Resource('foo') as a, Resource('bar') as b:
        print(f'doing something with resource a({a})')
        print(f'doing something with resource b({b})')
except:
    pass

Влицо освобождения исключения bar, foo все еще освобождено:

allocating resource foo
allocating resource bar
doing something with resource a(foo)
doing something with resource b(bar)
try free resource bar
try free resource foo
freed resource foo

Однако, делаято же самое с Task, я пропускаю второй ресурс:

try:
    with Task('foo', 'bar') as t:
        t.run()
except:
    pass

Вывод, показывающий, что я никогда не пробую бесплатно foo:

allocating resource foo
allocating resource bar
running task with resource foo and bar
try free resource bar

Вопросы:

  • Я мог бы заключить каждый явный вызов в Resource.__exit__ в блоке try / except, но затем, если я захочу распространить исключение дальше вверх постек вызовов, в то время как все еще освобождаются все ресурсы, я должен отслеживать все исключения, а затем перебрасывать их потом ... он кажется неправильным.

  • Он также чувствует себя "грязным" вызовом Resource.__exit__ явно из Task.__exit__, вместо того, чтобы оператор with вызывал его для меня неявно.Есть ли способ использовать менеджер контекста внутри класса, как я пытаюсь сделать?

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

1 Ответ

1 голос
/ 11 июня 2019

Как уже упоминалось в комментариях, ExitStack делает именно это.

Диспетчер контекста, разработанный для упрощения программного комбинирования других диспетчеров контекста

Вы можете просто наследовать от ExitStack и вызывать enter_context для каждого ресурса, которым хотите управлять:

class Task(contextlib.ExitStack):
    def __init__(self, res1, res2):
        super().__init__()
        self.a = self.enter_context(Resource(res1))
        self.b = self.enter_context(Resource(res2))

    def run(self):
        print(f'running task with resource {self.a} and {self.b}')

Обратите внимание, что нет необходимости определять свои собственные функции __enter__ и __exit__, как это делает ExitStack для нас.

Используя его как в примере:

try:
    with Task('foo', 'bar') as t:
        t.run()
except:
    pass

Теперь, когда выбрасывается исключение, освобождается bar, foo все еще освобождается:

allocating resource foo
allocating resource bar
running task with resource foo and bar
try free resource bar
try free resource foo
freed resource foo
...