Try   HackMD

Python の -u オプションを用いて stdout と stderr のバッファを行わないようにする

Python内で新しい子プロセスを立ち上げる時に、Pythonの subprocess モジュールを用いることで、子プロセスを良い感じに抽象化して扱うことができます。例えば以下のコードでは、Bashスクリプトを子プロセスで実行し、その子プロセスの標準出力を文字列として受け取っています。ここで注目して欲しいのは、asyncio を用いて、標準出力をインタラクティブに受け取れることです。1秒ごとに、インクリメントされた数値が表示されます。

cmd = 'for i in `seq 1 5`; do echo $i; sleep 1; done'
proc = await asyncio.create_subprocess_shell(
        cmd,
        stdout=asyncio.subprocess.PIPE)
async for data in proc.stdout:
    line = data.decode('ascii').rstrip()
    print(line)

ここで、BashではなくPythonの子プロセスを立ち上げて結果を受け取ってみようと考え、以下のコードを作成しました。

cmd = \
    'python -c "' + \
    'import time\n' + \
    'for i in range(5): print(i); time.sleep(1)"'
proc = await asyncio.create_subprocess_shell(
        cmd,
        stdout=asyncio.subprocess.PIPE)
async for data in proc.stdout:
    line = data.decode('ascii').rstrip()
    print(line)

いざこのプログラムを実行してみると、期待通りの結果が得られませんでした。最初に示したプログラムでは1秒毎に数値が表示されていましたが、上記プログラムを実行すると、最初5秒間は何も表示されず、5秒後に全ての結果が一気に表示されてしまいました。

色々試してみましたが、Pythonプログラムに変更を加えても、インタラクティブに結果を受け取ることはできませんでした。挙動を見る限り、どこかに文字列がバッファされているようだったので、もしや Python のランタイムが怪しいのではと思いググると、あっさり問題は解決しました。

PythonのCLIツールには、-u オプションというものがあります。-u オプションは、Pythonのランタイムに stdout と stderr のバッファをさせるかどうかを決めるオプションです。デフォルトでは、バッファをするようになっているため、このオプションをつけることでバッファをしないようにします。

-u オプションを追記した以下のプログラムでは、期待通りにインタラクティブな結果を得ることができました。よかった。

cmd = \
    'python -u -c "' + \
    'import time\n' + \
    'for i in range(5): print(i); time.sleep(1)"'
proc = await asyncio.create_subprocess_shell(
        cmd,
        stdout=asyncio.subprocess.PIPE)
async for data in proc.stdout:
    line = data.decode('ascii').rstrip()
    print(line)

今回は表面的な調査しかしていませんが、また時間があれば Python ランタイムでどのようにバッファが行われているのか、調査してみたいです。

参考文献

コード全文

import asyncio
import sys

async def run():
    cmd = \
        'python -u -c "' + \
        'import time\n' + \
        'for i in range(5): print(i); time.sleep(1)"'

    proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE)

    async for data in proc.stdout:
        line = data.decode('ascii').rstrip()
        print(line)

    await proc.wait()

asyncio.run(run())