3 ポイント 投稿者 GN⁺ 2024-04-28 | 1件のコメント | WhatsAppで共有
  • Python 3.15はUTF-8モードをデフォルトで有効にし、ファイル、標準入出力、パイプのデフォルトエンコーディングをUTF-8に揃える
  • UTF-8はソースファイル、JSON・TOML・YAML、主要エディタ、Webデータ、Node.js・Go・Rust・Javaなどで事実上の標準エンコーディングとして使われており、相互運用性が高まる
  • 従来のデフォルトエンコーディングはプラットフォームによって異なり、Unix開発者がencoding="utf-8"を省略するとWindowsなどで不一致によるバグが発生しうる
  • 必要であればPYTHONUTF8=0または-X utf8=0で無効化でき、互換性確認にはEncodingWarningencoding="utf-8"encoding="locale"locale.getencoding()が使われる
  • デフォルトエンコーディングに依存するプログラムは、主にWindowsでUnicodeError、mojibake、静かなデータ破損に遭遇する可能性があり、事前確認が必要

Python 3.15で変わるデフォルトエンコーディング

  • PEP 686は、PEP 540UTF-8モードをデフォルト値として有効化する変更
  • デフォルトエンコーディングが必要なファイル、stdio、パイプで、PythonはUTF-8を一貫して使用する
  • Python 3.15からデフォルトで有効化され、ユーザーは次の方法で無効化できる
    • PYTHONUTF8=0
    • -X utf8=0

UTF-8をデフォルトにする理由

  • UTF-8は多くの環境で標準的なテキストエンコーディングとして定着している
    • PythonソースファイルのデフォルトエンコーディングはUTF-8
    • JSON、TOML、YAMLはUTF-8を使用する
    • Visual Studio CodeやWindows Notepadを含むほとんどのテキストエディタがUTF-8をデフォルトで使用する
    • インターネット上のほとんどのWebサイトとテキストデータがUTF-8を使用する
    • Node.js、Go、Rust、Javaを含む多くの人気プログラミング言語がUTF-8をデフォルトで使用する
  • PythonのデフォルトエンコーディングがUTF-8に変わると、他のツール・言語・データ形式との相互運用性が向上する
  • 多くのUnix環境のPython開発者は、デフォルトエンコーディングがプラットフォーム依存であることを忘れ、JSON・TOML・Markdown・PythonソースファイルのようなUTF-8テキストを読む際にencoding="utf-8"を省略する
  • プラットフォームごとのデフォルトエンコーディングの違いは、このようなコードが別の環境で壊れるバグの原因になる

locale APIとencoding="locale"の修正

  • UTF-8モードはlocale.getpreferredencoding(False)に影響するため、UTF-8モードに関係なくlocaleエンコーディングを取得するAPIが必要
  • locale.getencoding()はこの目的のために追加され、localeエンコーディングを返すがUTF-8モードは無視する
    • このAPIはPython 3.11で追加された
  • warn_default_encodingオプションが指定されると、locale.getpreferredencoding()open()と同様にPEP 597EncodingWarningを発生させる
  • PEP 597TextIOWrapperencoding="locale"オプションを追加し、localeエンコーディングを明示的に指定できるようにした
  • 従来はUTF-8モードでencoding="locale"を指定しても、TextIOWrapper"UTF-8"を使用していた
    • これはPEP 597の動機と合っていなかった
    • Pythonのデフォルトテキストエンコーディングが変わる際に、UTF-8モードがデフォルトになる状況を想定していなかったため
  • この不一致はPython 3.11で修正され、UTF-8モードでもencoding="locale"を渡すとlocaleエンコーディングを使用する

