もっと積極的にgeneratorを使おうと思った話

これまであまり積極的にgenerator を使ってこなかったんですが、最近作ってたプログラムが多めのデータを扱うもので、DBの読み込み負荷を減らす必要があって今回初めて使いました。Pythonのgenerator書きやすい。ついでにJavaScript, Ruby, PHPでもどう書くか調べました。自分メモです。

Pythonのgenerator

python generator.pyで実行するコードです。

def get_generator(batch_size: int = 10):
    items = [x for x in range(1000)]
    i = 0
    while True:
        offset = batch_size * i
        _items = items[offset:offset + batch_size]
        if len(_items) == 0:
            break

        i += 1
        yield _items

g = get_generator()
for items in g:
    print(items)

実行結果は下記のようになります。

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
(中略)
[960, 961, 962, 963, 964, 965, 966, 967, 968, 969]
[970, 971, 972, 973, 974, 975, 976, 977, 978, 979]
[980, 981, 982, 983, 984, 985, 986, 987, 988, 989]
[990, 991, 992, 993, 994, 995, 996, 997, 998, 999]

JavaScriptのgenerator

JSだとredux-sagaでしかgeneratorを使った事なかったんですが、Symbol.iteratorというのを使うとfor-ofというのが使えるんですね。あまり馴染みがないのと便利だけど何やってるか分かりにくいというのがややマイナス要素です。

下記はbabel-nodeを予めインストールしておいて、babel-node generator.jsと実行するコードです。

function getGenerator(batchSize = 10) {
  return function* () {
    const items = [...Array(1000).keys()];
    let i = 0;
    while (true) {
      const offset = batchSize * i;
      const _items = items.slice(offset, offset + batchSize);
      if (_items.length === 0) {
        break;
      }
      i ++;
      yield _items;
    }
  }
}

const it = {};
it[Symbol.iterator] = getGenerator();

for (let items of it) {
  console.log(items);
}

実行結果は下記のようになります。

[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
[ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ]
[ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29 ]
[ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39 ]
[ 40, 41, 42, 43, 44, 45, 46, 47, 48, 49 ]
(中略)
[ 960, 961, 962, 963, 964, 965, 966, 967, 968, 969 ]
[ 970, 971, 972, 973, 974, 975, 976, 977, 978, 979 ]
[ 980, 981, 982, 983, 984, 985, 986, 987, 988, 989 ]
[ 990, 991, 992, 993, 994, 995, 996, 997, 998, 999 ]

Rubyのgenerator

Rubyは普段はChefくらいでしかあまり使わないですが、たまに書くことがあるので調べてみました。下記の記事を参考にしました。Rubyではyieldは別の意味で使われてるらしく、Enumeratorクラスのyieldを使う事で出来るようです。けど、あまり一般的じゃないのかな…?

Are there something like Python generators in Ruby?

ruby generator.rbで実行するコードです。

def get_generator(batch_size = 10)
  items = (0..999).to_a
  Enumerator.new do |enum|
    i = 0
    while true
      offset = batch_size * i
      _items = items[offset, batch_size]
      if _items == nil || _items.length == 0
        break
      end

      enum.yield _items
      i +=1
    end
  end
end

g = get_generator
g.each do |items|
  p items
end
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
(中略)
[960, 961, 962, 963, 964, 965, 966, 967, 968, 969]
[970, 971, 972, 973, 974, 975, 976, 977, 978, 979]
[980, 981, 982, 983, 984, 985, 986, 987, 988, 989]
[990, 991, 992, 993, 994, 995, 996, 997, 998, 999]

PHPのgenerator

最後にPHPです。意外にPythonと同じくらい書きやすかったです。PHPだとどんどん使ったほうが良いですね。

php generator.phpと実行するコードです。

function getGenerator($batchSize = 10)
{
    $items = range(0, 999);
    $i = 0;
    while (true) {
        $offset = $batchSize * $i;
        $_items = array_slice($items, $offset, $batchSize);

        if (count($_items) == 0) {
            break;
        }
        $i ++;
        yield $_items;
    }
}

$g = getGenerator();
foreach ($g as $items) {
    print_r($items);
}

下記が実行結果です。

Array
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
    [5] => 5
    [6] => 6
    [7] => 7
    [8] => 8
    [9] => 9
)
Array
(
    [0] => 10
    [1] => 11
    [2] => 12
    [3] => 13
    [4] => 14
    [5] => 15
    [6] => 16
    [7] => 17
    [8] => 18
    [9] => 19
)
(中略)
Array
(
    [0] => 980
    [1] => 981
    [2] => 982
    [3] => 983
    [4] => 984
    [5] => 985
    [6] => 986
    [7] => 987
    [8] => 988
    [9] => 989
)
Array
(
    [0] => 990
    [1] => 991
    [2] => 992
    [3] => 993
    [4] => 994
    [5] => 995
    [6] => 996
    [7] => 997
    [8] => 998
    [9] => 999
)

いろんな言語で比較してみると改めて参考になることがけっこうありますね。面白いです。