为什么模块不能成为上下文管理器(对 'with' 语句)?

Why can't a module be a context manager (to a 'with' statement)?

假设我们有以下 mod.py:

def __enter__():
    print("__enter__<")

def __exit__(*exc):
    print("__exit__< {0}".format(exc))

class cls:
    def __enter__(self):
        print("cls.__enter__<")

    def __exit__(self, *exc):
        print("cls.__exit__< {0}".format(exc))

及其以下用法:

import mod

with mod:
    pass

我得到一个错误:

Traceback (most recent call last):
  File "./test.py", line 3, in <module>
    with mod:
AttributeError: __exit__

根据文档文档,with 语句应按如下方式执行(我相信它在第 2 步失败,因此截断了列表):

  1. The context expression (the expression given in the with_item) is evaluated to obtain a context manager.
  2. The context manager’s __exit__() is loaded for later use.
  3. The context manager’s __enter__() method is invoked.
  4. etc...

据我了解,没有理由找不到 __exit__。有没有我遗漏的东西导致模块无法作为上下文管理器工作?

__exit__ 是一种 特殊方法 ,因此 Python 在 类型 上查找它。 module 类型没有这样的方法,这就是失败的原因。

请参阅 Python 数据模型文档的 Special method lookup section

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.

请注意,这适用于所有特殊方法。例如,如果您向模块添加了 __str____repr__ 函数,则在打印模块时也不会调用它。

Python 这样做是为了确保类型对象也是可哈希和可表示的;如果 Python 没有 这样做,那么当为 class 对象定义了 __hash__ 方法时,尝试将 class 对象放入字典会失败 class(因为该方法期望为 self 传入一个实例)。

由于@Martijn Pieters 中所述的原因,您无法轻松做到这一点。然而,通过一些额外的工作,它 可能的,因为 sys.modules 中的值不必是内置模块 class 的实例,它们可以使用上下文管理器所需的特殊方法成为您自己的自定义 class 的实例。

这里是将其应用到您想做的事情上。鉴于以下 mod.py

import sys

class MyModule(object):
    def __enter__(self):
        print("__enter__<")

    def __exit__(self, *exc):
        print("__exit__> {0}".format(exc))

# replace entry in sys.modules for this module with an instance of MyModule
_ref = sys.modules[__name__]
sys.modules[__name__] = MyModule()

以及下面的用法:

import mod

with mod:
    print('running within context')

将产生此输出:

__enter__<
running within context
__exit__> (None, None, None)

有关为什么需要 _ref 的信息,请参阅 this 问题。

一个比 Martineau 提出的版本更柔和的版本,少了一点争论:

import sys

class CustomModule(sys.modules[__name__].__class__):
  """
  Custom module
  """
  def __enter__(self):
    print('enter')

  def __exit__(self, *args, **kwargs):
    print('exit')


sys.modules[__name__].__class__ = CustomModule

不要替换模块(这可能会导致无数问题),只需将 class 替换为继承原始 class 的模块即可。这样,保留了原始模块对象,不需要另一个 ref(防止垃圾收集),并且它可以与任何自定义导入器一起使用。请注意一个重要的事实,即模块对象被创建并添加到 sys.modules BEFORE 模块代码被执行。

注意,使用这种方式,可以添加任何魔法方法