Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Ruby TIPS

Ruby TIPS

代入により決まる変数のデータ型/丸め誤差が発生する浮動小数点数の比較― コーディングミスを防ぐには?(3)

2016年2月4日

初心者向けにRubyプログラミングの落とし穴を紹介。代入する値により変数のデータ型が決まることに関する注意点と、浮動小数点数の比較における丸め誤差の問題と回避方法について説明する。

ローグ・インターナショナル 羽山 博
  • このエントリーをはてなブックマークに追加

 Rubyには他のプログラミング言語にはない便利な機能が数多く備わっている。一方で意外な落とし穴もある。本稿ではデータ型についてのこまごまとした留意点を見ながら、気づきにくいミスを防ぐ方法を見ていく。

代入する値によって変数のデータ型が決まる

 Rubyでは、特に宣言しなくても、値を代入すれば変数が宣言されたものと見なされる。変数のデータ型は代入された値によって決まるが、リスト1.1に示すように、異なるデータ型の値を代入すると変数のデータ型も変わる。

sample001.rb(前半)
a = 10     # 整数を代入
p a
a = "foo"  # 文字列(への参照)を代入
p a
リスト1.1 変数にデータを代入する

この例では10という整数を代入した時点で変数aが宣言されたものと見なされる。変数aのデータ型は整数(Integer)型となるが、続いて、"foo"という文字列への参照を代入すると、文字列型となる。実行例はリスト1.2と合わせて後で示す。

 変数のデータ型が事前に決まっていないので、演算を行う場合に注意が必要になる。整数と浮動小数点数などの場合は自動的にデータ型が変換されるが、数値と文字列を加算するなど、データ型の変換ができない場合はエラーになるので、そういった演算をうっかり行わないように注意する必要がある。リスト1.1のコードに引き続き、以下のコードを実行したものとしよう。

sample001.rb(後半)
a = a + "1"  # 文字列+文字列は文字列の連結になる
p a
a = a + 1    # 文字列+整数はエラーになる
p a
リスト1.2 異なるデータ型の値を使って演算する

変数aにはすでに"foo"という文字列への参照が代入されているものとする。このとき、+は文字列の連結を行う演算子と見なされるので、文字列+整数という演算はエラーになる。

 リスト1.1とリスト1.2の実行例は以下の通り。変数a"foo"という文字列への参照が入っているので、文字列同士の演算はできるが、文字列と整数の演算を行おうとするとエラーになる。なお、リスト1.2がリスト1.1の続きでない場合、つまり、変数aに何も代入されていない状態で、a = a + "1"を実行した場合は、右辺がnil + "1"という演算になるので、これもエラーとなる。

コンソール
$ ruby sample001.rb
10
"foo"
"foo1"
sample001b.rb:7:in `+': no implicit conversion of Fixnum into String (TypeError)
  from sample001b.rb:7:in `<main>'
実行例1.2 リスト1.1とリスト1.2の実行例

変数のデータ型は代入された値のデータ型によって決められる。演算の場合はデータ型が異なると変換が行われず、エラーになることもある。

 いくら何でも、そんな初歩的な間違いは犯さないと思われるかもしれないが、シンプルな例で説明したのでそう思うだけであって、こういうエラーは意外に多い。例えば、以下のようなコードをうっかり書いてしまうこともあるかもしれない。

sample002a.rb
limit = ARGV[0]        # コマンドの引数を取得
p Random.rand(limit)   # 0以上、limit未満の乱数を作る
リスト1.3 数値を指定すべき引数に文字列を指定してしまった例

ARGVはRubyスクリプトが実行された際のコマンド引数の配列。文字列の配列であることを忘れて、数値を指定すべきメソッド引数にそのまま指定してしまうと、エラーになる。

 このプログラムは、コマンドの引数で指定した値未満の乱数を作るという意図で作ったものだが、実行してみると以下のようなエラーが表示される。

コンソール
$ ruby sample002a.rb 10
sample002a.rb:2:in `rand': no implicit conversion of String into Integer (TypeError)
  from sample002a.rb:2:in `<main>'
実行例1.3 数値を指定すべき引数に文字列を指定してしまった

10というコマンド引数を指定して実行してみた。このとき、ARGV[0]"10"という文字列への参照が入れられている。しかし、Randomクラスのrandメソッドの引数には数値を指定する必要があるので、エラーとなった。

 このプログラムを正しく動作させるには、to_iメソッドを使って引数の文字列を整数に変換すればよい(リスト1.4)。to_iメソッドは文字列に含まれる数字を数値に変換する。ただし、数値に変換できない文字が現れた場合にはそこで変換をやめる。文字列が数値に変換できない文字で始まっている場合には0を返す。

sample002b.rb
limit = ARGV[0].to_i                  # コマンドの引数を取得
p limit<=0 ? 0 : Random.rand(limit)   # 0以上、limit未満の乱数を作る
リスト1.4 文字列を数値に変換して引数に指定した例

