👭

Pythonのitertools. groupby()で困った話

2025/01/23に公開

はじめに

こんにちは!美味しいものに目がないPOCO🐶です。
アプリを自作する中で、SQLでの処理が複雑になり、どうにか効率化できないかと模索していました。
その際に出会ったのがPythonのitertools.groupby()です。「これは便利そう!」と感じ、さっそく使ってみたのですが、実際に活用してみると意外な落とし穴があり、試行錯誤の連続でした。

今回の記事では、私がgroupby()に挑戦した経験を通じて感じた学びについて共有します。
同じように迷う初心者エンジニアの方々にとって、少しでも参考になれば幸いです!

対象読者

Python初心者

結論

groupby()したデータを再利用する場合は、事前にリストにして一時退避しておく必要があります。

後々、公式ドキュメントを確認すると「groupby()が返すグループは、一度進めると前のグループには戻れない仕様であることが分かりました。そのため、後にグループ化したデータを利用する場合は、リストとして格納する必要があります。」と記述されてしました。

The returned group is itself an iterator that shares the underlying iterable with groupby(). Because the source is shared, when the groupby() object is advanced, the previous group is no longer visible. So, if that data is needed later, it should be stored as a list:
引用:https://docs.python.org/3/library/itertools.html#itertools.groupby

開発環境

macOS 15.1.1

> python -V
Python 3.13

本文

itertools.groupby()とは?

tertools.groupby(iterable, key=None)
同じキーをもつような要素からなる iterable 中のグループに対して、キーとグループを返すようなイテレータを作成します。key は各要素に対するキー値を計算する関数です。キーを指定しない場合や None にした場合、key 関数のデフォルトは恒等関数になり要素をそのまま返します。通常、iterable は同じキー関数でソート済みである必要があります。
引用:https://docs.python.org/ja/3/library/itertools.html#itertools.groupby

例えば表でイメージします。
以下の表に対してIdをキーとしてgroupBy()すると、

Id Character Features
1 シナモン イヌ
1 シナモン 白色
2 ポムポムプリン イヌ
2 ポムポムプリン 黄色
3 ぽちゃこ イヌ
3 ぽちゃこ 白色

このような表を作成することができます。

Id Character Features
1 シナモン イヌ、白色
2 ポムポムプリン イヌ、黄色
3 ぽちゃこ イヌ、白色
Pythonでgroupby()を実行する

itertools.groupby()を利用する場合は事前にimportする必要があります。

from itertools import groupby

今回の例では、グループ化する前のデータとして、タプル型のリストを作成します。

temp_list = [(1, 'シナモン','イヌ'),(1,'シナモン','シロ'),(2,'ポムポムプリン','イヌ'),(2,'ポムポムプリン','黄色'),(3,'ぽちゃこ','イヌ'),(3,'ぽちゃこ','白色')]

IdをキーとしてgroupBy()すると、以下の通りにグループ化されます。

# 同じidのキャラクターでグループ化
grouped = groupby(temp_list, key=lambda x: x[0])
for key, group in grouped:
    print(f'key:{key}:')
    for item in group:
        print(f'{item[1]},{item[2]}')
#出力結果
#key:1:
#シナモン,イヌ
#シナモン,白色
#key:2:
#ポムポムプリン,イヌ
#ポムポムプリン,黄色
#key:3:
#ぽちゃこ,イヌ
#ぽちゃこ,白色
苦戦したところ

グループ化済みデータに対して、特徴(Features)に「イヌ」と「白色」を持つキャラクターを抽出しようとしました。今回のデータであれば、キャラクターのシナモンとぽちゃこが抽出されるようにしようとしました。

# 期待する結果
character_list = [(1, 'シナモン', 'イヌ'), (3, 'ぽちゃこ', 'イヌ')]

しかし、グループ化後のデータの取り扱いについて理解が十分にできていない状態で
以下の通り実装したところ、該当のキャラクターを抽出することができていませんでした。
実行結果はcharacter_listが空のデータで表示されました。

誤った実装
from itertools import groupby
from typing import List

temp_list = [(1, 'シナモン','イヌ'),(1,'シナモン','白色'),(2,'ポムポムプリン','イヌ'),(2,'ポムポムプリン','黄色'),(3,'ぽちゃこ','イヌ'),(3,'ぽちゃこ','白色')]
grouped = groupby(temp_list, key=lambda x: x[0])
keywords = ['イヌ','白色']

def group_extraction(group:str, keywords:List[str])->bool:
    """指定のキーワードが全て存在するキャラクターか判定

    Args:
        group (str): _description_
        keywords List[str]: _description_

    Returns:
        bool: _description_
    """
    for keyword in keywords:
        judge_list = []
        bool_list = []
        for item in group:
            bool_list.append(keyword in item[2])
        judge_list.append(any(bool_list))    
    return  all(judge_list)

# 指定のキーワードを全て含むものみを抽出
character_list = []
for key, group in grouped:
    if group_extraction(group, keywords):
          character_list.append(group)
print(character_list)
# 実行結果
# []
原因

for文でグループから取り出したgroupは、イテレータであるためループ内で一度使用されると再利用することができません。

for key, group in grouped:
    if group_extraction(group, keywords):
          character_list.append(group)
print(character_list)
修正後の実装

groupby()で取り出したgroupを再利用したい場合は、事前にリストに保管しておく必要があります。

from itertools import groupby
from typing import List

temp_list = [(1, 'シナモン','イヌ'),(1,'シナモン','白色'),(2,'ポムポムプリン','イヌ'),(2,'ポムポムプリン','黄色'),(3,'ぽちゃこ','イヌ'),(3,'ぽちゃこ','白色')]
grouped = groupby(temp_list, key=lambda x: x[0])
keywords = ['イヌ','白色']

def group_extraction(group:List[tuple], keywords:List[str])->bool:
    """指定のキーワードが全て存在するキャラクターか判定

    Args:
        group List[tuple]: _description_
        keywords List[str]: _description_

    Returns:
        bool: _description_
    """
    for keyword in keywords:
        judge_list = []
        bool_list = []
        for item in group:
            bool_list.append(keyword in item[2])
        judge_list.append(any(bool_list))    
    return  all(judge_list)
        
# 指定のキーワードを全て含むものみを抽出
character_list = []
for key, group in grouped:
    group_list = list(group)
    if group_extraction(group_list, keywords):
          character_list.append(group_list[0])
print(character_list)
# 実行結果
# [(1, 'シナモン', 'イヌ'), (3, 'ぽちゃこ', 'イヌ')]

問題なく期待の結果が出力されました。

最後に

公式ドキュメントを読んでも少し分かりづらいと感じた方にとって、この解説が少しでも理解の助けになれば幸いです。
初心者エンジニアとして、公式ドキュメントをしっかり読む習慣を身につけることの大切さを改めて実感しました。(自戒を込めて)

Discussion