巨大ループ
1. 状況
ループの中で様々な処理が行われており、ループが巨大化している
- ループ内で様々な処理が行われている為、ループのネストやループ自体の肥大が発生している
- ループやIF文のネストが発生し、可読性/保守性が低下している
2. 問題
保守性が低下する
- 逐次的に処理が行われている為、新たな処理を追加する場合、既存のコードに新たなコードを挿入する必要がある
- 既存のコードに処理を挿入していくと、関数が肥大化し保守性が低下してしまう
- 巨大なループの中で様々なロジックが実行されると、ループ毎に変数の状態や組合せが変化する為、処理を追うことが困難になる
3. 原因
プログラムの構造化不足
- 経験の浅いプログラマが処理を構造化せず一筆書きでロジックを書き下すと巨大ループが発生しやすい
- プログラマが巨大な処理を一筆書きで記述してしまうのは、良いコードについての知見や良いコードを記述するスキルが不足していることが主要な原因であることが多い
4. 対策
ループを分割する
- ループが巨大化する原因はループの中で異なる処理を行っている為である
- したがって、ひとつのループで全ての処理を行うのではなく、関心毎にループ自体を分割するべきである
- 例えば、↓の例では
v['type']
毎にループの中で処理を行っている為、巨大なループが形成されてしまっている
def example(values):
for v in values:
if v['type'] == '001':
#
# なにかしらの処理
#
if v['type'] == '002':
#
# なにかしらの処理
#
if v['type'] == '003':
:
:
- ↑の例はデバッグもしづらく、ロジックが追加されるとどんどんループが巨大化していってしまう
- そこで、
v['type']
毎に必要なデータを抽出したリストを作成し、個別のリスト(関心)毎に処理を行うことにする
def example(values):
type001_list = []
for v in values:
if v['type'] == '001':
type001_list.append(v)
for t1 in type001_list:
#
# 何かしらの処理
#
type002_list = []
for v in values:
if v['type'] == '002':
type001_list.append(v)
for t2 in type002_list:
#
# 何かしらの処理
#
type003_list = []
for v in values:
if v['type'] == '003':
type001_list.append(v)
for t3 in type003_list:
#
# 何かしらの処理
#
- ループの数は増えたが、ループ対象のデータと処理はコンパクトになり、デバッグ時に結果を確認できるポイントも増える為、保守性が向上する
- 分かりやすさの為ここでは行っていないが、それぞれの処理を関数に切り出すと更に保守性が高くなる
- 元のデータ(ここでは
values
)に対する全件ループが何度も行われることになるので、実行効率は低下するが、極端に大量のデータでも無い限り誤差の範囲なので原則として保守性を優先するべきである
ループ処理を関数/メソッドに切り出す
- 巨大なループは大抵多重ループになっていることが多い
- 大半の多重ループは、1段目のループで得られた値に対して2段目のループで処理を行うものばかりである
- ↓は典型的な例
def example(values1, values2):
result = []
for v1 in values1:
for v2 in values2:
if v2["xxx"] == v1["xxx"]:
#
# 何かしらの処理
#
return result
- ↑のような処理は1段目のループで得られた値を2段目のループ内で行われている処理のパラメータにしているだけである。
- したがって、2段目のループを関数に切り出してしまうのがよい
def example(values1, values2):
result = []
for v1 in values1:
result.append(inner_loop_function(v1, values2))
return result
def inner_loop_function(item, value_list):
result = []
for v in value_list:
if v["xxx"] == item["xxx"]:
#
# 何かしらの処理
#
return result
- 関数に切り出すことで見掛け上のコード量は増加するが、それぞれの処理が分割されることで、コードの可読性、保守性、テスト容易性が向上している
- ループ内の処理が別関数に切り出されることで、ループ内のロジックのテストがやりやすくなる
- 更に、処理に明確な名前を定義することで機能の役割がより明示的になる為、コードの理解しやすさも向上する
- この考え方を拡張し、ループ対象のリストをクラスにしてしまい、ループ処理はメソッドとして実装することも有用である(参照: 配列/連想配列はラップせよ)
5. 注意事項
処理の目的を明確にすることが重要
- ループの目的がデータのフィルタリングの為なのか、あるいは反復的なデータ加工の為なのか明確にすることが重要
- 処理の目的が明確になれば、目的ごとに処理を関数化することで巨大ループを分解できる
6. 関連
参考資料