早期リターンとガード節によるネスト削減(Python)

深くネストした if 文は、コードの可読性を著しく損なう。早期リターン(early return)とガード節(guard clause)を使えば、ネストを減らしてフラットな構造を維持できる。この記事では、ネスト削減のテクニックと、その適用判断について解説する。

問題:深いネスト

典型的な例として、ユーザー認証とデータ処理を行う関数を見てみよう。

# 深いネスト(悪い例)
def process_order(user, order):
    if user is not None:
        if user.is_authenticated:
            if user.has_permission("create_order"):
                if order is not None:
                    if order.is_valid():
                        if order.items:
                            # ここでようやく本題の処理
                            total = sum(item.price for item in order.items)
                            return {"status": "success", "total": total}
                        else:
                            return {"status": "error", "message": "No items"}
                    else:
                        return {"status": "error", "message": "Invalid order"}
                else:
                    return {"status": "error", "message": "Order is None"}
            else:
                return {"status": "error", "message": "Permission denied"}
        else:
            return {"status": "error", "message": "Not authenticated"}
    else:
        return {"status": "error", "message": "User is None"}

ネストが 6 段階にもなり、本題の処理がどこにあるか見つけにくい。条件の else 分岐がどの if に対応するかも追いづらい。

解決策:ガード節と早期リターン

条件を満たさない場合に早期リターンする「ガード節」を使うと、ネストを解消できる。

# ガード節を使った改善(良い例)
def process_order(user, order):
    # ガード節:異常系を先に処理
    if user is None:
        return {"status": "error", "message": "User is None"}
    
    if not user.is_authenticated:
        return {"status": "error", "message": "Not authenticated"}
    
    if not user.has_permission("create_order"):
        return {"status": "error", "message": "Permission denied"}
    
    if order is None:
        return {"status": "error", "message": "Order is None"}
    
    if not order.is_valid():
        return {"status": "error", "message": "Invalid order"}
    
    if not order.items:
        return {"status": "error", "message": "No items"}
    
    # ここから本題(正常系)
    total = sum(item.price for item in order.items)
    return {"status": "success", "total": total}

ネストが完全にフラットになり、各ガード節が何をチェックしているか一目で分かる。

ガード節のパターン

ガード節には主に 3 つのパターンがある。

None チェック

引数や変数が None でないことを確認する。最も基本的なガード。

前提条件チェック

処理を続行するための条件(認証済み、権限あり、データ有効など)を確認する。

境界条件チェック

空リスト、範囲外の値、不正な状態などをチェックする。

def calculate_average(numbers):
    # None チェック
    if numbers is None:
        raise ValueError("numbers cannot be None")
    
    # 境界条件チェック
    if not numbers:
        return 0.0
    
    # 本題
    return sum(numbers) / len(numbers)

例外を使ったガード

エラー時に値を返すのではなく、例外を投げるパターンもある。

def process_order(user, order):
    if user is None:
        raise ValueError("User cannot be None")
    
    if not user.is_authenticated:
        raise PermissionError("User is not authenticated")
    
    if not user.has_permission("create_order"):
        raise PermissionError("User lacks create_order permission")
    
    if order is None or not order.is_valid():
        raise ValueError("Invalid order")
    
    if not order.items:
        raise ValueError("Order has no items")
    
    total = sum(item.price for item in order.items)
    return {"status": "success", "total": total}

例外を使うと、呼び出し側でエラー処理を一元化できる。

ループ内での早期 continue

ループ内では continue を使って早期に次のイテレーションに進むことで、ネストを減らせる。

# ネストした例(悪い例)
def process_items(items):
    results = []
    for item in items:
        if item is not None:
            if item.is_active:
                if item.price > 0:
                    results.append(item.price * 1.1)
    return results

# 早期 continue を使った改善(良い例)
def process_items(items):
    results = []
    for item in items:
        if item is None:
            continue
        if not item.is_active:
            continue
        if item.price <= 0:
            continue
        
        results.append(item.price * 1.1)
    return results

複合条件の分解

複雑な複合条件も、ガード節として分解できる。

# 複合条件(読みにくい)
def can_access(user, resource):
    if user and user.is_authenticated and (user.is_admin or resource.owner == user.id):
        return True
    return False

# ガード節で分解(読みやすい)
def can_access(user, resource):
    if user is None:
        return False
    
    if not user.is_authenticated:
        return False
    
    if user.is_admin:
        return True
    
    if resource.owner == user.id:
        return True
    
    return False

注意点:過度な早期リターン

早期リターンを使いすぎると、逆に読みにくくなることもある。

# 過度な早期リターン(やりすぎ)
def get_discount(price, user):
    if price <= 0:
        return 0
    
    if user is None:
        return 0
    
    if not user.is_member:
        return 0
    
    if user.membership_years < 1:
        return price * 0.05
    
    if user.membership_years < 3:
        return price * 0.10
    
    if user.membership_years < 5:
        return price * 0.15
    
    return price * 0.20

この場合、後半の条件分岐は早期リターンより if-elif の方が意図が明確だ。

# 適切なバランス
def get_discount(price, user):
    # ガード節
    if price <= 0:
        return 0
    if user is None or not user.is_member:
        return 0
    
    # 本題の条件分岐は if-elif で
    years = user.membership_years
    if years >= 5:
        rate = 0.20
    elif years >= 3:
        rate = 0.15
    elif years >= 1:
        rate = 0.10
    else:
        rate = 0.05
    
    return price * rate

早期リターンと単一出口原則

かつては「関数の出口は 1 つにすべき」という単一出口原則(single exit point)が推奨されていた。しかし現代では、早期リターンを使う方が可読性が高いとされることが多い。

単一出口原則

関数の最後でのみ return する。制御フローが予測しやすい。ネストが深くなりやすい。

早期リターン

異常系を先に処理して return。ネストがフラット。出口が複数になるが、各ガードが明確。

リソース管理が必要な場合(ファイルハンドルのクローズなど)は、with 文や try-finally を使えば、早期リターンでも安全にリソースを解放できる。

まとめ

早期リターンとガード節は、深いネストを解消する強力なテクニックだ。

異常系を先にガード節で処理する
ループ内では continue で早期に次へ進む
複合条件は分解して個別にチェック
ただし使いすぎず、本題の条件分岐は if-elif を使う

コードを書くときは「ハッピーパス(正常系)をネストの外に出す」ことを意識すると、自然と読みやすい構造になる。