问题描述
最近在复现一个开源项目时,发现执行过该项目中的代码单元格后,其余单元格的print函数输出也会续在该单元格后。而正常情况下print函数输出应该位于其所属的单元格。下图中,我将出现问题的单元格执行后清空了输出,但是在其他单元格执行 print 函数后,输出还是会打印到该单元格后。而在执行该单元格前 print 功能表现正常。重启电脑无效。
原因
一开始是以为是bug,但是重启电脑都没有看到问题解决,推测多半是正常的原因。而出现问题的单元格是引用开源代码,内部执行逻辑较为复杂,从函数内部找原因耗时较久。
于是突然想到是不是 print 的输出流定位错误了,于是搜了一下print的默认输出流。
In Python, the default output stream for the
print()
function issys.stdout
.
于是就打印一下 stdout。
import sys
print(sys.stdout)
输出结果如下:
`<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> `
看上去好像没啥问题?同样的代码新建了一个文件测试,得到的输出如下:
`<ipykernel.iostream.OutStream object at 0x7fd1ed6f5720>`
果然是有问题的。
我尝试了直接获取原来的默认的 ipykernel 的 OutStream,翻了官方的源码[1],没有找到直接获取 OutStream 的方法。花了一小段时间折腾出了下面的代码,自己新建一个 stream 。能用但是用warning,不知道有没有隐藏风险,再次记录一下:
import sys
from IPython.core.getipython import get_ipython
from ipykernel.iostream import OutStream
kernel = get_ipython().kernel
default_out = OutStream(kernel.session, kernel.iopub_socket, name='stdout')
sys.stdout = default_out
会提示 warning:
/tmp/ipykernel_3746745/352382136.py:2: DeprecationWarning: Since IPykernel 4.3, OutStream should be created with IOPubThread, not <ipykernel.iostream.BackgroundSocket object at 0x7fd1ed6f4790> default_out = OutStream(kernel.session, kernel.iopub_socket, name='stdout')
测试了一下不太行。所以就只有朴素的两种解决方案列在下面了。
解决方案
1. 避免 sys.stdout 的操作
检查出现问题的单元格所调用的代码,你应该最终能找到对 sys.stdout 做操作的代码。以我的情况为例,问题的原因是下面的代码。
大概是因为代码的原作者中间调用了一部分输出很多的代码,懒得处理就直接把stdout指向虚空,跑完了再改回来,但是 sys.__stdout__ 并不是 Jupyter 默认的 out stream,所以就回不来了。
第一种思路就是把这里做一些处理(注释或删除),取消掉对 sys.stdout 的操作。
2. 处理 sys.stdout 前备份,操作后还原
另一种思路则是备份默认的 Jupyter OutStream,在执行代码块前将 sys.stdout 保存到一个临时变量中,然后执行结束后再将临时变量的值赋回给 stdout。
original_stdout = sys.stdout
# 原有的造成问题的代码
# xxx
sys.stdout = original_stdout
备注
这个问题非常少见,谷歌也没有找到类似的情况,遇到这种情况也是非常难了。但是解决方案其实很简单,做个备份事后回复就好。