我有一个 Python 类,它在内部发出警告
__init__()
。它还提供了用于打开和读取文件的工厂类方法:
from warnings import warn
class MyWarning(Warning):
"""Warning issued when an invalid name is found."""
pass
class MyClass:
def __init__(self, names):
# Simplified; actual code is longer
if is_invalid(names):
names = fix_names(names)
warn(f'{names!r} contains invalid element(s)',
MyWarning, stacklevel=2)
self._names = names
@classmethod
def from_file(cls, filename):
with open(filename) as file:
names = extract_names(file)
return cls(names)
stacklevel=2
使警告引用对 MyClass()
的调用,而不是 warn()
语句本身。当用户代码直接实例化 MyClass 时,这会起作用。但是,当 MyClass.from_file()
发出警告时,MyWarning
指的是 return cls(names)
,而不是调用 from_file()
的用户代码。
如何确保工厂方法也发出指向调用者的警告?我考虑过的一些选择:
_stacklevel
添加一个“隐藏”的__init__()
参数,并在_stacklevel=2
内部用from_file()
实例化MyClass。
_stacklevel
类属性,并在__init__()
内访问它。然后在from_file()
中临时修改这个属性
_set_names()
方法来检查/修复名称并在需要时发出警告。然后在构造函数中调用这个方法。对于from_file()
,首先用空参数实例化MyClass,然后直接调用_set_names()
以确保MyWarning指向调用者。
_set_names()
时有效地调用 from_file()
两次。warning
模块文档,但它对安全捕获和重新抛出警告几乎没有提供帮助。使用 warnings.simplefilter()
将警告转换为异常会中断 MyClass()
并迫使我再次调用它。
您可以使用类似于捕获异常的方式捕获警告
warnings.catch_warnings()
:
import warnings
class MyWarning(Warning):
"""Warning issued when an invalid name is found."""
pass
class MyClass:
def __init__(self, names):
# Simplified; actual code is longer
if is_invalid(names):
names = fix_names(names)
warn(f'{names!r} contains invalid element(s)',
MyWarning, stacklevel=2)
self._names = names
@classmethod
def from_file(cls, filename):
with open(filename) as file:
names = extract_names(file)
with warnings.catch_warnings(record=True) as cx_manager:
inst = cls(names)
#re-report warnings with the stack-level we want
for warning in cx_manager:
warnings.warn(warning.message, warning.category, stacklevel=2)
return inst
请记住
warnings.catch_warnings()
文档中的以下注释:
注意 catch_warnings 管理器通过替换然后恢复模块的
函数和过滤器规范的内部列表来工作。这意味着上下文管理器正在修改全局状态,因此不是线程安全的。showwarning()
大卫是对的,
warnings.catch_warnings(record=True)
可能就是你想要的。虽然我会把它写成函数装饰器:
def reissue_warnings(func):
def inner(*args, **kwargs):
with warnings.catch_warnings(record = True) as warning_list:
result = func(*args, **kwargs)
for warning in warning_list:
warnings.warn(warning.message, warning.category, stacklevel = 2)
return result
return inner
然后在你的例子中:
class MyClass:
def __init__(self, names):
# ...
@classmethod
@reissue_warnings
def from_file(cls, filename):
with open(filename) as file:
names = extract_names(file)
return cls(names)
inst = MyClass(['some', 'names']) # 58: MyWarning: ['some', 'names'] contains invalid element(s)
inst = MyClass.from_file('example') # 59: MyWarning: ['example'] contains invalid element(s)
这种方式还允许您跨多个功能干净地收集和重新发出警告:
class Test:
def a(self):
warnings.warn("This is a warning issued from a()")
@reissue_warnings
def b(self):
self.a()
@reissue_warnings
def c(self):
warnings.warn("This is a warning issued from c()")
self.b()
@reissue_warnings
def d(self):
self.c()
test = Test()
test.d() # Line 59
# 59: UserWarning: This is a warning issued from c()
# 59: UserWarning: This is a warning issued from a()
这对我有用:
def get_warning_stack_level(module_filenames: Union[str, List[str]]) -> int:
"""Calculate the stack level for warnings.
The stack level is the number of stack frames between the caller of the
function and the user code past the sequence of modules given in
module_filenames.
This is used to provide the appropriate stack level to warnings.warn or
warnings.warn_explicit so that the warning appears in user code rather
than in the library itself.
The calculation is based on the system call stack.
Args:
module_filenames: The filenames of the top-most modules after which the
user code starts. If a single filename is provided, it is wrapped
in a list. The modules should be given in the bottom-up order.
Returns:
The stack level to be used in warnings.warn or warnings.warn_explicit.
"""
if isinstance(module_filenames, str):
module_filenames = [module_filenames]
modules = list(module_filenames)
target = modules.pop(0)
stack = sys._getframe().f_back
stack_level = 1
found = False
while stack is not None:
filename = stack.f_code.co_filename
matches = filename.endswith((target, f"{target}.py"))
if not found and matches: # the sequence has started
found = True
elif found and not matches:
if not modules: # the sequence has ended
break
target = modules.pop(0)
matches = filename.endswith((target, f"{target}.py"))
if not matches:
raise ValueError("The module sequence has been found, but broken.")
stack = stack.f_back
stack_level += 1
if not found:
raise ValueError("The module sequence has not started in the stack.")
if not stack:
raise ValueError(
"The module sequence has been found, "
"but we have ran out of stack "
"before finding the top-most module."
)
return stack_level