2008年9月5日金曜日

Python で文字列が数値に変換できるか検査する

1. isdigit 関数は文字列のメソッド

文字列が数値であるか検査するとき、

isdigit

関数を利用する。

気をつけることは、このメソッドが文字列のメソッドであること。

2.3.6.1 文字列メソッド によると、

isdigit()

文字列中に数字しかない場合には真を返し、その他の場合は偽を返します。

試してみる。

tests = ["100", "100.1", "",
            "hoge", "10hoge", "hoge10", "ho10ge",
            100]

for i,e in enumerate(tests):
    try:
        print e.isdigit()
    except Exception, e:
        print e, ':', "[%d]" % i, tests[i]

結果は、
True
False
False
False
False
False
False
'int' object has no attribute 'isdigit' : [7] 100

数値 (int 型) は、文字列ではないので、例外が投げられる。

 

isdigit にまつわる失敗談

フォームの内容を受けとるオブジェクトがあり、int 型を保持するインスタンス変数を作成した。

フォームから値を受け取った後、内容を検証する前に、一時的に文字列としてそのインスタンス変数に設定した。その後、フォーマットを検証し、妥当であれば int 型に変換し、一時的に文字列を代入していた変数に再代入する処理をしていた。

もし、 Java のような強い型付けの言語で、同様のコードを書いたら、コンパイルエラーとなる。それに対して、Python のように動的な型付けのスクリプト系の言語は、変数の使い回しができてしまう。

上記のようなコード書いた後、当該のインスタンス変数に int 型を設定する別の処理を追加し、その処理を当該クラスのコンストラクタから呼出した。これにより、フォーマットの検証でエラーが表示された。なぜなら、int 型に変換した後のインスタンス変数に対して isdigit() を適用したから。

同じようなことが「Ruby でも型チェック (Codelogy)」に書かれている。

例えば、Ruby でプログラムを書いていて、次のようなバグに悩まされたことのある人は多いのではないでしょうか。

  • Integer オブジェクトを参照しているべき変数が、他の型のオブジェクトを参照している。
  • そのオブジェクトが「いつ」「どこで」代入されたものなのか分からない。

この手のバグは、問題の発生 (不正な型の代入) と発覚 (エラーの発生) の位置が離れてしまうので、非常に厄介。 発生箇所を絞り込むのが難しいため、プログラムを広範囲に渡って見直すハメになります。

「問題の発生と発覚の位置が離れていた」ので、当初エラーの原因が何なのかわからなかった。 (+_+)

ところで、Ruby には String クラスに isdigit() に相当するメソッドがない。Ruby では kind_of?() を使って型の検証をする。

それに相当するものは、Pyhton では、3.6 types -- 組み込み型の名前, 組み込み関数の isinstance( object, classinfo) ということか。 (@_@)

 

2. int()

ちなみに  int 型に変換する int() を、上記のリストに適用してみる。

for i,e in enumerate(tests):
    try:
        print int(e)
    except Exception, e:
        print e, ':', "[%d]" % i, tests[i]
        
print type(100)

結果は、

100
invalid literal for int() with base 10: '100.1' : [1] 100.1
invalid literal for int() with base 10: '' : [2] 
invalid literal for int() with base 10: 'hoge' : [3] hoge
invalid literal for int() with base 10: '10hoge' : [4] 10hoge
invalid literal for int() with base 10: 'hoge10' : [5] hoge10
invalid literal for int() with base 10: 'ho10ge' : [6] ho10ge
100

こちらは、文字列・数値なら OK 。ただし、小数もはじくとは結構厳しい。 (@_@;)

 

Ruby の to_i

Ruby で int() に相当する to_i は、動作が微妙に違う。

p "10".to_i
p "10.1".to_i
p "10hoge".to_i
p "ho10ge".to_i
p "hoge10".to_i
p "".to_i
p 100.to_i

結果は、

10
10
10
0
0
0
100

Ruby には、String、 Integer クラスに to_i があるから、上記のような結果になる。できるだけ数値にできるものは数値にするという意気込みが感じられる。 しかし考えてみると、上記の空文字に対する to_i と以下の

p "0".to_i 
p 0.to_i

とした場合、すべて 0 となってしまい元の値が何だかわからなくなってしまう。 (+_+) こういう場合は、必要であれば kind_of? で検査、内容のチェックをするのかな。

to_iとInteger - こげこげ堂はてな支舗 では、

数値に変換できるかどうかで処理を分けたいならキャストして例外を発生させたほうが扱いやすいと思います。

なるほど。

p Integer(0)
p Integer("0")
p Integer("")

とすると、

0
0
digittest.rb:19:in `Integer': invalid value for Integer: "" (ArgumentError)
 from digittest.rb:19