使用上下文管理器管理多种资源的正确方法

时间:2019-06-11 17:34:14

标签: python contextmanager

我有一些包装在上下文管理器类中的资源。

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中,并使其本身成为上下文管理器。

这是我第一次尝试创建管理2种资源的Task类:

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

面对异常释放barfoo仍然被释放:

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 个答案:

答案 0 :(得分:1)

如评论中所述,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
相关问题