有時候進程的運行環境裡,locale 會被設定成只支援 ASCII 字元集的(比如 LANG=C)。這時候 Python 就會把標準輸出和標準錯誤的編碼給設定成 ascii,造成輸出中文時報錯。
一種解決辦法是設定支援 UTF-8 的 locale,但是那需要在 Python 進程啟動前設定。啟動之後,初始化過了,再設定 locale 也不會重新初始化那些對象。
另一種辦法是往 sys.stdout.buffer 這種地方直接寫 bytes。理論上完全沒問題,但是寫起程式來好累……
我就去找了一下怎麼優雅地弄一個新的 sys.stdout 出來。Python 3 的 I/O 不再使用 C 標準庫的 I/O 函數,而是直接使用 OS 提供的介面。封裝位於 io 這個模組裡邊,有帶緩衝的,不帶緩衝的,二進位的,文本的。
研究了一下文檔可知,sys.stdout 是個 io.TextIOWrapper,有個 buffer 屬性,裡邊是個 io.BufferedWriter。我們用它造一個新的 io.TextIOWrapper,指定編碼為 UTF-8:
import sys
import io
def setup_io():
sys.stdout = sys.__stdout__ = io.TextIOWrapper(
sys.stdout.detach(), encoding='utf-8', line_buffering=True)
sys.stderr = sys.__stderr__ = io.TextIOWrapper(
sys.stderr.detach(), encoding='utf-8', line_buffering=True)
這裡除了可以設定編碼之外,也可以設定錯誤處理和緩衝。所以這個技巧也可以用來容忍編碼錯誤、改變標準輸出的緩衝(不需要在啟動的時候加 -u 了)。
其實這樣子還是不夠徹底。Python 在很多地方都有用到預設編碼。比如 subprocess,指定 universal_newlines=True 時 Python 會自動給標準輸入、輸出、錯誤編解碼,但是呢,在 Python 3.6 之前,這裡的編碼是不能手動指定的。還有參數的編碼,也是不能指定的(不過可以傳 bytes 過去)。