後方互換性と移行手順

  • ほとんどのUnixシステムはUTF-8 localeを使用し、PythonはlocaleがCまたはPOSIXのときにUTF-8モードを有効化するため、変更の影響は主にWindowsユーザーに集中する
  • デフォルトエンコーディングに依存するPythonプログラムは、次の問題に遭遇する可能性がある
    • UnicodeError
    • mojibake
    • 静かなデータ破損
  • 後方互換性の問題を修正する推奨手順は次のとおり
    1. UTF-8モードを無効化する
    2. PEP 597EncodingWarningで、UTF-8モードが影響する箇所を見つける
      • encodingオプションが省略されている場合は、encoding="utf-8"またはencoding="locale"の使用を検討する
      • locale.getpreferredencoding()を使用している場合は、"utf-8"またはlocale.getencoding()の使用を検討する
    3. UTF-8モードでアプリケーションをテストする

Ruby・Javaの先行事例と却下された代替案

  • RubyはRuby 3.0、2020年にWindowsのデフォルトexternal_encodingをUTF-8へ変更した
  • JavaはJDK 18、2022年にデフォルトテキストエンコーディングをUTF-8へ変更した
  • RubyとJavaはいずれも後方互換性のためのオプションを提供しているが、PythonのEncodingWarningのようなデフォルトエンコーディング使用の警告は提供していない
  • デフォルトエンコーディングの使用自体を廃止する案は却下された
    • ASCIIテキストだけを読み書きする用途でデフォルトエンコーディングを使うケースが多い
    • Unixでのみ実行される非クロスプラットフォームアプリケーションには、そのような警告は有用ではない
    • すべての場所でencodingを強制するとユーザー負担が大きく、多数のDeprecationWarningはユーザーが警告を無視する原因になりうる
    • PEP 387は後方互換性を破る変更に警告の追加を求めているが、必ずしもDeprecationWarningを求めているわけではない
  • subprocessモジュールのパイプのデフォルトエンコーディングとしてPYTHONIOENCODINGを使う案も却下された
    • この方式はUTF-8モードでもsubprocess.Popen(text=True)にレガシーエンコーディングを使えるようにする
    • しかし「デフォルトエンコーディング」を複雑にし、この方式自体も後方互換性を破る変更である
    • ユーザーはtext=Trueencoding="utf-8"またはencoding="locale"に変えるまで、UTF-8モードを無効化できる

ユーザー教育の観点

  • 新規ユーザーは最初の1年間、テキストエンコーディングを学ぶ必要が減る
  • 非UTF-8テキストファイルを扱う必要が出たときにエンコーディングを学べばよい
  • 既存ユーザーは後方互換性の手順に従って、影響を受ける箇所を確認する

