この記事は 2024 TSG Advent Calendar 初日の記事です。
…………🤔❓
Python の数値文字列判定ロジックを探る
str.isnumeric() メソッドとは?
Python の str.isnumeric()
メソッドは、文字列内のすべての文字が数値を表すものであれば True を、そうでなければ False を返します。*1 まず、このメソッドの基本的な動作を見てみましょう。
# Python の isnumeric() の動作例 print("123".isnumeric()) # True; 1, 2, 3 は数字 print("123a".isnumeric()) # False; a は数字ではない print("五千万".isnumeric()) # True; 五、千、万 は数値
漢数字にも対応しており、一見よさそうに見えます。ところが……
print("兆".isnumeric()) # True print("垓".isnumeric()) # False
「兆」と「垓」はどちらも漢数字に使われる文字なのに、なぜ Python の str.isnumeric() は異なる結果を返すのでしょうか?
Python と Unicode 文字データベース
Python の文字列メソッド str.isnumeric()
は、内部的に Unicode の情報を活用しています。Unicode とは、世界中の文字を一元管理するための国際的な規格で、それぞれの文字がどんな性質を持つのかを「Unicode 文字データベース (UCD)」1 という形で記録しています。データベースには「絵文字かどうか (Emoji)」や「大文字かどうか (Uppercase)」といった属性が文字ごとに記録されています。
その中で、この記事で鍵になる情報は Numeric_Type プロパティ です。このプロパティは、文字が「数値」として扱われるかを示したもので、Python の str.isnumeric()
もこの情報を元に判断を行っています。
Numeric_Type プロパティには以下の 4 種類があります。str.isnumeric()
の正式な仕様2 には、文字がこのうち Decimal, Digit, Numeric のいずれかである場合に True を返す、というふうに定めてあります。
Numeric_Type | 説明 | 例 |
---|---|---|
Decimal | 十進法 (0~9) で使われる基本的な数字 | U+0030~U+0039(アラビア数字 "0"~"9") U+0660~U+0669(アラビア語の数字) |
Digit | 数字として機能するが、十進法の桁として直接使用されない特殊な数字 | U+2070(上付きゼロ "⁰") U+2460(丸付き数字 "①") |
Numeric | それ以外の数値を表す文字。整数や分数、負の数など幅広い数値が含まれる | U+2155(1/5の分数 "⅕") U+4E00(漢数字 "一") |
None | 上記に該当しない文字 | U+0041(ラテン文字 "A") U+3042(ひらがな "あ") |
実際に数詞の Numeric_Type を見てみる
では、これをふまえて、「兆」「垓」を含む日本語の数詞の Numeric_Type をまとめてデータベース3 で確認してみましょう。
文字 | Unicode | Numeric_Type | str.isnumeric() |
---|---|---|---|
万 | U+4E07 | Numeric | True |
億 | U+5104 | Numeric | True |
兆 | U+5146 | Numeric | True |
京 | U+4EAC | Numeric | True |
垓 | U+5793 | None | False |
𥝱 | U+25771 | None | False |
穣 | U+7A63 | None | False |
溝 | U+6E9D | None | False |
澗 | U+6F97 | None | False |
正 | U+6B63 | None | False |
載 | U+8F09 | None | False |
極 | U+6975 | None | False |
すると確かに、「兆」は Numeric_Type=Numeric に分類されている一方、「垓」は分類されていないことがわかります。これが、Python の str.isnumeric() メソッドが「兆」と「垓」で異なる結果を返すことへの直接的な理由です!
……しかしながら、上の表を見ていると、新たに一つの疑問が沸き上がってきます。なぜ、Unicode は「垓」以上の数詞に Numeric_Type を割り当てていないのでしょうか?この謎を解くため、Unicode の数詞の扱いについてもう少し掘り下げていきます。
「京」と「垓」を分けた Unicode の判断を探る
現行の Unicode は「京」までの数詞に Numeric_Type を付与しているのにも関わらず、「垓」以上の数詞には付与していません。この線引きは、どのような基準で決まったのでしょうか? それを知るためにも、まずは Unicode における漢字と Numeric_Type の関係を知っておく必要があります。
漢字の Numeric_Type を決める仕組み
Unicode の漢字(CJK 統合漢字)に関する情報は、UCD とは別の Unicode Han Database (Unihan)5 というデータベースに収録されています。このデータベースには、各漢字の読みや意味、部首だけでなく、数値に関する情報も格納されています。
Unihan には kPrimaryNumeric という項目があり、ここにはその漢字が表す数値(たとえば「万」は104)が定義されています。 ある漢字に kPrimaryNumeric が付与されると、それに対応して UCD で Numeric_Type=Numeric が設定される、つまりその漢字が 「数を表す文字」として扱われる仕組みになっています。
「京」と「垓」の境界線:L2/22-223 提案と Unicode 技術委員会の判断
つい最近の Unicode 15.0(2022 年 9 月制定)まで、「万」「億」「兆」までが Numeric_Type を持つ数詞として扱われ、「京」「垓」以降の数詞は Numeric_Type が付与されていませんでした。
ここでターニングポイントとなるのが、2022 年 10 月に Unicode 技術委員会 (UTC) に提出された L2/22-2236 という提案です。L2/22-223 は、Unihan の数値フィールドに関する様々な修正を提案しています。
特にその中で、「京」以上の数詞(京、垓、𥝱、穣、溝、澗)などの文字 にも日本語で数値としての用例があることから、kPrimaryNumeric プロパティを追加する、すなわち新たに Numeric_Type を付与することを提案しているのです! 下の表の太字は、実際に L2/22-223 で追加が提案された kPrimaryNumeric の値です。
文字 | Unicode | kPrimaryNumeric(提案は太字) |
---|---|---|
兆 | U+5146 | 1012, 106 *2 |
京 | U+4EAC | 1016 |
垓 | U+5793 | 1020 |
𥝱 | U+25771 | 1024 |
穣 | U+7A63 | 1028 |
溝 | U+6E9D | 1032 |
澗 | U+6F97 | 1036 |
そして、この L2/22-223 提案をとりまとめた UTC の CJK & Unihan 作業部会も、UTC に対して上表のすべてを受け入れるよう勧告しました7。
しかしながら、最終的な UTC での議論の結果8、「兆」の 106 と「京」の 1016 のみが Unicode 15.1 に追加され、他の数詞への kPrimaryNumeric 付与は見送られることとなりました。この決定により、「京」(U+4EAC) が数値として扱われる最後の文字として認められ、それ以上の数詞(垓、𥝱など)は対象外となってしまいました。
[173-A45] Action Item for John Jenkins, CJK: Apply the adjustments to the kAccountingNumeric, kOtherNumeric, and kPrimaryNumeric properties, based on document L2/22-223, as amended in Section 11 of document L2/22-247, and excluding any property values greater than that for U+4EAC, for Unicode Version 15.1.
結局、なぜ「京」だけが追加されたのか?
「京」までが採用された顛末について、公開資料から読み取れるのはここまでです。しかし、これらの記録を読んでも、具体的に「なぜ『垓』以降が除外されたのか?」という理由は記されていません。
そこで、この疑問を解消するために、Unicode の専門家であり、先ほどの勧告を行った CJK & Unihan 作業部会の議長でもある Ken Lunde 博士に直接問い合わせを行いました。氏の返信によれば、「京」(10000000000000000) より上の数詞が除外された理由は、オーバーフローの懸念にあるとのことです。以下は博士からの返信の引用です:
See Consensus 173-C11 from #UTC173, along with the associated action items, specifically 173-A45. Summary: Any value over 10000000000000000 was explicitly excluded due to overflow concerns. https://t.co/4mlIljLhhl
— Ken Lunde 小林剣 (@ken_lunde) 2024年11月16日
このような判断が行われた背景には、Unicode が幅広いシステムや環境で採用されていることが影響しています。オーバーフローのリスクを抱える値を Numeric_Type に含めると、数値処理を行う多くの実装に影響を及ぼす可能性がある、という判断がなされたと考えることができます。
実際、64 bit 符号なし整数型が表現できる 0~264 -1(およそ 1.8×1019)の上界は「京」(1016) と「垓」(1020) の間に位置します。さらに、一部のシステムでは数値を倍精度浮動小数点数型で表現しますが、この型で安全に整数を表現できる上限 *3 は約 9×1015 です。この範囲を超える「京」(1016) が Numeric_Type を持つのは、この制約を直接反映したものではないかもしれませんが、それでも比較的小さな値として許容された可能性はあります。
あくまで推測ですが、128 bit 整数や任意精度演算が一般的になるような環境が普及すれば、「垓」以降の数詞にも Numeric_Type が付与される可能性があるかもしれません。たとえば、128 bit 符号なし整数を使えば、およそ 3.4×1038 までの値を安全に表現できます。この範囲なら「澗」(1036) も含めることができます。Unicode における数詞の扱いも変わっていくには、すなわち Python の str.isnumeric()
が「垓」以降の数詞を数値として扱うようになるには、さらなる計算機環境の進化を待つ必要があるかもしれません。
まとめ
Python の str.isnumeric()
メソッドは、文字列内のすべての文字が「数を表す文字」であるかを判定しますが、"兆"
には True、"垓"
には False を返します。この理由は、Unicode の Numeric_Type プロパティが「垓」以上の数詞には付与されていないことにありました。
さらにその理由を探ると、「垓」~「澗」に対する Unicode の数値フィールド修正の提案があったものの、オーバーフローのリスクを懸念して「京」までの数詞のみが採用されたことがわかりました。
Unicode のプロパティ設定には文字の意味以上に、技術的制約など多様な観点から仕様が検討されているのです。
謝辞
本記事の執筆にあたり、Ken Lunde 博士から貴重な情報提供をいただきました。特に、Unicode の数値扱いに関する技術的な背景についての理解を深めることができましたことに感謝しています。また、Unicode 議事録のサーベイなどに、幅広く助力していただいた hakatashi 氏にもここに謝意を表します。
ゴママヨコーナー
気軽に読める記事を目指していたのに、すごくまじめな内容になってしまったので、ゴママヨコーナーで中和したいと思います。
- 「垓」以上 ←⁉
- Numeric_Type プロパティ ←⁉
他に見つけたらぜひ教えてください。
- Unicode® Standard Annex #44 - Unicode Character Database、参照日: 2024-11-17↩
- Python documentation - Built-in Types、参照日: 2024-11-17↩
- Unicode Character Database - Numeric_Type、参照日: 2024-11-17↩
- What's New in Python 3.13 - unicodedata、参照日: 2024-11-17↩
- Unicode® Standard Annex #38 - Unicode Han Database (Unihan)、参照日: 2024-11-17↩
- L2/22-223: Proposed Updates and Expansions of Unihan Numeric Fields、参照日: 2024-11-17↩
- L2/22-247: CJK & Unihan Group Recommendations for UTC #173 Meeting、参照日: 2024-11-17↩
- UTC #173 Minutes、参照日: 2024-11-17↩
*1:つまり、文字列全体が数の表現として正しいかとは無関係です。たとえば、"123.45".isnumeric() は False、"兆兆".isnumeric() は True を返します。
*2:中国やベトナムで 106 を「兆」と書く慣習があり、現在でも中国本土で SI 接頭辞 106 を表す文字として「兆」を使うようです。
*3:JavaScript の Number.MAX_SAFE_INTEGER のことだと思ってください。