Python の isnumeric() の謎を追う:"兆" は True、"垓" は False?

この記事は 2024 TSG Advent Calendar 初日の記事です。

…………🤔❓

Python の数値文字列判定ロジックを探る

str.isnumeric() メソッドとは?

Pythonstr.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() は異なる結果を返すのでしょうか?

PythonUnicode 文字データベース

Python の文字列メソッド str.isnumeric() は、内部的に Unicode の情報を活用しています。Unicode とは、世界中の文字を一元管理するための国際的な規格で、それぞれの文字がどんな性質を持つのかを「Unicode 文字データベース (UCD)」1 という形で記録しています。データベースには「絵文字かどうか (Emoji)」や「大文字かどうか (Uppercase)」といった属性が文字ごとに記録されています。

その中で、この記事で鍵になる情報は Numeric_Type プロパティ です。このプロパティは、文字が「数値」として扱われるかを示したもので、Pythonstr.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 の数詞の扱いについてもう少し掘り下げていきます。

Coffee break ☕ 「京」の扱いが変わった? すこし面白いのは「京」の扱いです。「京」(U+4EAC) は、Unicode 15.1 で Numeric_Type が None から Numeric に変更されました。この変更が Python 3.13 から取り入れられたため4、これ以降のバージョンで "京".isnumeric() が True を返すようになりました。

# Python 3.12 以前なら False
# Python 3.13 以降なら True
print("京".isnumeric())

「京」と「垓」を分けた 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 が設定される、つまりその漢字が 「数を表す文字」として扱われる仕組みになっています。

Coffee break ☕ Unihan と Numeric_Type 漢字に Numeric_Type を付与する仕組みは、実際はもう少しいろいろあります。Unihan には kPrimaryNumeric の他にも

  • kAccountingNumeric: 領収書などで使われる「壱」「弐」「参」などの文字
  • kOtherNumeric: 数字として扱うのが一般的でない「幺」「㠪」などの文字

などの数値に関するフィールドがいくつかあり、これらのどれかが設定されている場合に 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) より上の数詞が除外された理由は、オーバーフローの懸念にあるとのことです。以下は博士からの返信の引用です:

このような判断が行われた背景には、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 における数詞の扱いも変わっていくには、すなわち Pythonstr.isnumeric() が「垓」以降の数詞を数値として扱うようになるには、さらなる計算機環境の進化を待つ必要があるかもしれません。

まとめ

Pythonstr.isnumeric() メソッドは、文字列内のすべての文字が「数を表す文字」であるかを判定しますが、"兆" には True、"垓" には False を返します。この理由は、Unicode の Numeric_Type プロパティが「垓」以上の数詞には付与されていないことにありました。

さらにその理由を探ると、「垓」~「澗」に対する Unicode の数値フィールド修正の提案があったものの、オーバーフローのリスクを懸念して「京」までの数詞のみが採用されたことがわかりました。

Unicode のプロパティ設定には文字の意味以上に、技術的制約など多様な観点から仕様が検討されているのです。

謝辞

本記事の執筆にあたり、Ken Lunde 博士から貴重な情報提供をいただきました。特に、Unicode の数値扱いに関する技術的な背景についての理解を深めることができましたことに感謝しています。また、Unicode 議事録のサーベイなどに、幅広く助力していただいた hakatashi 氏にもここに謝意を表します。

ゴママヨコーナー

気軽に読める記事を目指していたのに、すごくまじめな内容になってしまったので、ゴママヨコーナーで中和したいと思います。

  • 「垓」以上 ←⁉
  • Numeric_Type プロパティ ←⁉

他に見つけたらぜひ教えてください。

*1:つまり、文字列全体が数の表現として正しいかとは無関係です。たとえば、"123.45".isnumeric() は False、"兆兆".isnumeric() は True を返します。

*2:中国やベトナムで 106 を「兆」と書く慣習があり、現在でも中国本土で SI 接頭辞 106 を表す文字として「兆」を使うようです。

*3:JavaScript の Number.MAX_SAFE_INTEGER のことだと思ってください。