为什么模块不能成为上下文管理器(对 '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 步失败,因此截断了列表):
- The context expression (the expression given in the with_item) is evaluated to obtain a context manager.
- The context manager’s
__exit__()
is loaded for later use.
- The context manager’s
__enter__()
method is invoked.
- 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 模块代码被执行。
注意,使用这种方式,可以添加任何魔法方法
假设我们有以下 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 步失败,因此截断了列表):
- The context expression (the expression given in the with_item) is evaluated to obtain a context manager.
- The context manager’s
__exit__()
is loaded for later use.- The context manager’s
__enter__()
method is invoked.- 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 模块代码被执行。
注意,使用这种方式,可以添加任何魔法方法