如何刷新 Python print 函数的输出

2024年8月29日 | 阅读 10 分钟

在本教程中,我们将学习如何使用 print() 函数的 flush 参数显式刷新输出数据缓冲区。我们还将确定何时需要刷新数据缓冲区以及何时不需要刷新。我们还将讨论如何更改单个函数和脚本的数据缓冲。

print() 函数在默认参数下的行为取决于我们是在交互模式还是非交互模式下运行 Python。当以交互方式运行 Python(例如,在 Jupyter Notebook 中)时,print() 的输出是行缓冲的,这意味着每行输出一经生成就会立即写入屏幕。但是,当以非交互方式运行 Python(例如,从命令行运行 Python 脚本)时,输出是块缓冲的,这意味着它会存储在缓冲区中,直到累积到一定量的数据,然后一次性写入屏幕。

行缓冲意味着输出会在每个换行符 \n 打印后刷新(即写入屏幕)。块缓冲意味着输出会存储在缓冲区中,直到累积到一定量的数据,然后一次性刷新。

Python 如何缓冲输出

当我们对文件类对象进行写入调用时,Python 默认会缓冲调用——这是一个好主意!与随机存取存储器 (RAM) 访问相比,磁盘写入和读取操作很慢。当脚本通过将字符批量存储在 RAM 数据缓冲区中,然后通过单个系统调用将它们一次性写入磁盘来减少写入操作的系统调用次数时,我们可以节省大量时间。让我们以一个真实的例子来理解缓冲——

交通灯可以作为缓冲的一个例子;如果每辆车一到达路口就被允许通过,就会导致交通堵塞,来自不同方向的汽车会卡在路口。相比之下,交通灯会缓冲一个方向的交通流量,同时允许另一个方向的交通移动,从而防止交通堵塞并确保整体交通流量更有效。同样,在将数据写入磁盘之前将其缓冲到内存中,可以更有效地利用系统资源,减少写入数据所需的系统调用次数,并提高系统的整体性能。

有时,我们希望在数据缓冲区刷新之前将其填满。例如——救护车需要尽快通过,我们不希望它在交通灯处等待,直到排队一定数量的车辆。

在开发程序时,通常需要获取代码执行的实时反馈。在这种情况下,立即刷新数据缓冲区至关重要。以下是立即刷新有益的几个场景示例:

  • 即时反馈 - 在交互式环境(例如 Python REPL 或 Python 脚本写入终端的情况)中需要此功能。
  • 文件监视 - 当我们写入文件类对象时,写入操作的输出在我们的脚本仍在执行时被另一个程序读取。

在上述两种情况下,都需要在输出生成后立即读取,而不是等待足够的输出累积在缓冲区中并将其刷新。否则可能导致数据丢失或过时,从而在程序中引发错误或其他问题。

在许多情况下,缓冲是有益的,但在某些时候,过多的缓冲会导致缺点。我们可以根据需要实施不同类型的数据缓冲。

  • 无缓冲 - 这意味着没有对正在写入的数据进行缓冲。它不是批量处理数据并一次性写入,而是每个字节一生成就写入,为每个写入的字节创建一个新的系统调用。这可能导致许多系统调用,从而显著减慢写入过程并影响我们程序的整体性能。
  • 行缓冲 - 行缓冲通常用于交互模式,例如 Python 解释器或命令行界面,用户希望在输入时立即看到输出。在行缓冲模式下,当检测到换行符时,会显示输出,允许用户实时查看输出。然而,值得注意的是,行缓冲可能不适用于性能至关重要的场景,因为缓冲区在每个换行符之后都会刷新,这会影响我们程序的整体效率。
  • 全缓冲 - 全缓冲是一种缓冲类型,它将所有正在写入的数据收集到一个缓冲区中,直到它达到一定大小或直到缓冲区手动刷新。当缓冲区满或手动刷新时,数据会在一个系统调用中写入文件类对象。全缓冲是最有效的缓冲形式,因为它最大限度地减少了将数据写入文件类对象所需的系统调用次数,这可以显著提高我们程序的性能。

在 Python 中,当我们写入文件类对象时,会使用块缓冲。但是,如果我们在交互式环境中写入,它会执行行缓冲。

让我们看下面的脚本以更好地理解。

示例 -

输出

3
2
1
Go!

time 模块中的 sleep() 函数用于暂停程序执行指定的秒数。在此示例中,它会在每次计数之间暂停程序 1 秒。

range() 函数用于创建从 3 到 1(不包括 0)的数字序列,步长为 -1(即倒计时)。

如果程序缓冲整个倒计时并在完成后才打印,可能会导致在起跑线等待的运动员感到困惑和不确定。因此,有必要在每次倒计时发生时打印出来,并在一秒钟的延迟,这样运动员就能清楚准确地知道倒计时何时接近尾声。

为 Python 刷新打印输出添加换行符

如果我们在 Python REPL 中运行代码片段,或者直接使用 Python 解释器将其作为脚本执行,我们不会遇到任何问题。在交互式环境(例如终端)中运行程序时,标准输出流 (STDOUT) 将是行缓冲的。这意味着当你使用 print() 函数向 STDOUT 写入内容时,输出将保留在缓冲区中,直到遇到换行符 (\n)、缓冲区变满或程序结束。

当遇到换行符时,缓冲区会自动刷新,这意味着缓冲区的所有内容会立即写入 STDOUT。这就是为什么在使用 print() 输出多行文本时,每行一经打印就会立即出现,而无需等待缓冲区填满或程序结束。