文字列(String)クラスのto_iメソッドを利用すると、文字列に含まれる数字を数値に変換できる。数値に変換できる文字がない場合には0が返される。

 randメソッドは、0以下の値を指定するとエラーになるので、そのチェックも含めておいた。さまざまな引数を指定して実行してみるといい。

コンソール
$ ruby sample002b.rb 10
6
$ ruby sample002b.rb 10x
9
$ ruby sample002b.rb 0
0
$ ruby sample002b.rb -10
0
$ ruby sample002b.rb abc
0
実行例1.4 to_iメソッドにより引数の文字列を整数に変換して利用する

to_iメソッドの働きにより、"10""10x"10という整数に変換され、その値未満の乱数が求められる。"0""-10"も整数に変換され、"abc"0に変換されるが、条件演算子の働きにより、0が出力される。

 なお、小数の乱数を作成したいときには、to_iメソッドの代わりにto_fメソッドを使えばよい。0.0以上で、randメソッドの引数に指定された値未満の、小数の乱数が作成できる。

【コラム】変数と定数の種類

 Rubyの変数は、先頭の1文字によって種類が決まる。以下の表にまとめておこう。

先頭の1文字 変数の種類 備考・スコープなど
小文字または_(アンダースコア) ローカル変数 count、x、_num ブロック内で利用できる
$ グローバル変数 $limit、$company どこからでも利用できる
@ インスタンス変数 @height、@name クラス内やサブクラス内でインスタンスごとに利用できる
@@ クラス変数 @@year、@start クラス内やサブクラス内で共有される
大文字 定数 Pi、Discount 初期値として設定した値を変更できない
表1.1 変数と定数の種類は先頭の1文字によって決まる

浮動小数点数の比較では期待した結果にならないことがある

 Rubyに限った話ではないが、浮動小数点数には誤差が含まれることがあるので、比較演算子を使って大小の比較や等しいかどうかの比較をする場合には注意が必要だ。Rubyの浮動小数点数はIEEE 754に準拠した表現で、多くの場合、64bitで表される(いわゆる倍精度)。この場合、仮数部が52bitとなる。従って、10進数での有効桁数はlog102(52+1)≒15.95となる。つまり、せいぜい15桁の精度となっている。

 例えば、0.3を小数点以下20桁程度表示してそのことを確認してみよう。

sample003.rb
printf("%22.20f\n",0.3)
リスト1.5 精度以下の桁数まで表示する

printfメソッドを使って0.3を小数点以下20桁まで表示してみる。printfメソッドの最初の引数には%に続けて書式文字列が指定できる。例えば、%22.20fなら、全体が22桁、小数点以下が20桁、浮動小数点数の形式ということになる。書式文字列中のfが浮動小数点数を表す。なお、10進整数として表示したい場合は全体の桁数とdを指定する。

 では、実行してみよう。結果は以下の通り。0.3を出力したはずなのだが、0.3よりもわずかに小さい値になった。

コンソール
$ ruby sample003.rb
0.29999999999999998890
実行例1.5 浮動小数点数に誤差が含まれていることを確認する

0.3を浮動小数点数として表すと、0.3よりわずかに小さい値になる。

【コラム】丸め誤差とは

 上で見たような誤差は丸め誤差と呼ばれる。なぜこのような誤差が生じるかは浮動小数点数の仕組みから見ると簡単に分かる。以下の図はIEEE 754の浮動小数点表現(64bitの場合)である。

図1.1 IEEE754での浮動小数点数の表現(64bitの場合)

IEEE 754では、小数を1.xxxxx... × 2nの形式で表し、指数部の11bitに「n+1023」の2進数表現を入れ、仮数部52bitに「xxxxx...」の部分を入れる。

 浮動小数点数では、小数点付きの値を何とかこの枠内に収めて表現する必要がある。ところが、小数を2進数に変換すると循環小数になってしまうことがある。例えば、0.3を2進数に変換すると、0.01001100110011... となり、0011というパターンが無限に繰り返すことになってしまう。となると、どこかで計算をあきらめるしかない。そこで、有効桁数以下を丸める(通常は0捨1入する)。すると、0.01001100110011...0011となり、それ以下の桁が切り捨てられてしまう(この場合は最後の0011の次が0なので、切り捨てとなった)。そういうわけで、0.3の場合は0.3よりごくわずかに小さな値になる。当然のことながら、値によっては繰り上がることもある。その場合は、元の値よりわずかに大きな値になってしまう(リスト1.5の値を0.2にして実行してみよう)。なお、循環小数にならない場合は(例えば0.25では)丸め誤差は発生しない。

 前置きが少し長くなったが、浮動小数点数はあくまで「近似値」でしかないので、単純に比較すると期待した結果にならないことがある(ならないことの方が多い)。すでに誤差が出ることを実際に見たので、容易に想像できるだろう。こちらも簡単な例で見てみよう。

sample004.rb
printf("%18.17f\n",0.25)
printf("%18.17f\n",0.5)
p (0.25 * 2 == 0.5)? true: false
printf("%18.17f\n",0.1)
printf("%18.17f\n",0.3)
p (0.1 * 3 == 0.3)? true: false
リスト1.6 浮動小数点数の比較

