何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

Pythonの文字列メソッドの罠

うわあ...これはUnicodeですね。

業務で、「与えられた文字列が半角英数で構成されているかを判定したい」という場面に遭遇した。 これはユーザーに何かを入力させる際にその文字列が想定しているものなのかを調べるケースの典型例である。 Pythonの場合、正規表現を書くまでもなく文字列のメソッドで判定ができる、と思っていた。 次のようなユニットテストを書いていた。

import unittest

class UtilsTest(unittest.TestCase):

    def test_is_alphanumeric(self):
        """与えられた文字列が半角英数で構成されているか"""
        cases_is_ok = ("12334567890", "abcxyz", "ABCXYZ", "abc123XYZ")
        cases_is_ng = ("3.141592653589793", "たまげたなあ", "0120ー022−022", "0790-62-0110")
        for case in cases_is_ok:
            with self.subTest(case=case):
                self.assertTrue(case.isalnum())
        for case in cases_is_ng:
            with self.subTest(case=case):
                self.assertFalse(case.isalnum())

余裕で通ると思っていたユニットテストに失敗してしまった。どうしてだろうか。

いつから「半角英数」の「英」がアルファベットだと勘違いしていた...?

まずは、str.isalnum()ドキュメントを調べてみよう。

str.isalnum() 文字列中の全ての文字が英数字で、かつ 1 文字以上あるなら真を、そうでなければ偽を返します。文字 c は以下のいずれかが True を返せば英数字です: c.isalpha() 、 c.isdecimal() 、 c.isdigit() 、 c.isnumeric() 。

なるほど。あやしいと踏んだstr.isalpha()ドキュメントを調べる。

文字列中の全ての文字が英字で、かつ 1 文字以上あるなら真を、そうでなければ偽を返します。英字は、Unicode 文字データベースで "Letter" として定義されているもので、すなわち、一般カテゴリプロパティ "Lm"、 "Lt"、 "Lu"、 "Ll"、 "Lo" のいずれかをもつものです。なお、これは Unicode 標準で定義されている "Alphabetic" プロパティとは異なるものです。

なんということだ、str.isalpha()の英字とはUnicode 文字データベースで "Letter" として定義されているものなのか。

>>> import unicodedata
>>> unicodedata.category("た")
'Lo'

ひらがなは "Lo" 、つまりLetterでOther Letterのカテゴリに属しているようである。 たまげたなあ。

対策

1つは正規表現を書くことである。

>>> import re
>>> re.match(r"^[0-9A-Za-z]+$", "たまげたなあ") is None
True

Python 3.7以降はstr.isascii() メソッドが追加されているので援用できる。

>>> "たまげたなあ".isalnum() and "たまげたなあ".isascii()
False

Unicodeに強くなりましょう。