如果我们使用 print() 及其参数编写上述脚本,则每次调用 print() 的末尾都会隐式写入一个换行符。

示例 -

输出

3 
2
1
Go

当我们使用 print() 函数输出数字时,数字会连同换行符 (\n) 一起发送到输出缓冲区。由于我们使用的是交互式环境(例如终端),print() 函数以行缓冲模式运行,这意味着在每次 print() 调用之后,缓冲区会自动刷新并将输出显示在终端上。因此,每个数字都会单独发送到终端,并带有自己的系统调用,从而导致数字立即显示在终端上。

为了在输出中获得换行符,最好避免向 end 传递任何参数。在这种情况下,print() 语句将对 end 参数使用默认值 "/n"。

在下一节中,我们将讨论一个可能发生的数据缓冲意外问题,如果我们出于性能原因修改代码。此外,我们将发现如何解决此问题以确保我们的程序按预期运行。

设置 Flush 参数

我们知道 print() 有一个 end 参数,并且想将倒计时数字打印在一行中。我们可以将 end 值从默认换行符更改为空格 ("")。让我们理解下面的例子。

示例 -

输出

3 2 1 Go!

尽管我们打算在一行中打印数字,但为了性能而重构代码导致了一个新问题。数字不是在倒计时时一个接一个地出现,而是在程序执行完毕后同时出现。

块缓冲是导致意外行为的原因。尽管 print() 写入的输出流仍然是终端(一个交互式环境),但由于修改了 end 参数,行缓冲的行为发生了变化。输出流在技术上仍然是行缓冲的,和以前一样,但由于 end 参数已更改,不再写入任何换行符 (\n)。因此,行缓冲从未被触发。

示例 -

输出

3 2 1 Go

我们将默认的 flush 值更改为 True,这将刷新输出流,而与我们正在写入的文件流的默认数据缓冲无关。

如果我们决定从 print() 语句中删除任何换行符,需要注意的是,Python 的默认行为是缓冲输出而不是实时显示。输出只会在缓冲区满或程序执行完毕后才会显示。为了确保您的输出实时显示,我们必须将 flush 参数设置为 True。通过这样做,Python 将在每个 print() 语句之后立即刷新输出缓冲区,允许输出在终端上实时显示。

更改 PYTHONBUFFERED 环境变量

要在不更改现有代码的情况下更改 PYTHONBUFFERED 环境,我们需要在没有数据缓冲区的情况下使用命令执行脚本。

示例 -

根据操作系统,我们将使用 cat 或 echo 命令来监视脚本。要在不更改源代码的情况下无缓冲地运行 countdown.py,我们可以在将其输出传输到 cat 或 echo 之前使用 -u 命令选项执行 Python。

-u 命令选项用于禁用输出流、标准输出 (stdout) 和标准错误 (stderr) 的数据缓冲区。

即使将程序的输出传输到另一个程序,我们也可以从 print() 中获得无缓冲输出,而无需对代码进行任何更改。在此方法中,print() 直接写入标准输出,而无需缓冲数据,从而允许输出立即传输到管道中的下一个程序。

除了依赖 -u 命令行选项来实现无缓冲输出之外,您还可以显式地将 PYTHONUNBUFFERED 环境变量设置为非空字符串。默认情况下,此变量设置为空字符串,但将其设置为任何其他值将导致 Python 在当前环境中的所有脚本运行中以无缓冲模式执行。这提供了一种替代方法,可以确保您的程序输出立即显示,无论是否将其传输到另一个程序。

您需要在运行脚本之前更改环境中的 PYTHONUNBUFFERED 值,此更改才会生效。

对于 Windows -

对于 Linux -

使用 functools.partial 更改 Print 的签名

在本节中,我们将探讨另一个实际情况。假设一个团队引入了一个大型数据库,并要求对现有日志文件进行自动化监控。

我们已将脚本任务分配给使用 print() 监视,以将日志信息发送到输出流。

由于输出缓冲,脚本的输出无法实时监控。该脚本包含大量的 print() 语句,使其难以修改而不影响原始代码库。为了最大限度地减少对原始脚本的更改,找到不需要大量修改的解决方案非常重要。

更改其函数签名是快速修改我们要监视的脚本中 print() 默认行为的一种方法。这可以通过创建 print() 函数的修改版本来实现,该版本包含所需的默认行为,而无需修改原始脚本。

示例 -

当我们在脚本顶部添加上述两行代码时,我们将 print() 的内置签名更改为将 flush 设置为 True。通过在脚本中创建 print() 函数的修改版本,所有后续对 print() 的调用都将自动使用更新后的函数签名。即使输出流不是终端,数据缓冲区也会在每次 print() 调用后刷新。这种修改有助于确保无论输出流的目的地如何,都可以实时监控脚本的输出。

让我们理解下面的例子。

示例 -

输出

3
2
1
Go!

这种方法允许您在脚本中使用缓冲和非缓冲 print() 调用。通过预先定义一个指定非缓冲写入标准输出的函数,您可以轻松地将其集成到现有代码库中,而无需修改每个 print() 调用。此外,给函数一个描述性名称可以帮助其他开发人员比在每个 print() 调用中添加 flush=True 参数更快地理解修改的目的。

结论

本教程包括使用 print() 的 flush 参数显式刷新输出数据缓冲区。我们探讨了更改单个函数、整个脚本甚至整个 Python 环境的数据缓冲。通过运行一个稍作修改的短代码片段,您可以观察到 print() 在交互模式下以行缓冲方式执行,否则以块缓冲方式执行。您还了解了何时可能需要更改此默认行为以及可用的方法。