正常來說, sys 下的 stdout 因為是 File 物件, 所以他的 write() 應該要傳回輸出的字元數, 像是這樣才對:
>>> import sys
>>> sys.stdout.write("hello")
hello5
輸出結果中最後的 '5' 是因為輸出 5 個字元, 所以 Python shell 會把整個運算式, 也就是 write()
的傳回值顯示出來。
如果查看 sys.stdout 的型別, 可以看到他是 TextIOWrapper:
>>> type(sys.stdout)
<class '_io.TextIOWrapper'>
這個類別衍生自 TextIOBase, 因此上述的執行結果完全正確。
Thonny IDE 下的怪異現象
如果你把相同的程式拿到 Thonny 的 IDE 的互動窗格下測試, 就會發生異常狀況:
>>> import sys
>>> sys.stdout.write("hello")
hello
>>> a = sys.stdout.write("hello")
hello
>>> print(a)
None
>>>
你會發現不會印出字元數量的 5, 而且若是觀察 write()
的傳回值, 會發現是 None
, 也就是沒有傳回值。
檢查一下 sys.stdout
的型別:
>>> type(sys.stdout)
<class 'thonny.plugins.cpython_backend.cp_back.FakeOutputStream'>
你會發現它根本不是剛剛看到的 TextIOWrapper
類別, 甚至應該要是最原始輸出的 sys.__stdout__
也被改掉了:
>>> type(sys.__stdout__)
<class 'thonny.plugins.cpython_backend.cp_back.FakeOutputStream'>
>>>
這個 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()
我可以理解為了 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
不過 sys.__stdout__
卻沒有被改過, 可以正常運作:
In [3]: sys.__stdout__.write("hello")
Out[3]: hello5
如果觀察兩者的所屬類別, 就可以看到差異:
In [4]: type(sys.stdout)
Out[4]: colorama.ansitowin32.StreamWrapper
In [5]: type(sys.__stdout__)
Out[5]: _io.TextIOWrapper
sys.stdout 被重新導向到奇怪的 colorama.ansitowin32.StreamWrapper
類別了, 他的 write
一樣是不會傳回值:
def write(self, text):
self.__convertor.write(text)
從同一檔案中其他的註解看來:
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)
不過這個方法是在 2021/6/14 的版本才開始傳回字元數, 因此可以推斷 Colab 上的版本是比較舊的, 沒有傳回字元數。這可以由舊版本的 write()
並沒有 DOC 字串來證實:
def write(self, string):
if self.echo is not None:
...
另外, 根據這個版本的修改記錄, 原來的程式有為了 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)