会社のRubyistが「一つの言語を極めておくと、他の言語もゴリゴリ書けるようになるって最近Go言語書いてるPerl Mongerのオッサンが言ってた」って言ってました。
私も、Pythonをある程度書けるようになってたおかげで、RubyやR言語でも迷わずにプログラミングできるようになっている気がします (まだまだ少しコーディングが遅い気はするのですが)。
この間の記事で書いたように、DBから取ってきたリストをjoinさせる関数を定義し、抽象度の高い楽しいプログラミングをしていました。
ただし、実装していたバッチ処理で、割と汚いデータを相手にしていたため、何度かに分けてjoinする必要があり、 その中でinjectする際に毎回ループを回してしまっています。
Pythonでは、このようなときはジェネレーター(遅延評価させるリスト)を使って、内部的には一回のループで済むようにします。 ちゃんとした説明を見たい人はEffective Pythonを読んでください。
Effective Python ―Pythonプログラムを改良する59項目
- 作者: Brett Slatkin,石本敦夫,黒川利明
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/01/23
- メディア: 大型本
- この商品を含むブログ (4件) を見る
Rubyでも同じように遅延評価できないかと思ったら、Enumerator::Lazyというクラスがありました。また、.map
や.select
メソッドの前に.lazy
を挟むことで、することができるようです。
joinの関数のコードはややこしいので、↓のような問題を考えます。
# この配列から nested_list = [[1], [2, '2'], ['a', 3, 'b'], [4], ['c']] # 数字だけのリストが欲しいとする number_list = [1, 2, 3, 4]
Pythonであれば、ジェネレーター式を使ってこんな感じで処理すると思います。
# Python3.5ならこんな感じ def unlist(nested_list): for ary in nested_list: for elem in ary: if type(elem) is int: yield elem else: pass # 一応 gen = unlist(nested_ary) # ジェネレーターオブジェクトができてる print(gen) # => <generator object unlist at 0x1021db708> # 最初の要素を取り出す print(next(gen)) # => 1 # リストにして残りの要素を取り出す print(list(gen)) [2, 3, 4]
Rubyでは、同じような処理をinjectを使ってリストに追加していくことが多いように思います。 Pythonではyieldで値を1つずつ外に出していっているのに対し、injectで配列に突っ込んでいる感じですね。
number_list = nested_list.inject([]) do |ary, item| item.each do |x| ary << x if num.is_a?(Integer) end ary end # もちろん関数を作ることもできる # Rubyではreturnが不要なので、そのまま包めばいいだけで楽 def unlist(nested_list) nested_list.inject([]) do |ary, item| item.each do |x| ary << x if num.is_a?(Integer) end ary end end puts number_list # => [1, 2, 3, 4]
これを遅延評価させたいと思ったのですが、.lazy
で呼び出す方法では、map
とselect
とその亜種がほとんどで、
Pythonのジェネレーター式のようにyieldしていくようなメソッドは無さそうでした。
injectも、そもそも畳み込みのためのメソッドなので遅延評価されません。
代わりにEnumerator::Lazy
のインスタンスを作ることで、ジェネレーター式と同じものを作ることができました。
# Ruby2.2で試しています def lazy_unlist(nested_list) Enumerator::Lazy.new(nested_list) do |yielder, item| item.each do |x| yielder << x if x.is_a?(Integer) end end end generator = lazy_unlist(nested_list) puts generator # => #<Enumerator::Lazy:0x0055634bb03200> # 最初の要素を取り出す puts generator.first # => 1 # リストに変換する puts generator.force # => [1, 2, 3, 4] # Pythonと違って、一度使った値が消費されないみたいなので要調査
Pythonのジェネレーター式のようなものが、Rubyの柔軟な構文(後置のif文とか)を使った上で書けそうです。
Enumerator::Lazy.new(nested_list) do |yielder, item|
じゃなくてnested_list.lazy.generate do |yielder, item|
みたいな書き方が用意されててほしいなという気もします。
RubyのArrayとHashのInjectメソッドでArrayに詰め込むとき、ちょっと工夫すればPythonのジェネレーター風の遅延評価ができたら神だし、実際できそうな気がするけどよく分からない
— 黒めだか (@takeshi0406) 2016年3月28日
仕事のコードでは元々のinject
の書き方でも遅くなかったのでそのままですが、勉強になりました。
もう少し、PythonとRubyの挙動の違いとかいろいろ試してみたい&調べてみたいですが。