DEV Community

codemee
codemee

Posted on • Updated 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__ 是有內容的來證實。

結語

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

Top comments (0)