LoginSignup
2

More than 5 years have passed since last update.

itertools.groupbyは事前にソートしておく必要がある & n番目の要素を取得する関数がほしい場合はoperator.itemgetterが便利

Posted at

受信したメールの受信日付、送信元Adress、件名が以下のような形式でリストに格納されているとします。

received_mails = [
  ("2018-01-01", "alice@example.com", "subject1"),
  ("2018-02-02", "bob@example.com"  , "subject2"),
  ("2018-03-03", "chris@example.com", "subject3"),
  ("2018-04-04", "alice@example.com", "subject4"),
  ("2018-05-05", "bob@example.com"  , "subject5"),
]

受信したメールを送信元Adressによって振り分けたい場合、言い換えればSQLでいうところのGROUP-BYのようなことをしたい場合、pythonではitertools.groupbyが利用できます。

bad.py
def display_mails(mails):
    return ':'.join(f'[DATE={mail[0]} ADDRESS={mail[1]} SUBJECT={mail[2]}]' for mail in mails)

for address, mails in itertools.groupby(received_mails, lambda mail : mail[1]):
    print(address, display_mails(mails))

この結果は次の通りです。要するに送信元Addressによる振り分けができておらず、想定とは違う結果になっています。

alice@example.com [DATE=2018-01-01 ADDRESS=alice@example.com SUBJECT=subject1]
bob@example.com [DATE=2018-02-02 ADDRESS=bob@example.com SUBJECT=subject2]
chris@example.com [DATE=2018-03-03 ADDRESS=chris@example.com SUBJECT=subject3]
alice@example.com [DATE=2018-04-04 ADDRESS=alice@example.com SUBJECT=subject4]
bob@example.com [DATE=2018-05-05 ADDRESS=bob@example.com SUBJECT=subject5]

そこでitertools.groupbyのドキュメントを読んでみたところ、groupbyの対象になるイテレータはソート済みである必要があるとのことでした。

同じキーをもつような要素からなる iterable 中のグループに対して、キーとグループを返すようなイテレータを作成します。key は各要素に対するキー値を計算する関数です。キーを指定しない場合や None にした場合、key 関数のデフォルトは恒等関数になり要素をそのまま返します。通常、iterable は同じキー関数でソート済みである必要があります。
groupby() の操作は Unix の uniq フィルターと似ています。 key 関数の値が変わるたびに休止または新しいグループを生成します (このために通常同じ key 関数でソートしておく必要があるのです)。この動作は SQL の入力順に関係なく共通の要素を集約する GROUP BY とは違います。

今回の例であれば、次のように書き直す必要があるということです。

good.py
get_adress = lambda mail : mail[1]
for address, mails in itertools.groupby(sorted(received_mails, key=get_adress), get_adress):
    print(address, display_mails(mails))

この実行結果は以下のようになり、確かに送信元Adressでgroup by処理ができていることがわかります。

alice@example.com [DATE=2018-01-01 ADDRESS=alice@example.com SUBJECT=subject1]:[DATE=2018-04-04 ADDRESS=alice@example.com SUBJECT=subject4]
bob@example.com [DATE=2018-02-02 ADDRESS=bob@example.com SUBJECT=subject2]:[DATE=2018-05-05 ADDRESS=bob@example.com SUBJECT=subject5]
chris@example.com [DATE=2018-03-03 ADDRESS=chris@example.com SUBJECT=subject3]

good.pyではget_address関数をlambdaを利用して定義しました。これはdefを使って、以下のように定義することも可能です。

def get_address(mail):
    return mail[1]

lambdaにしろdefにしろ「リストやタプルのn番目の値を取得する」関数が欲しいわけで、こうした用途においてはoperator.itemgetterを利用することも可能です。たとえばgood.pyoperator.itemgetterで書き換えると、次のようになります。

good.py
get_address = operator.itemgetter(1)
for address, mails in itertools.groupby(sorted(received_mails, key=get_adress), get_adress):
    print(address, display_mails(mails))

あるいは他の例を考えてみると、たとえばreceived_mailsから件名だけを取り出して、リストに格納したい場合、以下のような3通りの書き方ができるわけです。

subjects = [mail[2] for mail in received_mails]
subjects = list(map(lambda mail : mail[2], received_mails))
subjects = list(map(operator.itemgetter(2), received_mails))

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2