DEV Community

codemee
codemee

Posted on • Edited on

為什麼我的 sys.stdout.write 不會傳回輸出的字元數

正常來說, sys 下的 stdout 因為是 File 物件, 所以他的 write() 應該要傳回輸出的字元數, 像是這樣才對:

>>> import sys
>>> sys.stdout.write("hello")
hello5
Enter fullscreen mode Exit fullscreen mode

輸出結果中最後的 '5' 是因為輸出 5 個字元, 所以 Python shell 會把整個運算式, 也就是 write() 的傳回值顯示出來。

如果查看 sys.stdout 的型別, 可以看到他是 TextIOWrapper

>>> type(sys.stdout)
<class '_io.TextIOWrapper'>
Enter fullscreen mode Exit fullscreen mode

這個類別衍生自 TextIOBase, 因此上述的執行結果完全正確。

Thonny IDE 下的怪異現象

如果你把相同的程式拿到 Thonny 的 IDE 的互動窗格下測試, 就會發生異常狀況:

>>> import sys
>>> sys.stdout.write("hello")
hello
>>> a = sys.stdout.write("hello")
hello
>>> print(a)
None
>>>
Enter fullscreen mode Exit fullscreen mode

你會發現不會印出字元數量的 5, 而且若是觀察 write() 的傳回值, 會發現是 None, 也就是沒有傳回值。

檢查一下 sys.stdout 的型別:

>>> type(sys.stdout)
<class 'thonny.plugins.cpython_backend.cp_back.FakeOutputStream'>
Enter fullscreen mode Exit fullscreen mode

你會發現它根本不是剛剛看到的 TextIOWrapper 類別, 甚至應該要是最原始輸出的 sys.__stdout__ 也被改掉了:

>>> type(sys.__stdout__)
<class 'thonny.plugins.cpython_backend.cp_back.FakeOutputStream'>
>>>
Enter fullscreen mode Exit fullscreen mode

這個 FakeOutputStrem 類別是為了搭配 Thonny IDE 運作而特別撰寫的類別, 他的 write 根本不會傳回值:

def write(self, data):
    try:
        self._backend._enter_io_function()
        # click may send bytes instead of strings
        if isinstance(data, bytes):
            data = data.decode(errors="replace")

        if data != "":
            self._backend._send_output(data=data, stream_name=self._stream_name)
            self._processed_symbol_count += len(data)
    finally:
        self._backend._exit_io_function()
Enter fullscreen mode Exit fullscreen mode

我可以理解為了 IDE 的運作以客製的類別取代原本的 TextIOWrapper 類別, 但我實在不明白為什麼不傳回字元數符合一致的介面?

補充:上述測試是在 Thonny 4.0.2 進行, 根據官方的回應, 這個問題會在 4.0.3 版本修正, 傳回輸出的字元數。

IPython 在 Windows 平台上的特殊處理

如果你慣用 IPython, 那麼在 Windwos 平台上, IPython 也會有和 Thonny 類似的處理:

In [1]: import sys

In [2]: sys.stdout.write("hello")
hello
Enter fullscreen mode Exit fullscreen mode

不過 sys.__stdout__ 卻沒有被改過, 可以正常運作:

In [3]: sys.__stdout__.write("hello")
Out[3]: hello5
Enter fullscreen mode Exit fullscreen mode

如果觀察兩者的所屬類別, 就可以看到差異:

In [4]: type(sys.stdout)
Out[4]: colorama.ansitowin32.StreamWrapper

In [5]: type(sys.__stdout__)
Out[5]: _io.TextIOWrapper
Enter fullscreen mode Exit fullscreen mode

sys.stdout 被重新導向到奇怪的 colorama.ansitowin32.StreamWrapper 類別了, 他的 write 一樣是不會傳回值:

def write(self, text):
    self.__convertor.write(text)
Enter fullscreen mode Exit fullscreen mode

從同一檔案中其他的註解看來:

Implements a write() method which, on Windows, will strip ANSI character sequences from the text, and if outputting to a tty, will convert them into win32 function calls.

主要是為了拿掉 Windows 之前不支援的 ANSI 序列碼, 並轉換成對應的 Win32 系統函式。不過我實在不懂為什麼不維持一致, 傳回字元數呢?

Colab 也和 IPython 一樣

以 web 為介面的 Colab 因為沒有終端機, 應該也會修改 sys.stdout, 我們來測試一下 (本文測試的是 2023/1/12 版本的 Colab):

果不其然, 跟 IPython 類似, sys.stdout 被改成 ipykernel.iostream.OutStream, 他的 write() 是正確的:

def write(self, string: str) -> int:
    """Write to current stream after encoding if necessary
    Returns
    -------
    len : int
        number of items from input parameter written to stream.
    """
    ...
        else:
            self._schedule_flush()

    return len(string)
Enter fullscreen mode Exit fullscreen mode

不過這個方法是在 2021/6/14 的版本才開始傳回字元數, 因此可以推斷 Colab 上的版本是比較舊的, 沒有傳回字元數。這可以由舊版本的 write() 並沒有 DOC 字串來證實:

def write(self, string):
    if self.echo is not None:
      ...
Enter fullscreen mode Exit fullscreen mode

另外, 根據這個版本的修改記錄, 原來的程式有為了 Python 2 所設計處理輸出內容並非字串的狀況, 因此無法計算字元數:

Remove the piece of logic that handle not isinstance(str) it is a leftover from Python 2, in pure Python, sys.stdout.write only accepts str, therefore we have no reason not to do the same.

因為新的 Python 3 版本限定只會輸出字串, 所以就改成和標準程式庫一樣傳回輸出的字元數了。

Jupyter Lab 採用的是新版的程式碼

既然 Colab 會有問題, 那麼系出同源的 Jupyter Lab 會不會也有一樣的狀況呢?我們來試看看:

你可以看到雖然 Jupyter Lab 也和 Colab 一樣將 sys.stdout 改成 ipykernel.iostream.OutStream, 但是顯然他用的是會傳回字元數的版本, 這可以從他的 __doc__ 是有內容的來證實。

結語

本文探討的雖然是個小細節, 不過如果沒注意到, 可能會在測試程式時百思不得其解, 造成莫大的困擾。

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs