巨大ループ
1. 状況
ループの中で様々な処理が行われており、ループが巨大化している
- ループ内で様々な処理が行われている為、ループのネストやループ自体の肥大が発生している
- ループやIF文のネストが発生し、可読性/保守性が低下している
2. 問題
保守性が低下する
- 逐次的に処理が行われている為、新たな処理を追加する場合、既存のコードに新たなコードを挿入する必要がある
- 既存のコードに処理を挿入していくと、関数が肥大化し保守性が低下してしまう
- 巨大なループの中で様々なロジックが実行されると、ループ毎に変数の状態や組合せが変化する為、処理を追うことが困難になる
3. 原因
プログラムの構造化不足
- 経験の浅いプログラマが処理を構造化せず一筆書きでロジックを書き下すと巨大ループが発生しやすい
- プログラマが巨大な処理を一筆書きで記述してしまうのは、良いコードについての知見や良いコードを記述するスキルが不足していることが主要な原因であることが多い
4. 対策
filter/map関数を利用する
- ループが多重化する時は、1段目のループで得られた値を元に別のリストからデータを抽出したい場合が多い
- ↓の例では2つのリストから一致するアイテムを抽出したいだけだが、既にネストが3段になってしまっている
def main():
result = []
list_a = get_list_a()
list_b = get_list_b()
for list_a_item in list_a:
for list_b_item in list_b:
if list_b_item["name"] == list_a_item["name"]:
result.append(list_b_item["id"])
return result
- 2つのリストからデータを抽出する際はfilter関数やmap関数でループを抽象化することで見掛け上のネストを除去することができる
- Pythonならリスト内包記法でもよい
def main():
result = []
list_a = get_list_a()
list_b = get_list_b()
for list_a_item in list_a:
_tmp = list(filter(lambda x: x['name'] == list_a_item['name'], list_b))
result.extend(list(map(lambda x: x["id"], _tmp)))
return result
ループ処理を関数/メソッドに切り出す
- シンプルな処理であればfilter/map関数 + クロージャを使えばよいが、抽出したデータを更に加工する必要がある場合は、別関数に切り出した方が可読性が高くなることもある
def main():
result = []
list_a = get_list_a()
list_b = get_list_b()
for list_a_item in list_a:
tmp1 = filter_by_name(list_a_item['name'], list_b)
tmp2 = extract_for_id(tmp1)
result.extend(tmp2)
return result
def filter_by_name(name, list_):
return list(filter(lambda x: x['name'] == name, list_))
def extract_for_id(list_):
return list(map(lambda x: x['id'], list_)
- 関数に切り出すことでコード量は増加するが、それぞれの処理はより凝集化される
- 関数に切り出されたことで各処理のスコープが明確に分離され、コードを読む時の関心事の量をコントロールできる
- 名前付き関数として定義することで機能の役割がより明示的になり、可読性の向上にも貢献している
ループ処理自体をクラスに切り出す
- 巨大ループはデータのリストに対して複雑な処理を手続き的に記述することで発生する
- 特定のデータリストに対する処理が複雑化しているのであれば、対象のデータをクラス化することを検討する
- クラス化することによって、ロジックに現れる巨大ループをメソッドに分解しつつ、それらの処理を隠蔽/カプセル化することができる
- 与えられたデータリストに対してフィルタリングを施して新たなデータリストを作り出すのであれば
Builder
パターンが適した構造になる
class MyDataBuilder:
def __init__(self, list_a, list_b):
self.list_a = list_a
self.list_b = list_b
def build(self):
result = []
for i in self.list_a:
tmp1 = self._filter_by_name(i['name'], self.list_b)
tmp2 = self._extract_for_id(tmp1)
result.extend(tmp2)
return result
def _filter_by_name(self, name, list_):
return list(filter(lambda x: x['name'] == name, list_))
def _extract_for_id(self, list_):
return list(map(lambda x: x['id'], list_)
if __name__=='__main__':
list_a = get_list_a()
list_b = get_list_b()
builder = MyDataBuilder(list_a, list_b)
result = builder.build()
print(result)
5. 注意事項
処理の目的を明確にすることが重要
- ループの目的がデータのフィルタリングの為なのか、あるいは反復的なデータ加工の為なのか明確にすることが重要
- 処理の目的が明確になれば、目的ごとに処理を関数化することで巨大ループを分解できる
6. 関連
参考資料
Appendix-1
事例1
def main():
result = []
list_ = [...]
for item in list_:
if item['type'] == 'Type01':
if item['price'] >= 300 and item['price'] <= 500:
result.append(item)
if item['type'] == 'Type02':
if item['price'] >= 400:
result.append(item)
return result
def main():
result = []
list_ = [...]
tmp1 = extract_type(list_, 'Type01')
tmp2 = filter_price(tmp1, 300)
tmp3 = filter_price(tmp2, 500)
result.extend(tmp3)
tmp4 = extract_type(list_, 'Type02')
tmp5 = filter_price(tmp4, 400)
result.extend(tmp5)
return result
def extract_type(list_, type_):
result = []
for item in list_:
if item['type'] == type_:
result.append(item)
return result
def filter_price(list_, price):
result = []
for item in list_:
if item['price'] >= price:
result.append(item)
return result
class Filter:
def __init__(self, value=[]):
self._value = value
@property
def value(self):
return self._value
def apply(self, function, *args, **kwargs):
result = function(self.value, *args, **kwargs)
return Filter(result)
def extract_type(list_, type_):
result = []
for item in list_:
if item['type'] == type_:
result.append(item)
return result
def filter_price(list_, price):
result = []
for item in list_:
if item['price'] >= price:
result.append(item)
return result
def main():
result = []
list_ = [...]
result.extend(
Filter(data)
.apply(extract_type, 'Type01')
.apply(filter_price, 300)
.apply(filter_price, 500)
.value)
result.extend(
Filter(data)
.apply(extract_type, 'Type02')
.apply(filter_price, 400)
.value)
print(result)