はじめに
リーダブルコード(オライリー)では、「読みやすさの基本定理」を下記のように定義している。
コードは他の人が最短時間で理解できるように書かなければいけない。
ここで、「他の人」とは、何週間後の自分自身も含む。また、「理解できる」とは、バグに気づき、コードの修正を行い、他のコードとの連携を設計することができることを指す。
本記事では特に、「ループとロジックの単純化」によるリーダブルコード達成方法を取りまとめる。
(前回記事:第Ⅰ部 表面上の改善)
ループとロジックの単純化
ループとロジックの単純化
複雑な制御フローや巨大な式はコード理解の妨げになり、その部分を理解するためだけに多くの時間と脳内メモリを割かなければならない。つまり、ループとロジックの単純化はリーダブルコードを目指すうえでの大きな伸びしろと言える。
読み手が余計な脳内メモリを使用せずに自然に読み進められるような、単純化されたループとロジックを作成するために主に下記の3点が重要である。
- 制御フローは可能な限り、上から下へ読み進められるようにする
- 巨大な式や処理を分割することで、コードを飲み込みやすくする
- 重要な変数を狭いスコープで活用することで、脳のメモリが圧迫されないようにする
以下でこの3項目を達成するために意識すべき点を説明する。
制御フローを読みやすくする
一般的に複雑な(理解しにくい)制御フローは、下記のような問題点を抱えている。
複雑な制御フロー
- 読み進める中で目線を下から上に戻す必要が生じる
- ネストがやたら深い
- 最後まで読まないと、返り値の内容が想像つかない
このような問題点を解消し、「自然」にコードを上から下へ読み進められるようにするため、下記4点に注意しながら制御フローを構成すると良い。
-
条件式は「変数」(数学記号)「定数」の順番で記載する
anti-pattern15_000 =< price_sum #「定数 ≦ 変数」となっており、理解しづらい
design-patternprice_sum => 15_000 #「変数 ≧ 定数」となっており、左から右に自然と流れる
-
if-else文は「肯定系」「単純」「目立つもの」を先に処理する
競合問題
これらの3項目で競合が起きたときは、高い視座から優先度を判断する。
-
関数の返り値は早めに返す(returnは複数あってもよい!)
anti-patterndef list_files_and_folders(path_target): """対象フォルダ内のファイル名とフォルダ名をリストとして取得する。 (省略) """ # path_target のパスが存在するかチェック if not os.path.exists(path_target): files = [] # <--返す用意ができているのに、 folders = [] # <--最後のreturnまで待っている else: # フォルダ内の全アイテムを取得 items = os.listdir(path_target) # ファイルとフォルダを分けてリストに格納 files = [item for item in items if os.path.isfile(os.path.join(path_target, item))] folders = [item for item in items if os.path.isdir(os.path.join(path_target, item))] return files, folders
design-patterndef list_files_and_folders(path_target): """対象フォルダ内のファイル名とフォルダ名をリストとして取得する。 (省略) """ # path_target のパスが存在するかチェック if not os.path.exists(path_target): return [], [] # <--返す用意ができたならすぐに返す! # フォルダ内の全アイテムを取得 items = os.listdir(path_target) # ファイルとフォルダを分けてリストに格納 files = [item for item in items if os.path.isfile(os.path.join(path_target, item))] folders = [item for item in items if os.path.isdir(os.path.join(path_target, item))] return files, folders
-
ネストを浅くするために、新鮮な目で制御フロー全体を見る
- 「失敗ケース」を早めに返す
anti-pattern
def huga(): if is_a: if is_b: return a # is_aかつis_bでようやく返す else: return b else: return c # is_aでない時点でcを返すことはわかっている
design-patterndef huga(): if not is_a: return c # 失敗ケースを先に出すことでネストが浅くなる if is_boundary_z: return a else: return b
- continueを使う
anti-pattern
for num in range(num_sample): if is_a: cnt += 1 if is_b: # is_aかつis_bのネストを通って初めてvalに加算できる val += calcurate_hoge(num)
design-patternfor num in range(num_sample): if not is_a: continue cnt += 1 if not is_b: continue # continueを使用することでネストが浅くなる val += calcurate_hoge(num)
- 「失敗ケース」を早めに返す
-
複雑なロジックは「反対側」からロジックを考えてみる
- 例:リスト内の重複した値を見つけるロジック
正当なロジック「重複を見つける」
ネストが深くなり、コードが理解しづらい。
def find_duplicates(lst):
duplicates = []
for i in range(len(lst)):
for j in range(i + 1, len(lst)):
if lst[i] == lst[j] and lst[i] not in duplicates:
duplicates.append(lst[i])
return duplicates
反対側のロジック「重複しない値を見つける」
シンプルな構成で理解しやすい。
def find_duplicates(lst):
seen = set()
unique = set()
for item in lst:
if item in seen:
unique.discard(item)
else:
seen.add(item)
unique.add(item)
return list(seen - unique)
巨大な式を分割する
コードを飲み込みやすくするための処理や分割として、「説明変数」と「要約変数」の活用が重要である。
種類 | 内容 | メリット |
---|---|---|
説明変数 | 具体的な値やデータそのものを説明するために命名される変数 | マジックナンバー対策、可読性向上 |
要約変数 | 式の各項や処理を要約する変数 | 可読性向上 |
price_top_city = (
df_all_stores_sales
.groupby("city")["price"]
.sum()
.sort_values("price")
.iloc[0]
)
TP = len(df.loc[(df["predict"] == 1) & (df["test"] == 1)])
FP = len(df.loc[(df["predict"] == 1) & (df["test"] == 0)])
precision = TP / (TP + FP)
重要な変数を狭いスコープで活用する
適切に変数を定義することで、リーダブルコードに近づくことがわかった。一方で、無神経に変数を扱うことで脳内メモリを圧迫してしまい、コードの理解の妨げになる恐れがある。
無神経な変数
- 存在意義の薄い変数
- コードの広範囲に現れる変数
- 何度も更新される変数
- 存在意義の薄い変数
中間結果を保持するだけの変数や制御フロー変数(大抵breakやcontinueで置き換えられる) - コードの広範囲に表れる変数
何行もスクロールして内容を見返しにいく必要がある変数 - 何度も更新される変数
更新される場所が多く、現在値の判断が難しい変数
終わりに
本記事では、リーダブルコーディングのための「ループとロジックの単純化」方法について紹介した。
以降の記事では、求められる「コードの再構成」について紹介していければと思う。