You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,
    }

问题

  1. 在CPython中,session = next(get_session_db())这种场景下,finally块是否一定会立即执行?
  2. 如果调用者不保留生成器对象的引用,当前get_session_db的实现是否本质上不安全?
  3. 在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上下文管理器)有几个关键区别:

  1. 生命周期严格可控:上下文管理器会在with块结束时(无论正常退出还是异常)确保finally块执行,不会因为生成器被提前回收导致会话过早关闭。
  2. 符合官方设计预期:SQLAlchemy的Session本身实现了上下文管理器接口,with SessionLocal() as session:是官方推荐的资源管理方式,能保证会话正确提交/回滚、关闭。
  3. 异常安全:如果with块内发生异常,上下文管理器会自动处理会话的回滚(事务场景下),而手动调用next()的方式无法保证这一点,异常会直接传播,可能导致会话资源泄漏或数据不一致。
  4. 跨解释器兼容:上下文管理器的行为是Python标准定义的,不依赖CPython的引用计数特性,在任何符合标准的Python解释器下都能稳定工作。

内容的提问来源于stack exchange,提问作者Kafka4PresidentNow

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 16:34:53