日常の感覚(10進数)では、0.25 * 20.5は等しく、0.1 * 30.3は等しいので、いずれもtrueと表示されるはずだが、丸め誤差のせいで期待した結果にならない。どういう結果が表示されるか、予想してみよう。

 実行してみよう。予想した結果と一致しただろうか。

コンソール
$ ruby sample004.rb
0.25000000000000000
0.50000000000000000
true
0.10000000000000001
0.29999999999999999
false
実行例1.6 等しいはずの比較が等しくならない

0.25は2進数で表しても循環小数にならないので、上の例では丸め誤差が出ない。従って、等しいという結果になる。

しかし、0.10.3も2進数で表すと循環小数になる。しかも、0.1はわずかに0.1より大きく、0.3はわずかに0.3より小さいので、等しくないという結果になる。

 リスト1.6の比較を正しく行うには、実用的に問題のない範囲で誤差を許容するように小数点以下を四捨五入してやればよい。例えば、小数点以下2桁まで一致すれば一致したものと見なすのであれば、roundメソッドの引数に2を指定する。

sample005.rb
a = (0.1 * 3).round(2)
b = 0.3.round(2)
p (a == b)? true: false
リスト1.7 小数点以下2桁まで求めるように四捨五入する

roundメソッドに小数点以下の桁数を指定すれば、その桁までが求められるように下位の桁を四捨五入する。なお、引数を指定しない場合や0を指定した場合は1の位まで求め、-1を指定すれば10の位まで、-2を指定すれば100の位までといった具合に四捨五入する(指定した引数がnであれば、10-nの位が求められるように四捨五入される)。

 リスト1.7を実行すると以下のようになり、等しいと判定される。なお、桁数の21516に変えて試してみるとどうなるか確認してみるといい(やってみてのお楽しみということにするので、結果はあえて掲載しないことにしよう)。

コンソール
$ ruby sample005.rb
true
実行例1.7 許容する誤差の範囲を決めておけば、正しく比較できる

今度は等しいと見なされ、結果がtrueになった。

 数値計算では、ここで見た丸め誤差の他、桁落ち誤差情報落ち誤差などの誤差を考慮する必要がある。また、四捨五入切り上げ切り捨てなどの丸めの方法についても注意点がいくつかあるが、それらについてはまたの機会に紹介することとしよう。

まとめ

 Rubyの変数は最初に代入された時点で宣言されたものと見なされる。また、代入する値によって変数のデータ型が決まる。演算の際には、演算ができるようにデータ型が自動的に変換される。ただし、文字列と整数の加算などのように変換ができない場合はエラーとなる。浮動小数点数は、小数を近似値として表すので、循環小数になる場合には丸め誤差が生じる。

処理対象:変数|ローカル変数|グローバル変数|インスタンス変数|クラス変数|定数 カテゴリ:文法 > 変数と定数
処理対象:型|型の変換 カテゴリ:文法
API:to_iメソッド|to_fメソッド カテゴリ:Stringクラス
処理対象:リテラル|数値リテラル|浮動小数点数 カテゴリ:文法 > リテラル
API:書式文字列 カテゴリ:その他
API:printf カテゴリ:Kernelモジュール

※以下では、本稿の前後を合わせて5回分(第3回~第7回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Ruby TIPS
3. 関数で複数の返り値を返す/関数の引数は値渡し ― コーディングミスを防ぐには?(2)

Rubyプログラミングでミスしやすい意外な落とし穴を紹介。関数を使って複数の値を返す方法と、引数による値の受け渡しに関するポイントを説明する。値渡しの関連として、非破壊的な変更と破壊的な変更についても取り上げる。

Ruby TIPS
4. Rubyをインストール/アップデートするには?(Windows編)

Windows上でのRubyプログラミングを始める入門者向けに、Ruby環境の構築方法、複数バージョンのインストール方法、バージョンのアップデート方法を説明する。

Ruby TIPS
5. 【現在、表示中】≫ 代入により決まる変数のデータ型/丸め誤差が発生する浮動小数点数の比較― コーディングミスを防ぐには?(3)

初心者向けにRubyプログラミングの落とし穴を紹介。代入する値により変数のデータ型が決まることに関する注意点と、浮動小数点数の比較における丸め誤差の問題と回避方法について説明する。

Ruby TIPS
6. 桁区切り数値の記述と出力/文字列を逆順に/文字列の分割/文字列の各文字の利用 ― コーディングミスを防ぐには?(4)

初心者向けにRubyプログラミングの落とし穴を紹介。桁区切り指定で数値リテラルを記述する方法や、桁区切りの数値文字列を出力する方法、文字列を逆順に並べ替える方法、文字列を文字列で分割する方法、文字列を1文字ずつ扱う方法を説明する。

Ruby TIPS
7. 複素数(Complexクラス)を活用するには?

組み込みライブラリに含まれるComplexクラスによる基本的な複素数の取り扱い方、複素数の四則演算、平面上の点(ベクトル)の操作方法を説明する。

サイトからのお知らせ

Twitterでつぶやこう!