raise runtimeerror(\"cannot find t"">
当前位置:   article > 正文

每周一个 Python 模块 | contextlib

"gribapi\\bindings.py\", line 34, in raise runtimeerror(\"cannot find t"

专栏地址:每周一个 Python 模块

用于创建和使用上下文管理器的实用程序。

contextlib 模块包含用于处理上下文管理器和 with 语句的实用程序。

Context Manager API

上下文管理器负责一个代码块内的资源,从进入块时创建到退出块后清理。例如,文件上下文管理器 API,在完成所有读取或写入后来确保它们已关闭。

  1. with open('/tmp/pymotw.txt', 'wt') as f:
  2. f.write('contents go here')
  3. # file is automatically closed
  4. 复制代码

with 语句启用了上下文管理器,API 涉及两种方法:当执行流进入内部代码块时运行 __enter__() 方法,它返回要在上下文中使用的对象。当执行流离开 with 块时,调用上下文管理器的 __exit__() 方法来清理正在使用的任何资源。

  1. class Context:
  2. def __init__(self):
  3. print('__init__()')
  4. def __enter__(self):
  5. print('__enter__()')
  6. return self
  7. def __exit__(self, exc_type, exc_val, exc_tb):
  8. print('__exit__()')
  9. with Context():
  10. print('Doing work in the context')
  11. # output
  12. # __init__()
  13. # __enter__()
  14. # Doing work in the context
  15. # __exit__()
  16. 复制代码

组合上下文管理器和 with 语句是一种更简洁的 try:finally 块,即使引发了异常,也总是调用上下文管理器的 __exit__() 方法。

__enter__() 方法可以返回与 as 子句中指定的名称关联的任何对象。在此示例中,Context 返回使用打开上下文的对象。

  1. class WithinContext:
  2. def __init__(self, context):
  3. print('WithinContext.__init__({})'.format(context))
  4. def do_something(self):
  5. print('WithinContext.do_something()')
  6. def __del__(self):
  7. print('WithinContext.__del__')
  8. class Context:
  9. def __init__(self):
  10. print('Context.__init__()')
  11. def __enter__(self):
  12. print('Context.__enter__()')
  13. return WithinContext(self)
  14. def __exit__(self, exc_type, exc_val, exc_tb):
  15. print('Context.__exit__()')
  16. with Context() as c:
  17. c.do_something()
  18. # output
  19. # Context.__init__()
  20. # Context.__enter__()
  21. # WithinContext.__init__(<__main__.Context object at 0x101f046d8>)
  22. # WithinContext.do_something()
  23. # Context.__exit__()
  24. # WithinContext.__del__
  25. 复制代码

与变量关联的值 c 是返回的 __enter__() 对象,该对象不一定是 Contextwith 语句中创建的实例。

__exit__() 方法接收包含 with 块中引发的任何异常的详细信息的参数。

  1. class Context:
  2. def __init__(self, handle_error):
  3. print('__init__({})'.format(handle_error))
  4. self.handle_error = handle_error
  5. def __enter__(self):
  6. print('__enter__()')
  7. return self
  8. def __exit__(self, exc_type, exc_val, exc_tb):
  9. print('__exit__()')
  10. print(' exc_type =', exc_type)
  11. print(' exc_val =', exc_val)
  12. print(' exc_tb =', exc_tb)
  13. return self.handle_error
  14. with Context(True):
  15. raise RuntimeError('error message handled')
  16. print()
  17. with Context(False):
  18. raise RuntimeError('error message propagated')
  19. # output
  20. # __init__(True)
  21. # __enter__()
  22. # __exit__()
  23. # exc_type = <class 'RuntimeError'>
  24. # exc_val = error message handled
  25. # exc_tb = <traceback object at 0x101c94948>
  26. #
  27. # __init__(False)
  28. # __enter__()
  29. # __exit__()
  30. # exc_type = <class 'RuntimeError'>
  31. # exc_val = error message propagated
  32. # exc_tb = <traceback object at 0x101c94948>
  33. # Traceback (most recent call last):
  34. # File "contextlib_api_error.py", line 34, in <module>
  35. # raise RuntimeError('error message propagated')
  36. # RuntimeError: error message propagated
  37. 复制代码

如果上下文管理器可以处理异常,__exit__() 则应返回 true 值以指示不需要传播该异常,返回 false 会导致在 __exit__() 返回后重新引发异常。

作为函数装饰器的上下文管理器

ContextDecorator 增加了对常规上下文管理器类的支持,使它们可以像用上下文管理器一样用函数装饰器。

  1. import contextlib
  2. class Context(contextlib.ContextDecorator):
  3. def __init__(self, how_used):
  4. self.how_used = how_used
  5. print('__init__({})'.format(how_used))
  6. def __enter__(self):
  7. print('__enter__({})'.format(self.how_used))
  8. return self
  9. def __exit__(self, exc_type, exc_val, exc_tb):
  10. print('__exit__({})'.format(self.how_used))
  11. @Context('as decorator')
  12. def func(message):
  13. print(message)
  14. print()
  15. with Context('as context manager'):
  16. print('Doing work in the context')
  17. print()
  18. func('Doing work in the wrapped function')
  19. # output
  20. # __init__(as decorator)
  21. #
  22. # __init__(as context manager)
  23. # __enter__(as context manager)
  24. # Doing work in the context
  25. # __exit__(as context manager)
  26. #
  27. # __enter__(as decorator)
  28. # Doing work in the wrapped function
  29. # __exit__(as decorator)
  30. 复制代码

使用上下文管理器作为装饰器的一个区别是,__enter__() 返回的值在被装饰的函数内部不可用,这与使用 withas 时不同,传递给装饰函数的参数以通常方式提供。

从生成器到上下文管理器

通过用 __enter__()__exit__() 方法编写类来创建上下文管理器的传统方式并不困难。但是,有时候完全写出所有内容对于一些微不足道的上下文来说是没有必要的。在这些情况下,使用 contextmanager() 装饰器将生成器函数转换为上下文管理器。

  1. import contextlib
  2. @contextlib.contextmanager
  3. def make_context():
  4. print(' entering')
  5. try:
  6. yield {}
  7. except RuntimeError as err:
  8. print(' ERROR:', err)
  9. finally:
  10. print(' exiting')
  11. print('Normal:')
  12. with make_context() as value:
  13. print(' inside with statement:', value)
  14. print('\nHandled error:')
  15. with make_context() as value:
  16. raise RuntimeError('showing example of handling an error')
  17. print('\nUnhandled error:')
  18. with make_context() as value:
  19. raise ValueError('this exception is not handled')
  20. # output
  21. # Normal:
  22. # entering
  23. # inside with statement: {}
  24. # exiting
  25. #
  26. # Handled error:
  27. # entering
  28. # ERROR: showing example of handling an error
  29. # exiting
  30. #
  31. # Unhandled error:
  32. # entering
  33. # exiting
  34. # Traceback (most recent call last):
  35. # File "contextlib_contextmanager.py", line 33, in <module>
  36. # raise ValueError('this exception is not handled')
  37. # ValueError: this exception is not handled
  38. 复制代码

生成器应该初始化上下文,只产生一次,然后清理上下文。如果有的话,产生的值绑定到 as 子句中的变量。with 块内的异常在生成器内重新引发,因此可以在那里处理它们。

contextmanager() 返回的上下文管理器派生自 ContextDecorator,因此它也可以作为函数装饰器使用。

  1. @contextlib.contextmanager
  2. def make_context():
  3. print(' entering')
  4. try:
  5. # Yield control, but not a value, because any value
  6. # yielded is not available when the context manager
  7. # is used as a decorator.
  8. yield
  9. except RuntimeError as err:
  10. print(' ERROR:', err)
  11. finally:
  12. print(' exiting')
  13. @make_context()
  14. def normal():
  15. print(' inside with statement')
  16. @make_context()
  17. def throw_error(err):
  18. raise err
  19. print('Normal:')
  20. normal()
  21. print('\nHandled error:')
  22. throw_error(RuntimeError('showing example of handling an error'))
  23. print('\nUnhandled error:')
  24. throw_error(ValueError('this exception is not handled'))
  25. # output
  26. # Normal:
  27. # entering
  28. # inside with statement
  29. # exiting
  30. #
  31. # Handled error:
  32. # entering
  33. # ERROR: showing example of handling an error
  34. # exiting
  35. #
  36. # Unhandled error:
  37. # entering
  38. # exiting
  39. # Traceback (most recent call last):
  40. # File "contextlib_contextmanager_decorator.py", line 43, in
  41. # <module>
  42. # throw_error(ValueError('this exception is not handled'))
  43. # File ".../lib/python3.7/contextlib.py", line 74, in inner
  44. # return func(*args, **kwds)
  45. # File "contextlib_contextmanager_decorator.py", line 33, in
  46. # throw_error
  47. # raise err
  48. # ValueError: this exception is not handled
  49. 复制代码

如上例所示,当上下文管理器用作装饰器时,生成器产生的值在被装饰的函数内不可用,传递给装饰函数的参数仍然可用,如 throw_error() 中所示。

关闭打开句柄

file 类支持上下文管理器 API,但代表打开句柄的一些其他对象并不支持。标准库文档中给出的 contextlib 示例是 urllib.urlopen() 返回的对象。还有其他遗留类使用 close() 方法,但不支持上下文管理器 API。要确保句柄已关闭,请使用 closing() 为其创建上下文管理器。

  1. import contextlib
  2. class Door:
  3. def __init__(self):
  4. print(' __init__()')
  5. self.status = 'open'
  6. def close(self):
  7. print(' close()')
  8. self.status = 'closed'
  9. print('Normal Example:')
  10. with contextlib.closing(Door()) as door:
  11. print(' inside with statement: {}'.format(door.status))
  12. print(' outside with statement: {}'.format(door.status))
  13. print('\nError handling example:')
  14. try:
  15. with contextlib.closing(Door()) as door:
  16. print(' raising from inside with statement')
  17. raise RuntimeError('error message')
  18. except Exception as err:
  19. print(' Had an error:', err)
  20. # output
  21. # Normal Example:
  22. # __init__()
  23. # inside with statement: open
  24. # close()
  25. # outside with statement: closed
  26. #
  27. # Error handling example:
  28. # __init__()
  29. # raising from inside with statement
  30. # close()
  31. # Had an error: error message
  32. 复制代码

无论 with 块中是否有错误,句柄都会关闭。

忽略异常

忽略异常的最常见方法是使用语句块 try:except,然后在语句 except 中只有 pass

  1. import contextlib
  2. class NonFatalError(Exception):
  3. pass
  4. def non_idempotent_operation():
  5. raise NonFatalError(
  6. 'The operation failed because of existing state'
  7. )
  8. try:
  9. print('trying non-idempotent operation')
  10. non_idempotent_operation()
  11. print('succeeded!')
  12. except NonFatalError:
  13. pass
  14. print('done')
  15. # output
  16. # trying non-idempotent operation
  17. # done
  18. 复制代码

在这种情况下,操作失败并忽略错误。

try:except 可以被替换为 contextlib.suppress(),更明确地抑制类异常在 with 块的任何地方发生。

  1. import contextlib
  2. class NonFatalError(Exception):
  3. pass
  4. def non_idempotent_operation():
  5. raise NonFatalError(
  6. 'The operation failed because of existing state'
  7. )
  8. with contextlib.suppress(NonFatalError):
  9. print('trying non-idempotent operation')
  10. non_idempotent_operation()
  11. print('succeeded!')
  12. print('done')
  13. # output
  14. # trying non-idempotent operation
  15. # done
  16. 复制代码

在此更新版本中,异常将完全丢弃。

重定向输出流

设计不良的库代码可能直接写入 sys.stdoutsys.stderr,不提供参数来配置不同的输出目的地。如果源不能被改变接受新的输出参数时,可以使用 redirect_stdout()redirect_stderr() 上下文管理器捕获输出。

  1. from contextlib import redirect_stdout, redirect_stderr
  2. import io
  3. import sys
  4. def misbehaving_function(a):
  5. sys.stdout.write('(stdout) A: {!r}\n'.format(a))
  6. sys.stderr.write('(stderr) A: {!r}\n'.format(a))
  7. capture = io.StringIO()
  8. with redirect_stdout(capture), redirect_stderr(capture):
  9. misbehaving_function(5)
  10. print(capture.getvalue())
  11. # output
  12. # (stdout) A: 5
  13. # (stderr) A: 5
  14. 复制代码

在此示例中,misbehaving_function() 写入 stdoutstderr,但两个上下文管理器将该输出发送到同一 io.StringIO,保存它以便稍后使用。

注意:redirect_stdout()redirect_stderr() 通过替换 sys 模块中的对象来修改全局状态,应小心使用。这些函数不是线程安全的,并且可能会干扰期望将标准输出流附加到终端设备的其他操作。

动态上下文管理器堆栈

大多数上下文管理器一次操作一个对象,例如单个文件或数据库句柄。在这些情况下,对象是事先已知的,并且使用上下文管理器的代码可以围绕该对象构建。在其他情况下,程序可能需要在上下文中创建未知数量的对象,同时希望在控制流退出上下文时清除所有对象。ExitStack 函数就是为了处理这些更动态的情况。

ExitStack 实例维护清理回调的堆栈数据结构。回调在上下文中显式填充,并且当控制流退出上下文时,以相反的顺序调用已注册的回调。就像有多个嵌套 with 语句,只是它们是动态建立的。

堆叠上下文管理器

有几种方法可以填充 ExitStack。此示例用于 enter_context() 向堆栈添加新的上下文管理器。

  1. import contextlib
  2. @contextlib.contextmanager
  3. def make_context(i):
  4. print('{} entering'.format(i))
  5. yield {}
  6. print('{} exiting'.format(i))
  7. def variable_stack(n, msg):
  8. with contextlib.ExitStack() as stack:
  9. for i in range(n):
  10. stack.enter_context(make_context(i))
  11. print(msg)
  12. variable_stack(2, 'inside context')
  13. # output
  14. # 0 entering
  15. # 1 entering
  16. # inside context
  17. # 1 exiting
  18. # 0 exiting
  19. 复制代码

enter_context() 首先调用 __enter__() 上下文管理器,然后将 __exit__() 方法注册为在栈撤消时调用的回调。

上下文管理器 ExitStack 被视为处于一系列嵌套 with 语句中。在上下文中的任何位置发生的错误都会通过上下文管理器的正常错误处理进行传播。这些上下文管理器类说明了错误传播的方式。

  1. # contextlib_context_managers.py
  2. import contextlib
  3. class Tracker:
  4. "Base class for noisy context managers."
  5. def __init__(self, i):
  6. self.i = i
  7. def msg(self, s):
  8. print(' {}({}): {}'.format(
  9. self.__class__.__name__, self.i, s))
  10. def __enter__(self):
  11. self.msg('entering')
  12. class HandleError(Tracker):
  13. "If an exception is received, treat it as handled."
  14. def __exit__(self, *exc_details):
  15. received_exc = exc_details[1] is not None
  16. if received_exc:
  17. self.msg('handling exception {!r}'.format(
  18. exc_details[1]))
  19. self.msg('exiting {}'.format(received_exc))
  20. # Return Boolean value indicating whether the exception
  21. # was handled.
  22. return received_exc
  23. class PassError(Tracker):
  24. "If an exception is received, propagate it."
  25. def __exit__(self, *exc_details):
  26. received_exc = exc_details[1] is not None
  27. if received_exc:
  28. self.msg('passing exception {!r}'.format(
  29. exc_details[1]))
  30. self.msg('exiting')
  31. # Return False, indicating any exception was not handled.
  32. return False
  33. class ErrorOnExit(Tracker):
  34. "Cause an exception."
  35. def __exit__(self, *exc_details):
  36. self.msg('throwing error')
  37. raise RuntimeError('from {}'.format(self.i))
  38. class ErrorOnEnter(Tracker):
  39. "Cause an exception."
  40. def __enter__(self):
  41. self.msg('throwing error on enter')
  42. raise RuntimeError('from {}'.format(self.i))
  43. def __exit__(self, *exc_info):
  44. self.msg('exiting')
  45. 复制代码

这些类的示例基于 variable_stack(),它使用上下文管理器来构造 ExitStack,逐个构建整体上下文。下面的示例通过不同的上下文管理器来探索错误处理行为。首先,正常情况下没有例外。

  1. print('No errors:')
  2. variable_stack([
  3. HandleError(1),
  4. PassError(2),
  5. ])
  6. 复制代码

然后,在堆栈末尾的上下文管理器中处理异常示例,其中所有打开的上下文在堆栈展开时关闭。

  1. print('\nError at the end of the context stack:')
  2. variable_stack([
  3. HandleError(1),
  4. HandleError(2),
  5. ErrorOnExit(3),
  6. ])
  7. 复制代码

接下来,处理堆栈中间的上下文管理器中的异常示例,其中在某些上下文已经关闭之前不会发生错误,因此这些上下文不会看到错误。

  1. print('\nError in the middle of the context stack:')
  2. variable_stack([
  3. HandleError(1),
  4. PassError(2),
  5. ErrorOnExit(3),
  6. HandleError(4),
  7. ])
  8. 复制代码

最后,一个仍未处理的异常并传播到调用代码。

  1. try:
  2. print('\nError ignored:')
  3. variable_stack([
  4. PassError(1),
  5. ErrorOnExit(2),
  6. ])
  7. except RuntimeError:
  8. print('error handled outside of context')
  9. 复制代码

如果堆栈中的任何上下文管理器收到异常并返回 True,则会阻止该异常传播到其他上下文管理器。

  1. $ python3 contextlib_exitstack_enter_context_errors.py
  2. No errors:
  3. HandleError(1): entering
  4. PassError(2): entering
  5. PassError(2): exiting
  6. HandleError(1): exiting False
  7. outside of stack, any errors were handled
  8. Error at the end of the context stack:
  9. HandleError(1): entering
  10. HandleError(2): entering
  11. ErrorOnExit(3): entering
  12. ErrorOnExit(3): throwing error
  13. HandleError(2): handling exception RuntimeError('from 3')
  14. HandleError(2): exiting True
  15. HandleError(1): exiting False
  16. outside of stack, any errors were handled
  17. Error in the middle of the context stack:
  18. HandleError(1): entering
  19. PassError(2): entering
  20. ErrorOnExit(3): entering
  21. HandleError(4): entering
  22. HandleError(4): exiting False
  23. ErrorOnExit(3): throwing error
  24. PassError(2): passing exception RuntimeError('from 3')
  25. PassError(2): exiting
  26. HandleError(1): handling exception RuntimeError('from 3')
  27. HandleError(1): exiting True
  28. outside of stack, any errors were handled
  29. Error ignored:
  30. PassError(1): entering
  31. ErrorOnExit(2): entering
  32. ErrorOnExit(2): throwing error
  33. PassError(1): passing exception RuntimeError('from 2')
  34. PassError(1): exiting
  35. error handled outside of context
  36. 复制代码

任意上下文回调

ExitStack 还支持关闭上下文的任意回调,从而可以轻松清理不通过上下文管理器控制的资源。

  1. import contextlib
  2. def callback(*args, **kwds):
  3. print('closing callback({}, {})'.format(args, kwds))
  4. with contextlib.ExitStack() as stack:
  5. stack.callback(callback, 'arg1', 'arg2')
  6. stack.callback(callback, arg3='val3')
  7. # output
  8. # closing callback((), {'arg3': 'val3'})
  9. # closing callback(('arg1', 'arg2'), {})
  10. 复制代码

__exit__() 完整上下文管理器的方法一样,回调的调用顺序与它们的注册顺序相反。

无论是否发生错误,都会调用回调,并且不会给出有关是否发生错误的任何信息。它们的返回值被忽略。

  1. import contextlib
  2. def callback(*args, **kwds):
  3. print('closing callback({}, {})'.format(args, kwds))
  4. try:
  5. with contextlib.ExitStack() as stack:
  6. stack.callback(callback, 'arg1', 'arg2')
  7. stack.callback(callback, arg3='val3')
  8. raise RuntimeError('thrown error')
  9. except RuntimeError as err:
  10. print('ERROR: {}'.format(err))
  11. # output
  12. # closing callback((), {'arg3': 'val3'})
  13. # closing callback(('arg1', 'arg2'), {})
  14. # ERROR: thrown error
  15. 复制代码

因为它们无法访问错误,所以回调无法通过其余的上下文管理器堆栈阻止异常传播。

回调可以方便清楚地定义清理逻辑,而无需创建新的上下文管理器类。为了提高代码可读性,该逻辑可以封装在内联函数中,callback() 可以用作装饰器。

  1. import contextlib
  2. with contextlib.ExitStack() as stack:
  3. @stack.callback
  4. def inline_cleanup():
  5. print('inline_cleanup()')
  6. print('local_resource = {!r}'.format(local_resource))
  7. local_resource = 'resource created in context'
  8. print('within the context')
  9. # output
  10. # within the context
  11. # inline_cleanup()
  12. # local_resource = 'resource created in context'
  13. 复制代码

无法为使用装饰器形式注册的 callback() 函数指定参数。但是,如果清理回调是内联定义的,则范围规则允许它访问调用代码中定义的变量。

部分堆栈

有时,在构建复杂的上下文时,如果上下文无法完全构建,可以中止操作,但是如果延迟清除所有资源,则能够正确设置所有资源。例如,如果操作需要多个长期网络连接,则最好不要在一个连接失败时启动操作。但是,如果可以打开所有连接,则需要保持打开的时间长于单个上下文管理器的持续时间。可以在此方案中使用 ExitStackpop_all() 方法。

pop_all() 从调用它的堆栈中清除所有上下文管理器和回调,并返回一个预先填充了相同上下文管理器和回调的新堆栈。 在原始堆栈消失之后,可以稍后调用新堆栈的 close() 方法来清理资源。

  1. import contextlib
  2. from contextlib_context_managers import *
  3. def variable_stack(contexts):
  4. with contextlib.ExitStack() as stack:
  5. for c in contexts:
  6. stack.enter_context(c)
  7. # Return the close() method of a new stack as a clean-up
  8. # function.
  9. return stack.pop_all().close
  10. # Explicitly return None, indicating that the ExitStack could
  11. # not be initialized cleanly but that cleanup has already
  12. # occurred.
  13. return None
  14. print('No errors:')
  15. cleaner = variable_stack([
  16. HandleError(1),
  17. HandleError(2),
  18. ])
  19. cleaner()
  20. print('\nHandled error building context manager stack:')
  21. try:
  22. cleaner = variable_stack([
  23. HandleError(1),
  24. ErrorOnEnter(2),
  25. ])
  26. except RuntimeError as err:
  27. print('caught error {}'.format(err))
  28. else:
  29. if cleaner is not None:
  30. cleaner()
  31. else:
  32. print('no cleaner returned')
  33. print('\nUnhandled error building context manager stack:')
  34. try:
  35. cleaner = variable_stack([
  36. PassError(1),
  37. ErrorOnEnter(2),
  38. ])
  39. except RuntimeError as err:
  40. print('caught error {}'.format(err))
  41. else:
  42. if cleaner is not None:
  43. cleaner()
  44. else:
  45. print('no cleaner returned')
  46. # output
  47. # No errors:
  48. # HandleError(1): entering
  49. # HandleError(2): entering
  50. # HandleError(2): exiting False
  51. # HandleError(1): exiting False
  52. #
  53. # Handled error building context manager stack:
  54. # HandleError(1): entering
  55. # ErrorOnEnter(2): throwing error on enter
  56. # HandleError(1): handling exception RuntimeError('from 2')
  57. # HandleError(1): exiting True
  58. # no cleaner returned
  59. #
  60. # Unhandled error building context manager stack:
  61. # PassError(1): entering
  62. # ErrorOnEnter(2): throwing error on enter
  63. # PassError(1): passing exception RuntimeError('from 2')
  64. # PassError(1): exiting
  65. # caught error from 2
  66. 复制代码

此示例使用前面定义的相同上下文管理器类,其差异是 ErrorOnEnter 产生的错误是 __enter__() 而不是 __exit__()。在 variable_stack() 内,如果输入的所有上下文都没有错误,则返回一个 ExitStackclose() 方法。如果发生处理错误,则 variable_stack() 返回 None 来表示已完成清理工作。如果发生未处理的错误,则清除部分堆栈并传播错误。

相关文档:

https://pymotw.com/3/contextlib/index.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/130304
推荐阅读
相关标签
  

闽ICP备14008679号