如何让Python生成器函数调用时立即抛出异常而非延迟触发
问题描述
我有一个用于包装包含多个变量和属性的数据文件的Python类,部分变量自身也带有属性,我希望通过生成器遍历这些属性。当指定变量不存在于文件中时,我希望立即抛出Exception;若变量存在则生成对应的属性。但由于Python生成器采用惰性求值,异常仅在首次迭代生成器时才会抛出,而非调用生成器函数时。示例代码如下:
from typing import List, Dict, Any class file: def __init__(self, varlist: List[str], attrlist: Dict[str, Any]): self.varlist = varlist self.attrlist = attrlist def loadvar(self, varname: str): ... def attrs(self, varname: str = None): if varname: if varname not in self.varlist: raise RuntimeError(f"{varname} is not a valid variable name") yield from self.loadvar(varname).attrs() yield from self.attrlist.keys() datafile = file(['a', 'b','c'], {'d':1, 'e':2, 'f':3}) list(datafile.attrs('non-existent')) # <-- 抛出异常;符合预期 attr = datafile.attrs('non-existent') # <-- 不抛出异常;不符合预期 list(attr) # <-- 此时才抛出异常
核心问题:是否可以实现让异常在生成器函数调用时抛出,而非迭代时?
解决方案
可以实现,核心思路是将参数校验逻辑提前到生成器对象创建前执行——因为生成器函数调用时仅返回对象,不会执行内部代码,直到首次迭代。以下是两种可行方案:
方案一:拆分校验与生成器逻辑
把参数检查放在单独的前置逻辑中,校验通过后再返回生成器,这样调用attrs()时会立即执行校验:
from typing import List, Dict, Any class File: # 遵循PEP8规范,类名采用大驼峰命名 def __init__(self, varlist: List[str], attrlist: Dict[str, Any]): self.varlist = varlist self.attrlist = attrlist def loadvar(self, varname: str): # 模拟返回带attrs方法的变量对象 class MockVariable: def attrs(self): yield from ['var_attr1', 'var_attr2'] return MockVariable() def _attrs_generator(self, varname: str = None): # 纯生成器逻辑,不包含参数校验 if varname: yield from self.loadvar(varname).attrs() yield from self.attrlist.keys() def attrs(self, varname: str = None): # 调用时立即执行参数校验 if varname and varname not in self.varlist: raise RuntimeError(f"{varname} is not a valid variable name") # 校验通过后返回生成器 return self._attrs_generator(varname) # 测试验证 datafile = File(['a', 'b','c'], {'d':1, 'e':2, 'f':3}) attr = datafile.attrs('non-existent') # 调用时直接抛出异常,符合预期
方案二:用装饰器触发前置校验
如果不想拆分函数,可以通过装饰器在生成器创建后立即触发一次迭代,执行校验逻辑:
from typing import List, Dict, Any from functools import wraps def validate_on_call(func): @wraps(func) def wrapper(*args, **kwargs): gen = func(*args, **kwargs) try: # 触发一次迭代,执行到第一个yield,此时校验逻辑已完成 first_item = next(gen) # 重新组合生成器,确保所有元素都能被遍历 yield first_item yield from gen except StopIteration: # 处理生成器无元素的情况 return return wrapper class File: def __init__(self, varlist: List[str], attrlist: Dict[str, Any]): self.varlist = varlist self.attrlist = attrlist def loadvar(self, varname: str): class MockVariable: def attrs(self): yield from ['var_attr1', 'var_attr2'] return MockVariable() @validate_on_call def attrs(self, varname: str = None): if varname: if varname not in self.varlist: raise RuntimeError(f"{varname} is not a valid variable name") yield from self.loadvar(varname).attrs() yield from self.attrlist.keys() # 测试验证 datafile = File(['a', 'b','c'], {'d':1, 'e':2, 'f':3}) attr = datafile.attrs('non-existent') # 调用时立即抛出异常,符合预期
原理说明
Python生成器的惰性求值特性决定了:调用生成器函数时仅返回生成器对象,不会执行函数内部代码,直到第一次调用next()或开始迭代。因此要让异常在调用时抛出,必须在生成器对象可用前完成参数校验,或者创建对象后立即触发一次迭代来执行校验逻辑。
方案一的可读性和维护性更优,推荐使用这种拆分逻辑的方式,明确区分参数校验与迭代生成的职责。
内容的提问来源于stack exchange,提问作者MrAzzaman
相关产品推荐
相关产品推荐