1件のコメント

 
GN⁺ 2024-04-28
Hacker News の意見
  • デフォルトのテキストファイルエンコーディングがプラットフォームによって変わることにはずっとイライラしていたので、今回の変更は歓迎
    ファイルシステムのエンコーディングまで触ろうとしていない点も良い。あれは別問題で、それはそれで厄介

    • Windows のシステム既定コードページは、プラットフォームだけでなくシステムロケールにも依存する
      Windows が TextOutA のような ANSI 関数に UTF-8 コードページを使わせる簡単な選択肢を長らく提供しなかったのは大きな失敗だった。manifest ファイルで可能になったのは Windows 10 開発の中盤ごろのことで、こうした機能は NT4 や Windows 98 の時代に入っているべきだった
    • 歴史的には筋が通っていた。ほとんどのソフトウェアがローカル専用で、テキストファイルもローカルのエンコーディングだと期待されていたため
      プラットフォームだけでなくユーザーの優先ロケールにも依存しており、C 標準ライブラリも同じように動作する。たとえば Unix/Linux では西欧言語で iso-8859-1 が一般的で、ユーロ導入後は 記号を含む iso-8859-15 に切り替えることが多くなった。UTF-8 が問題なく動作し始めたのは 2000 年代後半ごろで、Debian は Etch リリースでデフォルトを UTF-8 に変更した
    • 数日前には、改行コードが暗黙に変わることにやられた
      会社のノート PC でのローカルテストはすべてうまくいっていたのに、Linux ホストにデプロイすると、下位アプリケーションが CRLF を要求していて消費できなかった。たまに思い出さなければならない、些細で愚かな問題の一つ。ただし、新しく書かれたソフトウェアがなぜ特定の行終端を要求するのかも、もっともな疑問ではある
    • Windows で誰かがコードを書き始めると、この問題に何度も引っかかった
  • 不安定なシステムデフォルトに頼らないのは良いこと
    こうした値は、ある時点で自分の想定とは違うものを返しがち。数年前に Ubuntu と init.d スクリプトを扱っていたとき、Java を起動するスクリプトが root で実行されていた。Docker 以前だったのでなおさらだが、一般ユーザーには正常な UTF-8 のデフォルトを設定しないシェルで実行されていた。その結果、OS のデフォルトを使う Java の悪い API 利用が露呈した
    最近はほとんどの場合、エンコーディングを明示できる API のバリエーションがあり、静的コード検査ツールも間違ったものを使うと警告する。しかし一箇所でも抜けるとコンテンツが壊れ始める。今や UTF-8 ではないエンコーディングの使用は、ほとんどの場合意図しないものの可能性が非常に高く、意図しているなら OS の奇妙な間接設定に頼らず明示すべき。だから良い変更であり、これで壊れるコードには簡単な修正を入れるほうがよい

    • PowerShell でエイリアスとして作った touch 関数が生成した .gitignore を使っていたが、どうしても Git がそれを尊重しなかった
      確認してみると、生成されたテキストファイルがUTF-16だったため、実質的に無視されていた。教訓を得てシステムデフォルトを UTF-8 に変えたが、今は単にテキストエディタに頼っている
    • グローバルロケールは、エンコーディングに限らず全般的に失敗だった
      printf("%f", 4.2) が環境によって魔法のように別の文字列を出力するなら、解決より問題のほうが多くなる。ロケール依存の動作が欲しいときは、関数にロケール情報や関連する部分を明示的に渡すべき
  • この数十年でますます当てはまるようになったヒューリスティックがある。どこかに charset 設定があって、それがUTF-8 でなければ間違いというもの
    Python 2 は文字集合に無関心だったので常に動いたが、Python 3 の改善は単純な改善だけではなかった。Python 3 スクリプトと Python 2 スクリプトを見分ける方法はこうだ。文字列 utf-8 が入っていれば Python 3 で、C.UTF-8 ロケールでしか動かないなら Python 3。今回の変更は Python 3 を「修理」するもののように理解できるので歓迎する

  • Python 3 からデフォルトだと思っていた

    • おそらく Python 3 で u"" 接頭辞が不要になった文字列のことを思い浮かべているのだろう
      たった今 Python 2.7 で "éķů" を入力してみたところ、その文字の UTF-8 バイト列が出力されたので、u 接頭辞が正確に何をしていたのかはよく分からない。ただ、Python 2 から 3 への移行における大きな変化の一つは、文字列がエンコーディングを持ち、バイト文字列はエンコーディングのないバイト列になったこと。今回の変更は主に、Windows のようにデフォルトエンコーディングが UTF-8 ではない環境で open('filename', mode='r') を使う際に、open('filename', mode='r', encoding='UTF-8') と明示する必要があった問題に関するものに見える
    • Python 3 では、Python ソースコードはデフォルトで UTF-8。しかしファイルに保存するときに使う文字エンコーディングについては何も言ってくれず、デフォルトはロケール依存
      Path("filenames use their own encoding").write_text("file content encoding uses yet another encoding") のように、文字列リテラル、ファイル名、ファイル内容のエンコーディングはそれぞれ異なる。対応するエンコーディングは tokenize.open の UTF-8、os.fsencodesys.getfilesystemencoding()openlocale.getpreferredencoding()
  • 「Node.js、Go、Rust、Java を含む他の人気プログラミング言語も UTF-8 をデフォルトで使う」とは、Java がUTF-16 から UTF-8 に移ったことを見落としていた

    • Java でバイトを文字列に変換するときのデフォルトエンコーディングはもともとプラットフォーム依存で、今は UTF-8
      String クラス内部では UTF-16 と latin-1 エンコーディングが今も使われており、JVM は以前と同じく modified UTF-8 エンコーディングを使う。String クラスはもともと UTF-16 だけを使っていたが、Java 9 以降は可能な場合、1 文字あたり 1 バイトの latin-1 エンコーディングも使う
    • 内部の文字列表現と読み書きのエンコーディングを混同して話しているようだ
      Java は読み書きのエンコーディングのデフォルトとして UTF-16 を使ったことはない
    • Java 18 で 2 年前に変わったようだ
  • CPython の内部エンコーディングはいま UTF-8 なのか?
    Python の文字列は添字でインデックス指定できるが、ランダムアクセスは十分まれなので、必要なときに遅延インデックス化してもよさそう。1 つ前や後ろへ進むだけならインデックスは不要なので、内部表現を UTF-8 にすることも十分可能ではある

    • str を表現しているのは PyUnicode オブジェクト
      UTF-8 バイトが要求されると、必要に応じて bytes オブジェクトが生成され、PyUnicode の一部としてキャッシュされ、PyUnicode が解放されると一緒に解放される。別途、文字列を構成するコードポイントはランダムアクセスできるよう単純な配列に格納される。各コードポイントのサイズは 1、2、4 バイトになり得て、PyUnicode を作るときに最大コードポイント値を指定すると、127、255、65535、1,114,111 のいずれかに切り上げられ、1/2/4 バイトのどれを使うかが決まる
      最大コードポイント値が 127 なら、その配列表現を UTF-8 としてそのまま使える。なので質問への答えは、すべてのコードポイントが 127 以下である多くの文字列は UTF-8 として保存される、ということ。ただし文字列を走査するときにコードポイント単位で考えるべきではない。ユーザーが認識する文字、つまり書記素クラスタは 1 つ以上のコードポイントから成る。たとえばアクセント付きの e は e のコードポイントの後に結合アクセントのコードポイントが続くことがあり、フェニックスの絵文字は鳥の絵文字、ゼロ幅接合子、火の絵文字で構成される。何億人もの人が使う一部の文字体系も、子音に母音を表す結合記号が付く方式に近い。この - - はコードポイント 5 個であり、複数の言語がその「長さ」をどう報告するかを扱った良い記事がある: https://hsivonen.fi/string-length/。これは、この部分を扱う Unicode TR29 を Python C 拡張として実装してみた経験からの話
  • なぜ utf-8-sig ではないのか気になる。任意の BOM を処理してくれるし、先週もそれのせいでスクリプトを直す必要があった

    • いまや UTF-8 に BOM を入れるべきものは何もない
      推奨もされていないし、最近では BOM で失敗する挙動も妥当だと思う
    • Python がすべての入出力の前に見えない BOM を黙って付けるように変えるのは良い考えではない
  • UTF-8 の話なら、Linux フレームバッファはとっくにまともな UTF-8 サポートを備えているべきだった
    256/512 グリフのものではなく、本物のサポートのこと。GNU Hurd ですら 2007 年ごろから UTF-8 をサポートするより良い端末コンソールがあったのに、いまは 2024 年だ

  • 良い。あとは JS が UTF-8 に変えるだけ
    もちろん JS は改善できない。ほかのどのプログラミング言語とも違って、1995 年に書かれたコードとの互換性が必要だから

    • これは Python にファイルを「テキストとして」開くよう頼んだとき、デフォルトでどのエンコーディングを使うかの話
      文字列の内部表現は別問題で、JavaScript と同じく Python も内部で「単に UTF-8」を使っているわけではない
  • 「Unix を使う多くの Python 開発者は、デフォルトエンコーディングがプラットフォーム依存であることを忘れ、UTF-8 でエンコードされたテキストファイルを読むときに encoding="utf-8" を省略する」について、これは忘れたというより十分に知られていないのかもしれない
    正直、Python は明示的に別の指定をしない限り、どこでも UTF-8 だけを使うと思っていた

    • 実際には場合による
      bytes.decodestr.encode は、少なくとも Python 3 以降は UTF-8 をデフォルトにしてきた。一方、ファイル名をデコードするときのデフォルトエンコーディングは sys.getfilesystemencoding() を使い、Windows と macOS ではこれも UTF-8 だが、Linux ではロケール、具体的には CODESET によって変わる。最後に、openlocale.getencoding() を直接使う