如何检查可能为空的stdin,而无需等待输入?

5
我想在不等待输入的情况下从键盘读取输入。目的是在“无限”的循环中使用,即while True:
到目前为止,我一直在尝试操作readcharhttps://pypi.python.org/pypi/readchar/0.6,但没有成功。虽然它不等待Enter,但仍然在等待一些输入。我不希望它等待输入,而只是检查并返回""或如果没有输入则返回某个占位符。
以下是我正在使用的内容:
def readchar():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

def main():
    while True:
        current = readchar()
        if current == "some letter":
            print("things happen")

1
你所说的“等待(wait)”通常被称为“阻塞(blocking)”。尝试在SO和Google上搜索“read stream non blocking python”。 - brunsgaard
通常来说,这个过程有两个部分:使用O_NONBLOCK打开/重新打开/fcntl文件(您可能会选择使用/dev/tty而不是标准输入),然后要么使用select.select,要么处理read返回的EAGAIN错误或None(如果您正在使用类似于sys.stdinTextIOWrapper而不是其底层的.buffer.raw,则在任一情况下您可能会得到一个''或其他内容;据我所知,它实际上没有被指定...)。 - abarnert
richinput这样的库的源代码可能展示了你需要的所有东西,即使你不想直接使用这样的库。 - abarnert
还有一件事:看起来你可能真正想要的是curses,所以...如果你从未听说过它,请查看一下。 - abarnert
我确实考虑过 curses,但就整个项目而言,我认为它对我的目的来说有点过度杀伤力/离题。这是 curses 唯一真正能够大幅帮助的部分。 - LegendaryQ
1个回答

7
POSIX I/O函数是Python文件对象的基础,它们有两种模式:阻塞和非阻塞,由名为O_NONBLOCK的标志控制。特别地,read函数表示:

当尝试读取一个当前没有数据可用的文件时…如果设置了O_NONBLOCK,则read()将返回-1并将errno设置为[EAGAIN]

在Python中,该标志在os模块中可用。

sys.stdin已经打开,因此你不能只是将O_NONBLOCK传递给os.open函数,那么你该怎么办?嗯,你实际上可能希望打开/dev/tty;这有点取决于你实际在做什么。在那种情况下,答案很明显。但是让我们假设你不需要这样做。所以,你想要更改已经打开文件的标志。这正是fcntl的用途。你使用F_GETFL操作来读取当前标志,或者在O_NONBLOCK位中设置,然后F_SETFL结果。当然,如果你想恢复原状,可以记住当前标志。

在Python中,fcntl函数和操作常量都在fcntl模块中可用。


最后一个问题: sys.stdin 不是原始文件对象,它是一个TextIOWrapper,在BufferedReader上进行Unicode解码,而FileIO则添加了缓冲。因此,sys.stdin.read() 并没有直接调用 POSIX 的 read 函数。为了做到这一点,你需要使用 sys.stdin.buffer.raw。如果你想在原始和普通输入之间来回切换,可能还需要进行大量的小心清除。(请注意,这意味着你放弃了 Unicode,并获得了一个单字节的 bytes 对象,这可能是UTF-8字符的一半或终端转义字符的四分之一。希望你已经预料到了。)


现在,当没有可用内容时,FileIO.read 返回什么?嗯,它是 RawIOBase 的一个实现,RawIOBase.read 说:

如果返回 0 字节,并且大小不为 0,则表示文件结束。如果对象处于非阻塞模式并且没有可用的字节,则返回 None

换句话说,如果没有可用内容,则会得到 None,对于 EOF,将获得 b'',对于其他任何内容,都将获得一个单字节的 bytes


因此,将所有内容组合在一起:

old_settings = termios.tcgetattr(fd)
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
try:
    tty.setraw(fd)
    fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
    return sys.stdin.buffer.raw.read(1)
finally:
    fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

最后一件事:有没有一种方法可以在不读取输入的情况下判断输入是否已准备好?是的,但不具有可移植性。根据您的平台,selectpollepoll和/或kqueue可能可用,并且可能适用于常规文件。read(0)可能保证返回b''而不是None等等。您可以阅读本地手册页面。但更简单的解决方案是与C的stdio相同:添加一个1字节最大缓冲区,并使用它来实现自己的readpeekreadunread包装器。


感谢您详细的回答。我尝试了您的实现以及一个使用/dev/tty(这是一个选项)的简单实现,但在尝试从/dev/tty或stdio读取时,它返回了“OSError: [Errno 11] Resource temporarily unavailable”的错误。有什么想法吗? - LegendaryQ
@LegendaryQ:你在哪个平台上?你是如何运行这个脚本的?特别是,它是否实际在终端或其他TTY提供程序中运行?它是作为拥有该TTY的用户运行的,还是被某个降低权限的程序启动?除了基本的猜测之外,如果没有看到你的实际代码以及引发异常的确切行,很难调试它。 - abarnert
啊,我的错。我之前使用了sys.stdin.read(1)而不是sys.stdin.buffer.raw.read(1),后者解决了问题。谢谢! - LegendaryQ
@LegendaryQ:抱歉,我在最初的版本中错过了它,并在后来的编辑中进行了更正。正如我在文本中解释的那样,sys.stdin.read() 可能正在从缓冲区读取,或者读取到完整的 Unicode 字符为止。 - abarnert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接