Pythonのリストのスライスに同等のメソッドをRubyで実装

Python にはリスト(Ruby の配列)のスライスと呼ばれる操作があります。

>>> a = [0, 1, 2, 3]
>>> a[0 : 2]
[0, 1]

みたいなやつです。Python のお勉強に、これと同等のメソッドRuby で実装してみました。Array#slice は既に存在するので、pickup というメソッドにしました。だいぶ苦労しましたねえ。きれいなコードじゃないし。引数が負の場合がややこしかったです。

class Array
  def pickup(left, right, step=nil)
    len = self.length
    unless step
      return [] unless !right or right != 0
      left ||= 0
      right ||= 0
      right -= 1
      self[left..right]
    else
      if step > 0
        left ||= 0
        right ||= len
        left = convp(left, len)
        right = convp(right, len)
        right = len if right >= len
        right -= 1
      elsif step < 0
        left ||= len - 1
        left = len - 1 if left >= len
        right ||= - len - 1
        left = convm(left, len)
        right = convm(right, len)
        right = - len - 1 if right < - len - 1
        right += 1
      else
        raise "ValueError: slice step cannot be zero"
      end
      selected = []
      left.step(right, step) {|i| selected.push(self[i])}
      selected
    end
  end
  
  def convp(i, len)
    return i if i >= 0
    i + len
  end
  
  def convm(i, len)
    return i if i < 0
    i - len
  end
  private :convp, :convm
end

こんな風に使います。引数第三項のステップ数は省略できます(デフォルトは 1)。引数第一項と第二項は(言語仕様上)省略できませんので、代わりに nil を使います。

irb(main):045:0> a = [0, 1, 2, 3, 4, 5]
=> [0, 1, 2, 3, 4, 5]
irb(main):046:0> a.pickup(0, 4)
=> [0, 1, 2, 3]
irb(main):047:0> a.pickup(-4, -1)
=> [2, 3, 4]
irb(main):048:0> a.pickup(2, nil)
=> [2, 3, 4, 5]
irb(main):049:0> a.pickup(nil, -2)
=> [0, 1, 2, 3]
irb(main):050:0> a.pickup(nil, nil)
=> [0, 1, 2, 3, 4, 5]
irb(main):051:0> a.pickup(nil, nil, -1)
=> [5, 4, 3, 2, 1, 0]
irb(main):052:0> a.pickup(-1, 2, -1)
=> [5, 4, 3]
irb(main):053:0> a.pickup(2, nil, 2)
=> [2, 4]

勉強用なので、エラー処理などはいいかげんです。たぶんだいたいは再現していると思うのですが。Python の対話型シェルも使いながら実装しました。

しかしこれ、移植してみて思ったのですが、引数が負の場合はわかりにくい仕様だと感じました。a.pickup(-1, 2, -1) の挙動などは、こうする必然性があまり感じられないのですよね。あと、引数が省略された場合(移植メソッドだと nil を入れた場合)、それぞれの端を意味するというのも実装しにくい理由です。自分が慣れているせいですが、Ruby の slice の方がすっきりしているような気もします。

どうも、第二引数が実際のインデックスと一致していないのがわかりにくいです。引数が負の場合など、却ってややこしくなっているのは否めません。


※追記 続編の記事があります。
Python のスライスを Ruby で実装(その2) - Camera Obscura
モジュールの Mix-in を使い、Array 同様 String でも pickup メソッドを使えるようにしました。