PythonとWindowsとUnicode
まあ、そうこうするうちにWindowsでもUTF-8デフォルトになりそうだけども。
https://methane.hatenablog.jp/entry/2022/04/26/Python_3.15からデフォルトのエンコーディングがUTF-8になり
とりあえず3.11頃の話。
デフォルトエンコーディング
locale.getencoding() で取れるデフォルトのエンコーディングは、Windowsだとシステムに設定したANSIコードページ依存
「地域」コントロールパネルの「Unicode対応ではないプログラムの言語」のやつ
open() で encoding を省略した場合など、多くの場面でこのエンコーディングが使われる
システムエンコーディングを無視してUTF-8を強制したかったら -Xutf8 や PYTHONUTF8=1 で UTF-8 Mode を有効にするとよい
これはあくまでPythonコードの実行に影響するだけで、C拡張やサブプロセスに直接影響するものではない
*nix 環境向けには locale coercion という挙動もあり、こっちはCロケールみたいなのが設定されていた時に LC_CTYPE をUTF-8系のに変更する。こっちはPython本体だけでなく、C拡張やサブプロセスにも影響する。
Windowsでもコンソール周りとファイルシステム周りはデフォルトUTF-8になる
コンソール周り: WindowsのコンソールはもうUnicode持ちになっていて、APIもUnicode系が推奨されている。ので移行しよう、ぐらいの話かな? https://peps.python.org/pep-0528/
ファイルシステム周り: POSIX系ではパスのネイティブな持ち方がバイト列なので、Pythonでもbytesで扱うコードがちょいちょいある。一方最近のWindowsはネイティブがUTF16なので、ACP経由だとエンコーディング変換が挟まり、事故る可能性がある。ので、UTF16にすれば安心でしょ?という話らしい。 https://peps.python.org/pep-0529/
逆にバイト列なんでもこいなPOSIX系でもUnicodeなstrを使いやすいように surrogateescape が生えたらしい https://peps.python.org/pep-0383/
Windowsでバイト指向インターフェイスだと完全には保持できないってのは、ACP系のWinAPIを使うからそこで壊れるって話なんだろうか?Unicode系のWinAPIでやりとりして、内部はUTF8ででも持てばという気もするが…。
コンソールとリダイレクト
前述の通り、3.11あたりでもstdioは基本utf-8になっている。が、ConsoleAPIを使うという話からも推測されるように、stdioの先がファイルやパイプである場合はそうはならない。
code:stdio
C:\> python -c "import sys; [print(type(out.buffer.raw), out.isatty(), out.encoding, out.errors) for out in sys.stdout,sys.stderr]"
<class '_io._WindowsConsoleIO'> True utf-8 surrogateescape
<class '_io._WindowsConsoleIO'> True utf-8 backslashreplace
C:\>python -c "import sys; [print(type(out.buffer.raw), out.isatty(), out.encoding, out.errors) for out in sys.stdout,sys.stderr]" 2>&1 | more
<class '_io.FileIO'> False cp1252 surrogateescape
<class '_io.FileIO'> False cp1252 backslashreplace
手入力や端末出力なら日本語扱えるのに、入力や出力をパイプした途端に死ぬ、ということが起きる
ACPと互換性のない文字列を入出力する場合…
stdinはしれっと化けてエラーにはならない
surrogateescape がエンコードしてくれないの?というと、ANSI系APIを使う時点で落ちてる(気がする)
stdoutはerrorsが surrogateescape なので UnicodeEncodeError になる
stderrはerrorsが backslashreplace なので \uxxxx の形で出力され、エラーにはならない
特殊なケースとして、fdを複製してfdopenしてstderrを置き換えるみたいなことをされてると、rawやencodingは普通だがerrorsがstrictになってたりする
これを避けるには前述の UTF-8 Mode を使うか、stdioのエンコーディングを PYTHONIOENCODING 環境変数で指定する
余談: コンソールのコードページ
ここまでコードページというとGetACPで取れるシステムロケールの話をしていたが、Windowsのコンソールはそれとは別のコードページ設定を持っている(GetConsoleCP, GetConsoleOutputCP)
https://learn.microsoft.com/en-us/windows/console/high-level-console-i-o
https://learn.microsoft.com/en-us/windows/console/console-code-pages
https://stackoverflow.com/questions/1259084/what-encoding-code-page-is-cmd-exe-using
多分…WindowsConsoleIO(中身ConsoleWriteW等々)を使う今のPythonはほとんど関係ない話だと思っているが…昔は関係あったかもしれない
Pythonでは os.device_encoding にfd番号を渡すことで取れると思うが、cpythonのコードでこれを参照しているところは(もはや)ない?
stdioがpipeにつながっている場合に使われるのもこれではなく locale.getencoding() っぽいし
chcpというと、pyenv-winを使うと勝手に chcp 1250 される
https://github.com/pyenv-win/pyenv-win/issues/344
これが原因でコマンドライン引数に渡した文字列が化けるとかいう話を見かけたが再現しない
入力はwmainがwchar_tで受けてるのでいい気がするし、出力はここまでに書いた通りなので
#Python #Windows