isliceの紹介、具体例添え(pythonのitertoolsを使いこなすために)


Pythonの標準ライブラリである[itertools](https://docs.python.org/ja/3/library/itertools.html)を(超個人的に)推している。
しかしながら、まわりで活用している人は多くない様子なので、 ~~布教~~ 紹介記事を書くことにする。

この記事では isliceを紹介する。
Pythonのバージョンは3.7を使っている。

## はじめに：どうすれば良さが伝わるか

itertoolsを紹介するブログは日本語でもちゃんと存在する。

* [すごいぞitertoolsくん](https://qiita.com/anmint/items/37ca0ded5e1d360b51f3) 
* [Python - itertools チートシート (頻出パターン)](http://pynote.hatenablog.com/entry/python-itertools-recipe)

にも関わらず、なぜみんな使わないのか。

話を聞いていくうち、「実際に、どういうタイミングで、どう使うかわからない」という意見があることを知った。
確かに、「ここはもっとスマートに書く方法がある気がする」と思っても、今まで一度も使ったこともないitertoolsを使えばよいとは思いつかないものだ。

そういう訳で、実際のコードでどう使われているのかも調べることにした。これは後半で紹介する。



## isliceとは

> iterable から要素を選択して返すイテレータを作成します。 start が0でない場合、iterable の要素は start に達するまでスキップされます。その後、 要素が順に返されます。
[itertools.islice 公式ドキュメントより](https://docs.python.org/ja/3/library/itertools.html#itertools.islice)

isliceは、簡単に言えば、listの `[start:stop:step]` がiterableな要素に対して実行できる関数。
下記の例では 0~999までの要素が入った `data` から最初の10個だけ出力している。

```python:islice_sample.py
from itertools import islice

data = [ i for i in range(1000)]
for line in islice(data,10): # data[:10]と等価
	print(line)
```

## ファイルオブジェクトに対してisliceを使う

上記例(`islice_sample.py`)だと、「`data[:10]`でいいじゃん」と思うかもしれない。
しかし、isliceは、listではないiterableに対しても使えることがキモだ。

具体的には、ファイルを特定の行数だけ読み込む時に便利だ。

- テキストファイルを読み込んだ時のオブジェクト [TextIOWrapper](https://docs.python.org/ja/3/library/io.html#io.TextIOWrapper) はiterableなため[^1]、以下の例(`islice_sample2.py`)のように、ファイルの最初のn行だけ読み込むことができる。
	- 例えば、巨大なファイルを扱う時、とりあえず最初の10行だけ受け取り、関数の動作を確認できる。

[^1]: もう少し詳しく書くと、TextIOWrapperはIOBaseを継承しているため(参考： [【Python】open関数で返されたtext objectがiterableなわけ](https://sorededou.com/python-reason-textobject-is-iterable))

```python:islice_sample2.py
# isliceで10行だけ読み込み
with open("./sample.txt", "r")as f:
	for line in islice(f, 10):
		print(line.strip())

# 20~30行目だけdataに読み込み
with open("./sample.txt", "r")as f:
	for line in islice(f, 20, 30):
		print(line.strip())
```


## 具体例

テキストファイルの他にも活用方法はないか、普段使っているレポジトリでisliceが使われている箇所を調べてみた。
ざっとみたところ、やはりファイル読み込みに使われることが多く、クラスの `__iter__()`に活用されているようだ。
ファイル読み込みの例を含め、利用しているコードを紹介する。

#### ファイル読み込み(gensim)

```python:word2vec.py
for line in itertools.islice(self.source, self.limit):
	line = utils.to_unicode(line).split()
```
> 引用元 [gensim/models/word2vec.py](https://github.com/RaRe-Technologies/gensim/blob/8420768d658020d448ab0ea1ec0012fdfcd23d75/gensim/models/word2vec.py#L1427-L1447)


- `source` ファイルから `limit` だけ読み込む。
	- *one line one sentence* のファイルからsentence(文)を読み込むクラスの、 `__iter__()` 内で使われている。
	- `limit`のデフォルトは`None` なので、基本的にはファイル全てのsentenceを読み込んでいる
	

#### ミニバッチ生成(spacy)

```python:util.py
    while True:
        batch_size = next(size_)
        batch = list(itertools.islice(items, int(batch_size)))
        if len(batch) == 0:
            break
        yield list(batch)
```
> 引用元 [spaCy/spacy/util.py](https://github.com/explosion/spaCy/blob/573e543e4aadc83e30a1f4069f3624899945e66e/spacy/util.py#L495-L500)


- `minibatch` 関数における実装。
 - `batch_size` の数だけ`items` を受け取り、ミニバッチをyieldで返す
	 - `size_`  のデフォルトは `itertools.repeat(size)` で定義されるため、`items` の中身がなくなるまで返し続ける
	 


#### ラベルの色指定(scikit-learn)

```python:plot_linkage_comparison.py
         colors = np.array(list(islice(cycle(['#377eb8', '#ff7f00', '#4daf4a',
                                              '#f781bf', '#a65628', '#984ea3',
                                              '#999999', '#e41a1c', '#dede00']),
                                       int(max(y_pred) + 1))))
```
> 引用元 [scikit-learn/examples/cluster/plot_linkage_comparison.py](https://github.com/scikit-learn/scikit-learn/blob/16f4ac90f0732988e3b7efe0c937eaff70e99692/examples/cluster/plot_linkage_comparison.py#L134-L137)

- scikit-learnにある`examples` のクラスタリングの結果をプロットする例
- `cycle` によって色情報のlistが無限ループしているので、そのうち `int(max(y_pred) + 1` (=予測クラス数) だけ読み込む
	- クラス数が1~9ならば `cycle`なし(`islice(['#377eb8', '#ff7f00', '#4daf4a', ..., '#dede00']), int(max(y_pred) + 1)))`)と等価
- プロットに使う色数がわからないとき、使う色数を制限したいときに便利かもしれない
	

ちなみに、実際にプロットした結果はscikit-learnのサイトにある→[該当ページ](https://scikit-learn.org/stable/auto_examples/cluster/plot_linkage_comparison.html)


#### まとめ

* isliceはテキストファイルから特定の行数のみ取得したいときに便利
* `repeat` や `cycle`  のような Infinite iteratorsと組み合わせて、ループを止めるカウンターのように扱うときにも便利
	* [zip_longest](https://docs.python.org/ja/3/library/itertools.html#itertools.zip_longest) でも、以下のように書いてある。
	
> iterables の1つが無限になりうる場合 zip_longest() は呼び出し回数を制限するような何かでラップしなければいけません(例えば islice() or takewhile())。 
> https://docs.python.org/ja/3/library/itertools.html#itertools.zip_longest