Python subprocess.Popen仅重定向STDERR保留STDOUT到新窗口问题
环境搭建
我写了一个简易的Runner程序,该程序会向sys.stderr输出日志、未捕获异常等信息,向sys.stdout输出程序有效信息、用户交互类内容等,代码如下:
import sys import time for i in range(1, 4): sys.stdout.write(f"This is text #{i} to STDOUT\n") sys.stderr.write(f"This is text #{i} to STDERR\n") time.sleep(5)
另有一个Main主程序,通过subprocess.Popen在新窗口中启动Runner进程并打印其输出,代码如下:
import subprocess cmd = "python runner.py" proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, # Problem line stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NEW_CONSOLE ) proc.wait() out, err = proc.communicate() if out: print(f"[{out.decode('utf-8')}]") if err: print(f"[{err.decode('utf-8')}]")
当前程序运行后的输出结果为:
[This is text #1 to STDOUT This is text #2 to STDOUT This is text #3 to STDOUT ] [This is text #1 to STDERR This is text #2 to STDERR This is text #3 to STDERR ]
为何选择Popen?
我需要并行运行多个Runner进程并在后续等待其执行结束,认为subprocess.check_input或subprocess.run无法实现该需求(如有错误欢迎指正)。
为何需要新窗口?
我希望每个Runner进程的输出都能在独立的专属窗口中展示,方便区分查看。
需求目标
我希望实现仅重定向stderr,将stdout保留在子进程打开的独立窗口中输出,最终Main主程序仅打印子进程的错误类输出,预期效果如下:
[This is text #1 to STDERR This is text #2 to STDERR This is text #3 to STDERR ]
该实现可以大幅提升Runner新功能的调试效率。
已尝试方案
当为subprocess.Popen设置stderr=subprocess.PIPE参数、stdout=None(默认值)时,stdout出现阻塞问题:
- stdout内容不会显示在
Runner的独立窗口中 proc.communicate返回的out值为None
最终stdout的打印内容直接丢失。我还尝试将sys.stdout传入stdout=参数(希望将stdout输出到当前主程序控制台而非子进程窗口),但触发了Bad file descriptor错误,错误信息如下:
[Traceback (most recent call last): File "C:\Users\kirin\source\repos\python_tests\runner.py", line 5, in <module> sys.stdout.write(f"This is text #{i} to STDOUT\n") OSError: [Errno 9] Bad file descriptor Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='cp1251'> OSError: [Errno 9] Bad file descriptor ]
(备注:该场景下stderr内容可成功从Runner重定向到Main程序)
解决方案
先纠正代码里的核心问题:
- 先调用
proc.wait()再调用proc.communicate()是错误写法,一旦子进程输出的stderr内容占满管道缓冲区,子进程会阻塞在写操作上,wait()会永久卡死造成死锁。communicate()本身就会等待进程执行结束,不需要额外调用wait()。 - Windows平台下只要给
Popen传入任意重定向参数(比如设置的stderr=subprocess.PIPE),subprocess会默认将close_fds设为True,导致子进程无法继承新创建控制台的标准输出句柄,这就是设置stdout=None时新窗口看不到stdout内容的根本原因。至于传sys.stdout报句柄错误,是因为主进程的stdout绑定在主程序自己的控制台,跨控制台的子进程没有访问这个句柄的权限。 - 选择Popen的思路是对的,
subprocess.run是阻塞接口,要并行启动多个Runner再统一等待结果,Popen是正确选择。
修改后的Main程序代码如下,可直接运行达到预期效果:
import subprocess import sys # 用sys.executable调用和主程序同版本的Python解释器,参数列表形式避免路径空格问题 cmd = [sys.executable, "runner.py"] proc = subprocess.Popen( cmd, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NEW_CONSOLE, close_fds=False, # 关键参数:允许子进程继承新控制台的标准输入输出句柄 stdin=None, stdout=None ) # 直接读取stderr管道内容,communicate会自动等待进程结束 _, err = proc.communicate() if err: print(f"[{err.decode('utf-8')}]")
运行后会弹出独立的新控制台窗口,Runner的所有stdout内容都会正常打印在这个专属窗口中,主程序控制台只会接收并打印Runner输出到stderr的内容,完全符合需求。如果需要并行跑多个Runner,循环启动存下每个proc对象,最后逐个调用communicate即可。
内容的提问来源于stack exchange,提问作者Moshe Groot

