先日、Rubyで文字列を指定数分だけ繰り返す処理を書いていた所、面白い現象に遭遇しました。
文字列を繰り返すために*
を使ったのですが「文字列 * 数値」の場合はエラーが発生せずに望み通りの値が返るものの、順序を逆にして「数値 * 文字列」とするとエラーが発生します。
puts "s" * 5 # sssss puts 5 * "s" # Main.rb:3:in `*': String can't be coerced into Integer (TypeError) # from Main.rb:3:in `<main>'
どちらの呼び出し方でもエラーが発生せずに使えると思っていたのですが、想定違いでした。
自分の認識がおかしいのかな...と思いPython3で試してみたところ、どちらの呼び出し方でもエラーになりませんでした。
print("s" * 5) # sssss print(5 * "s") # sssss
うーん、これはRuby固有の現象なのでしょうか。気になったので調べてみました。
※はてブの都合上、*
があると斜体として認識されてしまうため以降、*
をmultipleと表記します。
Stringのmultiple
self * times -> String
文字列の内容を times 回だけ繰り返した新しい文字列を作成して返します。[PARAM] times:
整数
String#* (Ruby 3.2 リファレンスマニュアル)
Stringクラスが提供しているmultipleは引数に整数を指定しています。
先ほど「文字列 * 数値」の順序で実行した時にエラーが発生しなかったのは指定されている引数の型を満たしていたからなんですね。逆を言えば、Integerクラスが提供しているmultipleは文字列型を期待していないためにエラーが発生したんだろうと推測できます。
ruby/string.cのコードを見てみると、RubyのVALUE型の値が引数に期待されていることが分かります。
ただし、NUM2LONG
はRubyのVALUE型オブジェクトをlong型に変換するためのものだそうで、数値に変換できない場合にエラーが発生します。
VALUE rb_str_times(VALUE str, VALUE times) { VALUE str2; long n, len; : /* ここでlong型に変換している */ len = NUM2LONG(times); if (len < 0) { rb_raise(rb_eArgError, "negative argument"); } : }
Integerのmultiple
self * other -> Numeric
算術演算子。積を計算します。[PARAM] other:
二項演算の右側の引数(対象)
Integer#* (Ruby 3.2 リファレンスマニュアル)
こちらには期待している値の情報が明記されていませんでした。
二項演算の右側の引数とありますが何のことでしょうか。コードを見るのが一番早いのでruby/numeric.cを見てみると、こちらも同様にRubyのVALUE型を引数に期待しています。Fixnum
とBignum
について今回は詳細には触れませんが、古いRubyのversionではIntegerはFixnum
とBignum
の二つに分かれていたそうですが、現在ではIntegerに統合されています。
VALUE rb_int_mul(VALUE x, VALUE y) { if (FIXNUM_P(x)) { return fix_mul(x, y); } else if (RB_BIGNUM_TYPE_P(x)) { return rb_big_mul(x, y); } return rb_num_coerce_bin(x, y, '*'); }
動作確認はしていませんが、Integerの場合にはFIXNUM_P(x)
が真となりfix_mul
が呼び出されます。
つまり、Integerのmultipleも数値に変換できないRubyのVALUE型の値を受け渡すことによって、エラーが発生していたということです。
Pythonでエラーにならないのはなぜ
おそらくmultipleの実装に違いがあるからというのが答えでしょう。
RubyではStringとIntegerのmultipleがそれぞれ定義されており、どちらも引数に数値へ変換可能なRubyのVALUE型を期待していました。Pythonでも同じような実装がされているものの、引数を受け取った後の処理に違いがあり「文字列 * 数値」と「数値 * 文字列」のどちらであっても、同じ結果が返るようになっているのでしょう。
...と結論を出してしまうのはもったいないので調べてみました。
Pythonではmultipleはマジックメソッド__mul__
を使用して実行されます。また__mul__
での実行に失敗した場合にはスワップに対応したマジックメソッドの__rmul__
が実行されるそうです。Pythonで演算子の順序を入れ替えてもエラーにならないのは、スワップして再度、トライする機構が備わっているからなんですね。
mul_ = (4).__mul__(5) rmul_ = (4).__rmul__(5) print(mul_) # 20 print(rmul_) # 20
3. Data model — Python 3.11.4 documentation