🐍

【python】意図が伝わるコード(コレクション・イテレーション)

2023/10/23に公開

記事の概要

Pythonのデータ構造は、異なる状況や要件に適応するための多様な特性を持っています。この記事では、Pythonで利用可能な主要なコレクション型とそれぞれの特性、そして適切な使用例を紹介します。
また複数の方法があるiteration(ループ文)についてもどれを選択すれば、どういう意図で書いたかを認識してもらいやすいかを紹介していこうと思います。

自己文書化されたコードを目指して、コレクションとiterationの手段がどのような意図の表現になるかを知りましょうということですね。

コレクションの選択による意図

コレクションとそれに付随する意図を記載しています。
この意図に沿わないのであれば、他のコレクションを選択した方が良いという考えでいたら良いかと思います。

納得感をもってもらうために、まずはコレクションの選択を誤ったばかりに意図が伝わらなかったパターンをみてもらいたいです。

【例】
あるオンラインブッククラブでは、メンバーが読んだ本とその評価を共有することができます。ブッククラブの管理者は、各メンバーの読書履歴と評価を追跡し、月間のトップリーダーを決定したいと考えています。

誤った方法: リストを用いて情報を管理

reviews = [
    "Alice", "Python Mastery", 5,
    "Bob", "Deep Dive into ML", 4,
    "Alice", "Deep Dive into ML", 5
]

この方法でデータを管理すると以下の問題が生じます。
曖昧性: 誰がどの本を読んだのか、どの評価をつけたのかが一目瞭然でない。
複雑性: 新しいレビューを追加または既存のレビューを修正する際に、正しいインデックスを見つけるのが難しい。
非効率: あるユーザーの全てのレビューを見つけるには、リスト全体をスキャンする必要がある。

適切な方法: 辞書とタプルの組み合わせを使用

reviews = {
    "Alice": [("Python Mastery", 5), ("Deep Dive into ML", 5)],
    "Bob": [("Deep Dive into ML", 4)]

当然ですが、感覚的にも理解しやすくなりました。
この構造には以下の利点があります。
明確性: それぞれのユーザーの読書履歴と評価が直接的に関連付けられている。
拡張性: あるユーザーの読書履歴に新しいレビューを追加するのが容易。
高速な検索: あるユーザーのレビューをすぐに参照できる。

例を示してみましたが、要するに利用するコレクションによって、その処理が何をしたいのか、なぜそのような形式にしているのかなど背景知識を伝えることになるので、特徴を理解して使用するのが良いですね。
以下にコレクションが意図する表現や役割を示してみました。

リスト

iterableなコレクションでミュータブルである
静的なインデックスを用いてリストの特定の要素を取得したりはしない
重複する要素が含まれている可能性はある

users = ["Alice", "Bob", "Charlie", "Alice"]
users.append("David")
print(users)  # ['Alice', 'Bob', 'Charlie', 'Alice', 'David']

文字列

文字のイミュータブルなコレクション

product_description = "High-quality leather wallet. Perfect for everyday use."
print(product_description)

ジェネレータ

iterateされ、インデックスでは参照されないコレクション
要素アクセス方法は遅延処理なので、処理属度がかかる場合がある
計算コストが高いコレクションや要素数が無数にあるコレクションに向いている

def generate_dates(start_date, end_date):
    current_date = start_date
    while current_date <= end_date:
        yield current_date
        current_date += timedelta(days=1)

dates = generate_dates(date(2023, 1, 1), date(2023, 1, 5))
for d in dates:
    print(d)

タプル

イミュータブルなコレクション
イミュータブルであるのでインデックスかアンパック等で特定の要素を抽出する可能性が高い。
iterateされることはほとんどない

product = ("Leather Wallet", 49.99, "High-quality leather wallet.")
name, price, description = product
print(name)  # Leather Wallet

集合

iterableなコレクションで、要素に重複がない
要素が一定の順序で並ぶことを堰堤とする処理はできない
つまりインデックスでは参照されない可能性が高い

transaction_ids = {101, 102, 103}
transaction_ids.add(104)
print(105 in transaction_ids)  # False

辞書

キーから値へのマッピングを格納
キーは重複がない
iterateしたり、動的なキーを使って値を参照されることが多い

payments = {
    "Alice": [{"product": "Leather Wallet", "amount": 49.99}, {"product": "Handbag", "amount": 79.99}],
    "Bob": [{"product": "Belt", "amount": 29.99}]
}
print(payments["Alice"])  # [{'product': 'Leather Wallet', 'amount': 49.99}, {'product': 'Handbag', 'amount': 79.99}]

イテレーションの選択による意図

異なる繰り返しの方法を選択することで、読み手に意図を明確に伝えることができます。以下では、Pythonの主なループ手法を挙げ、それらの意図について紹介します。

forループ

コレクションや範囲に含まれる各要素に対して処理を実行したり副作業を発生させたりするために使われる。

products = ["スマートウォッチ", "ワイヤレスイヤホン", "タブレット"]
for product in products:
    print(f"{product}の発売が近づいています!")

whileループ

一定の条件が満たされている間、iteration続けるために使われる。

stock = 5
while stock > 0:
    print(f"在庫があと{stock}点です。")
    stock -= 1

内包表記

コレクションを別のコレクションに変換するために使われる。

prices = [1000, 2000, 3000, 4000]
discounted_prices = [price * 0.9 for price in prices]

再帰

コレクションの下部構造がコレクションの構造と同じ場合に使われる。

def display_department(department, level=0):
    print("  " * level + department["name"])
    for sub_dept in department["sub_departments"]:
        display_department(sub_dept, level + 1)

org_structure = {
    "name": "本社",
    "sub_departments": [
        {"name": "営業部", "sub_departments": []},
        {"name": "技術部", "sub_departments": [
            {"name": "ソフトウェアチーム", "sub_departments": []},
            {"name": "ハードウェアチーム", "sub_departments": []}
        ]}
    ]
}

display_department(org_structure)

以上、Pythonの様々な繰り返し手法とそれぞれの適切な使用シーンを通じて、コードが持つ意図の大切さを探求しました。明確な意図を持って正しい手法を選ぶことで、コードの読み手にとって理解しやすく、保守も容易なコードを書くことができます。
と自分に言い聞かせるのであった。

Discussion