CPython中未保留生成器引用时调用next(),finally为何立即执行?
生成器模式实现数据库会话的资源清理问题
我在CPython环境里用生成器模式实现数据库会话提供者,原本计划通过finally块确保会话关闭完成资源清理,但发现两种不同的生成器使用行为:用上下文管理器时运行正常,但手动调用next()推进生成器且不保留生成器引用时,finally块(也就是session.close())会在赋值后立即执行,导致会话还没使用就已经关闭。
最小复现示例
import contextlib class SessionMaker(): def __init__(self): print('Session initiated') self.id = id(self) def close(self): print(f'Close called for Session {self.id}') def __del__(self): print(f'Session {self.id} finalized (GC)') # 生成器函数 def get_session_db(): session = SessionMaker() try: yield session finally: session.close()
情况1:使用contextlib时运行正常
用@contextlib.contextmanager装饰器配合with语句时,close方法会在代码块结束时调用。
@contextlib.contextmanager def get_session_db_ctx(): session = SessionMaker() try: yield session finally: session.close() with get_session_db_ctx() as session: print(f'Working with {session.id}') print('End of work')
输出:
Session initiated Working with 129669442755408 End of work Close called for Session 129669442755408 Session 129669442755408 finalized (GC)
情况2:手动调用next()出现问题
如果用未装饰的原生生成器直接调用next(),会话会立即关闭。
# 测试用的无装饰器生成器函数 def get_session_db(): session = SessionMaker() try: yield session finally: session.close() # 直接在生成器创建时调用next() session = next(get_session_db()) print(f'Working with {session.id}') print('End of work')
输出:
Session initiated Close called for Session 137729226680096 <-- 为什么这里执行? Working with 137729226680096 <-- 会话已经关闭了 End of work Session 137729226680096 finalized (GC)
实际业务场景
我在生产代码里尝试封装SQLAlchemy会话创建逻辑:
# 简化后的生产代码 def get_session_db(package_name: str) -> Generator[Session, None, None]: session = SessionLocal() try: yield session finally: session.close() # 其他文件中的用法,每个文件有这样的会话赋值: session = next(get_session_db("my_pkg")) # 这里执行查询,但会话可能已经关闭? def get_data(event_id: int) -> dict: event = session.execute( text('SELECT data FROM xyz WHERE id = :id'), params={'id': event_id} ).one() return event[0] def get_foo(mon_id: int) -> dict[str, bool]: smy = ( session.query(Mon.has_attachment) .filter(Mon.mon_id == mon_id) .first() ) return { "has_attachment": smy.has_attachment, }
问题
- 在CPython中,
session = next(get_session_db())这种场景下,finally块是否一定会立即执行? - 如果调用者不保留生成器对象的引用,当前
get_session_db的实现是否本质上不安全? - 在SQLAlchemy环境中,使用上下文管理器防止过早垃圾回收的实际区别是什么?
回答
问题1:finally块是否一定会立即执行?
是的,在CPython中这种场景下finally块几乎一定会立即执行。原因是:
- 调用
next(get_session_db())时,生成器对象是临时创建的,没有被任何变量引用。 - CPython采用引用计数垃圾回收机制,临时生成器对象在
next()调用结束后引用计数变为0,会被立即回收。 - 生成器被回收时,Python会自动调用生成器的
close()方法,触发未执行的finally块执行。
问题2:不保留生成器引用的实现是否本质不安全?
完全不安全。
- 如你观察到的,会话会在赋值后立刻被关闭,后续对会话的操作都会在已关闭的会话上执行,会导致SQLAlchemy抛出
InvalidRequestError类的异常。 - 即使极端情况下垃圾回收没有立即触发,这种实现也依赖CPython的具体行为,移植到PyPy等其他Python解释器时问题会更明显,属于不可靠的资源管理方式。
问题3:SQLAlchemy环境中使用上下文管理器的实际区别?
在SQLAlchemy场景下,使用上下文管理器(无论是contextlib.contextmanager装饰的生成器,还是SQLAlchemy自带的Session上下文管理器)有几个关键区别:
- 生命周期严格可控:上下文管理器会在
with块结束时(无论正常退出还是异常)确保finally块执行,不会因为生成器被提前回收导致会话过早关闭。 - 符合官方设计预期:SQLAlchemy的
Session本身实现了上下文管理器接口,with SessionLocal() as session:是官方推荐的资源管理方式,能保证会话正确提交/回滚、关闭。 - 异常安全:如果
with块内发生异常,上下文管理器会自动处理会话的回滚(事务场景下),而手动调用next()的方式无法保证这一点,异常会直接传播,可能导致会话资源泄漏或数据不一致。 - 跨解释器兼容:上下文管理器的行为是Python标准定义的,不依赖CPython的引用计数特性,在任何符合标准的Python解释器下都能稳定工作。
内容的提问来源于stack exchange,提问作者Kafka4PresidentNow
相关产品推荐
相关产品推荐

