テキストを行ごとにキー,値,キー,値という辞書(連想配列)にするコードをチームみんなで書いた

:rage1:**鬼上司**「このテキストを一行ごとに、キー・値・キー・値…として辞書にしろ。行数はかならず偶数になっているものとする。」

:man:**部下**「」

という業務課題があり、やってもらったもののコードレビューのときにちょっとイマイチ…となったので、チームみんなでやってみました。言語は Python です。

なお、この課題は実際に業務で出てきたものにかなり近いものですが、この記事内のコードは一部手直しを入れて書いています。

# 課題

```python
source = """key1
value1
key2
value2
key3
value3
key4
value4"""
```
このように、キー、値、キー、値… と交互に改行区切り出てくるテキストを、辞書にしなさい。ただし、行数は必ず偶数であるものとします。また、改行文字は `\n` とします。

# 最初のコード

```python
def solution1(source):
    items = source.split('\n')
    d = {}
    for index, item in enumerate(items):
        if index % 2 == 1:
            d[items[index - 1]] = items[index]
    return d
```

### 結果
```python
print(solution1(source))
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4'}
```

うん。まあ問題なく動いている。ただ、なんかもっと…こう…あるでしょ! って感じがします。ループの半分がスカるのがちょっとなーって感じました。とはいえ、明快なコードの最終形は、まだ見えていません。

なんか zip 使うとうまくいきそうじゃない? という話になり、書いてみたらこんなコードになりました。

```python
def solution2(source):
    items = source.split('\n')
    keys = []
    values = []
    for i, v in enumerate(items):
        (values if i % 2 else keys).append(v)
    return dict(zip(keys, values))
```

```python
print(solution2(source))
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4'}
```

なるほど。なかなかおもしろい。ただし、空リストを作って突っ込んでく感じがあまり python っぽくない気がします。

どうせなら、チームみんなで考えてみようってことになり、コードを書いてもらいました。

# PHP の場合

その前に。PHPを書かれる人は気づかれたと思いますが、PHP では、[array_chunk](https://www.php.net/manual/ja/function.array-chunk.php) という、配列を特定サイズごとに切り分ける関数が最初から用意されており、これを使うとさっぱり書けます。ただし Python には無い。[itertools](https://docs.python.org/ja/3/library/itertools.html) に入ってそうですけど、無いんです。

配列やジェネレータを特定個数ごとに切り分ける、いわゆるチャンク処理は webアプリ開発ではけっこう出てくるコードで、例えば UI でグリッド表示する時に1行の要素数で全要素を切り分けたり (ラベルシール印刷時のレイアウトとか)、 SQL のバルクインサートでよく使います。

そのため、プロジェクトによってはチャンク関数が既に用意されていることも多いと思います、それを使えば簡単に書けます。

例えば、リストをチャンクする場合であれば、全体の長さ(len)が最初からわかっているので

```python
def chunk(source_list: list, n: int):
    for i in range(0, len(source_list), n):
        yield source_list[i:i + n]
```

こんな感じで書けますので、もしこのようなチャンク関数が既にあれば

```python
def solution3(source):
    return dict(chunk(source.splitlines(), 2))
```
という形で、一発で書けます。

ちなみに、イテレータに対するチャンクは、下記コメントにかなり良いコードがありました。

https://qiita.com/norami_dream/items/6f350cf3a72d0f3e685b#comment-2794437efe4022d1ae4d


# みんなのコード

チームのメンバーが書いてくれたコードを掲載します。ついでに1万回ループで実行させた時の経過時間も書いておきます。(単位のusはマイクロ秒の意味です)

なお、コードを書く時は速度を重視する要件は無かったことをお伝えします。また、上記のサンプルコードも見ていない状態で書いてもらいました。

## deque を使う

```python
import collections

def solution4(source):
    a = collections.deque(source.splitlines())
    d = {}
    while a:
        k = a.popleft()
        d[k] = a.popleft()
    return d
```
1万回実行 7642us

deque は、スレッドセーフな双方向キューで、頭からもお尻からも push, pop ができるいかしたやつです。頭からキーと値を1つづつ pop するパターンです。

## 引数つき pop を使う
```python
def solution5(source):
    a = source.splitlines()
    d = {}
    while a:
        d[a.pop(0)] = a.pop(1)
    return d
```
1万回実行 6991us

普通の list でも、pop(0) で頭から pop ができます。シングルスレッドではこれで十分でしょう。

## イテレータにして next(), next() する
```python
def solution6(source):
    def gen():
        a = (l for l in source.splitlines())
        while True:
            try:
                yield next(a), next(a)
            except StopIteration:
                return

    return dict(gen())
```
1万回実行 11288us
この方式は、少し遅かったようです。try を使ってるからかもしれません。

## range で2ステップづつ回す
```python
def solution7(source):
    a = source.splitlines()
    return {a[i]: a[i + 1] for i in range(0, len(a), 2)}
```
1万回実行 6907us


## 2ステップのスライスを、1つずらして作る
```python
def solution8(source):
    a = source.splitlines()
    return dict(zip(a[0::2], a[1::2]))
```
1万回実行 5369us

## リスト内包表記の if を使う
```python
def solution9(source):
    splited = source.split('\n')
    two_each_list = [
        splited[i:i + 2] for i, _ in enumerate(splited) if i % 2 == 0]
    return {l[0]: l[1] for l in two_each_list}
```
1万回実行 12033us
若干オーバーヘッドがあるようです。

## 半分にしたインデックスを回す
```python
def solution10(source):
    source_list = source.split()
    return {source_list[2 * i]: source_list[2 * i + 1]
            for i in range(len(source_list) // 2)}
```
1万回実行 7557us

## 一つのイテレータを二箇所で進める
```python
def solution11(source):
    return dict(zip(*[iter(source.split())] * 2))
```

……え:disappointed_relieved:

読めない……

このノードを書いてくれた方は、丁寧に説明も書いてくれました。

```python
it = iter(source.split())  # <list_iterator object at 0x1067df070>
z = zip(it, it)  # it がコールされるたび 0x1067df070 の __next__ が次の要素を返すので交互に振り分けられる
result2 = { k: v for k, v in z }

print(result2)
{'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4'}
```
まじだ。すごい。アイディアが良い。

計測したところ、速度もこのコードが一番出てました。

1万回実行 4646us


## 正規表現で2行づつ取得する

```python
def solution12(source):
    return {m.group().split()[0]: m.group().split()[1] for m in
            re.finditer(r'.+\n.+\n*', source)}
```
これも面白いですね。思いつかない方法でした。速度は

1万回実行 19113us

と、速度的な優位は無いようです。

正規表現のコンパイルをあらかじめ行っておき、結果取得の箇所も少し手直しすると

```python
_r = re.compile(r'(.+)\n(.+)\n?')

# これをループで計測
def solution12_2(source):
    return {m.group(1): m.group(2) for m in
            _r.finditer(source)}
```
1万回実行 12253us

と、早くなりましたが素直にスライスをしたりする方法と比べると速さでの優位性は無いです。
ただ、そういう発想もあるのか、と感心させられたコードでした。