str.isdigit()
で浮動小数点数を判定できない
野球の人としてよく知られている@shinyorkeさんが興味深いツイートをしていた。
恐らく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()
は次のような実装である。
static PyObject *
unicode_isdigit_impl(PyObject *self)
{
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);
if (length == 1) {
const Py_UCS4 ch = PyUnicode_READ(kind, data, 0);
return PyBool_FromLong(Py_UNICODE_ISDIGIT(ch));
}
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 {
const int upper;
const int lower;
const int title;
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];
}
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()
で浮動小数点数を判定するのはダメゼッタイ。