如何阻止C共享库在Python中打印到stdout?
我在使用一个Python库,这个库会引入一个C语言的共享库,这个共享库会在标准输出(stdout)上打印信息。我希望输出能够干净整洁,这样我就可以用管道或者重定向到文件。打印的内容是在Python之外的共享库里完成的。
一开始,我的做法是:
# file: test.py
import os
from ctypes import *
from tempfile import mktemp
libc = CDLL("libc.so.6")
print # That's here on purpose, otherwise hello word is always printed
tempfile = open(mktemp(),'w')
savestdout = os.dup(1)
os.close(1)
if os.dup(tempfile.fileno()) != 1:
assert False, "couldn't redirect stdout - dup() error"
# let's pretend this is a call to my library
libc.printf("hello world\n")
os.close(1)
os.dup(savestdout)
os.close(savestdout)
这个初步的做法有点效果:
- 不知道为什么,在移动标准输出之前需要有一个“print”语句,否则“hello”这个词总是会被打印出来。结果就是它会打印一个空行,而不是共享库通常输出的所有内容。
- 更麻烦的是,当我尝试把输出重定向到文件时,它就失败了:
$python test.py > foo && cat foo
hello world
我第二次尝试用Python的做法是受到评论中另一个类似讨论的启发:
import os
import sys
from ctypes import *
libc = CDLL("libc.so.6")
devnull = open('/dev/null', 'w')
oldstdout = os.dup(sys.stdout.fileno())
os.dup2(devnull.fileno(), 1)
# We still pretend this is a call to my library
libc.printf("hello\n")
os.dup2(oldstdout, 1)
这个方法也没能阻止“hello”被打印出来。
因为我觉得这个方法有点底层,所以我决定完全使用ctypes。我参考了一个不打印任何内容的C程序:
#include <stdio.h>
int main(int argc, const char *argv[]) {
char buf[20];
int saved_stdout = dup(1);
freopen("/dev/null", "w", stdout);
printf("hello\n"); // not printed
sprintf(buf, "/dev/fd/%d", saved_stdout);
freopen(buf, "w", stdout);
return 0;
}
我构建了以下示例:
from ctypes import *
libc = CDLL("libc.so.6")
saved_stdout = libc.dup(1)
stdout = libc.fdopen(1, "w")
libc.freopen("/dev/null", "w", stdout);
libc.printf("hello\n")
libc.freopen("/dev/fd/" + str(saved_stdout), "w", stdout)
这个例子会打印“hello”,即使我在printf之后调用了libc.fflush(stdout)。我开始怀疑在Python中实现我想要的功能可能不太可能。或者说,我获取标准输出的文件指针的方式可能不对。
你觉得怎么样?
8 个回答
是的,你确实应该使用 os.dup2
,而不是 os.dup
,就像你第二个想法那样。你的代码看起来有点绕。除了 /dev/null
,不要随便搞 /dev
里的东西,这样做没必要。而且在这里用C语言写东西也是多余的。
诀窍是先用 dup
保存 stdout
的文件描述符,然后把它传给 fdopen
,这样就能创建一个新的 sys.stdout
Python 对象。同时,打开一个指向 /dev/null
的文件描述符,并用 dup2
来覆盖现有的 stdout
文件描述符。然后关闭指向 /dev/null
的旧文件描述符。调用 dup2
是必要的,因为我们无法告诉 open
我们想要哪个文件描述符,实际上,dup2
是唯一能做到这一点的方法。
编辑:如果你是把输出重定向到一个文件,那么 stdout
就不是行缓冲的,所以你需要刷新它。你可以在Python中做到这一点,它会和C语言正确配合。当然,如果你在写任何东西到 stdout
之前就调用这个函数,那就没关系了。
这里有一个我刚测试过的例子,在我的系统上可以正常工作。
import zook
import os
import sys
def redirect_stdout():
print "Redirecting stdout"
sys.stdout.flush() # <--- important when redirecting to files
newstdout = os.dup(1)
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, 1)
os.close(devnull)
sys.stdout = os.fdopen(newstdout, 'w')
zook.myfunc()
redirect_stdout()
zook.myfunc()
print "But python can still print to stdout..."
“zook”模块是一个非常简单的C语言库。
#include <Python.h>
#include <stdio.h>
static PyObject *
myfunc(PyObject *self, PyObject *args)
{
puts("myfunc called");
Py_INCREF(Py_None);
return Py_None;
}
static PyMethodDef zookMethods[] = {
{"myfunc", myfunc, METH_VARARGS, "Print a string."},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC
initzook(void)
{
(void)Py_InitModule("zook", zookMethods);
}
输出呢?
$ python2.5 test.py
myfunc called
Redirecting stdout
But python can still print to stdout...
还有重定向到文件呢?
$ python2.5 test.py > test.txt
$ cat test.txt
myfunc called
Redirecting stdout
But python can still print to stdout...
结合了两个答案 - https://stackoverflow.com/a/5103455/1820106 和 https://stackoverflow.com/a/4178672/1820106,这是一个上下文管理器,它只在特定范围内阻止打印到标准输出(第一个答案中的代码阻止了所有外部输出,而第二个答案在最后缺少了 sys.stdout.flush()):
class HideOutput(object):
'''
A context manager that block stdout for its scope, usage:
with HideOutput():
os.system('ls -l')
'''
def __init__(self, *args, **kw):
sys.stdout.flush()
self._origstdout = sys.stdout
self._oldstdout_fno = os.dup(sys.stdout.fileno())
self._devnull = os.open(os.devnull, os.O_WRONLY)
def __enter__(self):
self._newstdout = os.dup(1)
os.dup2(self._devnull, 1)
os.close(self._devnull)
sys.stdout = os.fdopen(self._newstdout, 'w')
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stdout = self._origstdout
sys.stdout.flush()
os.dup2(self._oldstdout_fno, 1)
根据@Yinon Ehrlich的回答,这个变种试图避免文件描述符泄露:
import os
import sys
from contextlib import contextmanager
@contextmanager
def stdout_redirected(to=os.devnull):
'''
import os
with stdout_redirected(to=filename):
print("from Python")
os.system("echo non-Python applications are also supported")
'''
fd = sys.stdout.fileno()
##### assert that Python and C stdio write using the same file descriptor
####assert libc.fileno(ctypes.c_void_p.in_dll(libc, "stdout")) == fd == 1
def _redirect_stdout(to):
sys.stdout.close() # + implicit flush()
os.dup2(to.fileno(), fd) # fd writes to 'to' file
sys.stdout = os.fdopen(fd, 'w') # Python writes to fd
with os.fdopen(os.dup(fd), 'w') as old_stdout:
with open(to, 'w') as file:
_redirect_stdout(to=file)
try:
yield # allow code to be run with the redirected stdout
finally:
_redirect_stdout(to=old_stdout) # restore stdout.
# buffering and flags such as
# CLOEXEC may be different