str.isdigit()
で浮動小数点数を判定できない
野球の人としてよく知られている@shinyorkeさんが興味深いツイートをしていた。
hoge = ‘.384’という変数がPythonにあったとして,
— Shinichi Nakagawa (@shinyorke) 2017年8月6日
float(hoge)
> 0.384
hoge.isdigit()
> False
っていう挙動をしてて,本来floatで処理できるハズのコがisdigitで弾かれるという
strとfloatの違い
恐らくC言語レベルで違う処理が行われていると思い、調べてみた。
float()
の挙動
組み込み函数float()
に文字列を渡すとfloat
型が返される。
渡せる文字列の規則はfloat
のドキュメント
及び浮動小数点数リテラルに記述されている。
打率を意味する".384"
という文字列は規則を満たす文字列である。
cpython/Objects/floatobject.c
は次のようになっている。
PyObject * PyFloat_FromString(PyObject *v) { const char *s; PyObject *s_buffer = NULL; Py_ssize_t len; Py_buffer view = {NULL, NULL}; PyObject *result = NULL; if (PyUnicode_Check(v)) { s_buffer = _PyUnicode_TransformDecimalAndSpaceToASCII(v); if (s_buffer == NULL) return NULL; s = PyUnicode_AsUTF8AndSize(s_buffer, &len); if (s == NULL) { Py_DECREF(s_buffer); return NULL; } } // 中略 else { PyErr_Format(PyExc_TypeError, "float() argument must be a string or a number, not '%.200s'", Py_TYPE(v)->tp_name); return NULL; } result = _Py_string_to_number_with_underscores(s, len, "float", v, v, float_from_string_inner); PyBuffer_Release(&view); Py_XDECREF(s_buffer); return result; }
詳細はもっとC言語のソースに踏み込まないとわからないが、_Py_string_to_number_with_underscores
で上手くドキュメントの記述通りに処理されていることが推測できる。
str.isdigit()
の挙動
str.isdigit()
のドキュメントから引用する。
文字列中の全ての文字が数字で、かつ 1 文字以上あるなら真を、そうでなければ偽を返します。 ここでの数字とは、十進数字に加えて、互換上付き数字のような特殊操作を必要とする数字を含みます。 また 10 を基数とした表現ができないカローシュティー数字のような体系の文字も含みます。 正式には、数字とは、プロパティ値 Numeric_Type=Digit または Numeric_Type=Decimal を持つ文字です。
この説明から".384"の小数点が小数点と認識されないからFalse
となってしまう、と予想できる。
cpython/Objects/unicodeobject.c
を調べると、str.isdigit()
は次のような実装である。
/*[clinic input] str.isdigit as unicode_isdigit Return True if the string is a digit string, False otherwise. A string is a digit string if all characters in the string are digits and there is at least one character in the string. [clinic start generated code]*/ static PyObject * unicode_isdigit_impl(PyObject *self) /*[clinic end generated code: output=10a6985311da6858 input=901116c31deeea4c]*/ { Py_ssize_t i, length; int kind; void *data; if (PyUnicode_READY(self) == -1) return NULL; length = PyUnicode_GET_LENGTH(self); kind = PyUnicode_KIND(self); data = PyUnicode_DATA(self); /* Shortcut for single character strings */ if (length == 1) { const Py_UCS4 ch = PyUnicode_READ(kind, data, 0); return PyBool_FromLong(Py_UNICODE_ISDIGIT(ch)); } /* Special case for empty strings */ if (length == 0) return PyBool_FromLong(0); for (i = 0; i < length; i++) { if (!Py_UNICODE_ISDIGIT(PyUnicode_READ(kind, data, i))) return PyBool_FromLong(0); } return PyBool_FromLong(1); }
Py_UNICODE_ISDIGIT
は恐らくヘッダーで定義されたマクロであろう、と推測した。
実際、cpython/Include/unicodeobject.h
に定義されていた。
#define Py_UNICODE_ISDIGIT(ch) _PyUnicode_IsDigit(ch)
実体は次のように実装されている。
typedef struct { /* These are either deltas to the character or offsets in _PyUnicode_ExtendedCase. */ const int upper; const int lower; const int title; /* Note if more flag space is needed, decimal and digit could be unified. */ const unsigned char decimal; const unsigned char digit; const unsigned short flags; } _PyUnicode_TypeRecord; #include "unicodetype_db.h" static const _PyUnicode_TypeRecord * gettyperecord(Py_UCS4 code) { int index; if (code >= 0x110000) index = 0; else { index = index1[(code>>SHIFT)]; index = index2[(index<<SHIFT)+(code&((1<<SHIFT)-1))]; } return &_PyUnicode_TypeRecords[index]; } /* Returns the integer digit (0-9) for Unicode characters having this property, -1 otherwise. */ int _PyUnicode_ToDigit(Py_UCS4 ch) { const _PyUnicode_TypeRecord *ctype = gettyperecord(ch); return (ctype->flags & DIGIT_MASK) ? ctype->digit : -1; } int _PyUnicode_IsDigit(Py_UCS4 ch) { if (_PyUnicode_ToDigit(ch) < 0) return 0; return 1; }
str.isdigit()
の場合、頭から文字を一つずつ調べてstr.isdigit()
のドキュメントにある「数字」であるかどうかを調べている。
ピリオド(U+002E)はここの意味で「数字」ではないので、".384".isdigit()
はFalse
を返してしまう。
そのため、"0.384"と頭に0を補っても意味がない。
例えば、
>>> "0.384".isdigit() False >>> "0.384".isdecimal() False >>> "0.384".isnumeric() False
指数表記でも同じ運命をたどる。
>>> "384e-3".isdigit() False >>> "384e-3".isdecimal() False >>> "384e-3".isnumeric() False >>> float("384e-3") 0.384
結論
str.isdigit()
で浮動小数点数を判定するのはダメゼッタイ。