LBYL vs EAFP:条件分岐と例外処理の使い分け(Python)

Python でエラー処理を書くとき、2 つのアプローチがある。LBYL(Look Before You Leap:跳ぶ前に見よ)は事前に条件をチェックする方法、EAFP(Easier to Ask for Forgiveness than Permission:許可を求めるより許しを請う方が簡単)は例外処理に任せる方法だ。Python では EAFP が推奨されることが多いが、常にそうとは限らない。両者の使い分けを解説する。

LBYL:事前チェック方式

LBYL は、操作を実行する前に条件をチェックするスタイルだ。

# LBYL スタイル
def get_value_lbyl(data, key):
    if key in data:
        return data[key]
    else:
        return None

# ファイル操作の例
import os

def read_file_lbyl(path):
    if os.path.exists(path):
        if os.path.isfile(path):
            with open(path) as f:
                return f.read()
    return None

C 言語や Java など、例外処理のコストが高い言語ではこのスタイルが一般的だ。

EAFP:例外処理方式

EAFP は、まず実行してみて、失敗したら例外で処理するスタイルだ。

# EAFP スタイル
def get_value_eafp(data, key):
    try:
        return data[key]
    except KeyError:
        return None

# ファイル操作の例
def read_file_eafp(path):
    try:
        with open(path) as f:
            return f.read()
    except (FileNotFoundError, IsADirectoryError):
        return None

Python では例外処理のコストが比較的低く、EAFP が推奨されることが多い。

EAFP が推奨される理由

Python で EAFP が好まれる理由はいくつかある。

レースコンディションの回避

LBYL では、チェックと操作の間に状態が変わる可能性がある(TOCTOU 問題)。EAFP ではアトミックに処理される。

コードの簡潔さ

事前チェックが複雑になる場合、例外処理の方がシンプルに書ける。

# LBYL のレースコンディション問題
import os

def process_file_lbyl(path):
    if os.path.exists(path):  # チェック時点では存在する
        # この間に別プロセスがファイルを削除するかもしれない
        with open(path) as f:  # FileNotFoundError の可能性!
            return f.read()

# EAFP ならレースコンディションを気にしなくてよい
def process_file_eafp(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError:
        return None

LBYL が適切な場合

EAFP が万能というわけではない。以下の場合は LBYL が適切だ。

# 例 1:例外を発生させること自体が副作用を持つ場合
class DatabaseConnection:
    def execute(self, query):
        # 例外が発生するとロールバックが必要など
        pass

def safe_execute_lbyl(conn, query):
    if conn.is_connected():
        return conn.execute(query)
    return None

# 例 2:失敗が頻繁に起きる場合
def get_user_age_lbyl(users, user_id):
    # ユーザーが存在しないことが多いなら LBYL が効率的
    if user_id in users:
        return users[user_id].get("age")
    return None

# 例 3:チェックが安価で、操作が高価な場合
def process_large_data_lbyl(data):
    if data is not None and len(data) > 0:
        # 重い処理を開始する前にチェック
        return expensive_operation(data)
    return None

パフォーマンスの違い

成功率が高い場合は EAFP が効率的だが、失敗率が高い場合は LBYL が効率的になることがある。

import timeit

data = {i: i for i in range(1000)}

# 存在するキーを取得(成功率 100%)
def lbyl_success():
    for i in range(1000):
        if i in data:
            _ = data[i]

def eafp_success():
    for i in range(1000):
        try:
            _ = data[i]
        except KeyError:
            pass

# EAFP の方がわずかに速い(try のオーバーヘッドが小さい)
print(timeit.timeit(lbyl_success, number=1000))
print(timeit.timeit(eafp_success, number=1000))
# 存在しないキーを取得(失敗率 100%)
def lbyl_fail():
    for i in range(1000, 2000):
        if i in data:
            _ = data[i]

def eafp_fail():
    for i in range(1000, 2000):
        try:
            _ = data[i]
        except KeyError:
            pass

# LBYL の方が速い(例外生成のコストがない)
print(timeit.timeit(lbyl_fail, number=1000))
print(timeit.timeit(eafp_fail, number=1000))

実践的な使い分け指針

EAFP を使う場面

操作が成功する可能性が高い。事前チェックと操作の間にレースコンディションがありうる。事前チェックのコストが高い。

LBYL を使う場面

操作が失敗する可能性が高い。例外発生自体が副作用を持つ。チェックが安価で操作が高価。可読性が向上する。

両方を組み合わせる

実際のコードでは、両方を適切に組み合わせることが多い。

def process_user_data(users, user_id, field):
    # LBYL:基本的な型チェック
    if not isinstance(users, dict):
        raise TypeError("users must be a dict")
    
    if not user_id:
        return None
    
    # EAFP:辞書アクセス
    try:
        user = users[user_id]
        return user[field]
    except KeyError:
        return None
    except TypeError:
        # user が dict でなかった場合
        return None
# より実践的な例:API クライアント
class APIClient:
    def get_user(self, user_id):
        # LBYL:明らかな無効入力を事前に弾く
        if not user_id or not isinstance(user_id, (str, int)):
            raise ValueError("Invalid user_id")
        
        # EAFP:ネットワーク操作は例外処理
        try:
            response = self._request(f"/users/{user_id}")
            return response.json()
        except RequestError as e:
            logger.error(f"Failed to get user {user_id}: {e}")
            return None

hasattr と getattr の例

属性アクセスでも両方のスタイルが見られる。

# LBYL スタイル
def call_method_lbyl(obj, method_name):
    if hasattr(obj, method_name):
        method = getattr(obj, method_name)
        if callable(method):
            return method()
    return None

# EAFP スタイル
def call_method_eafp(obj, method_name):
    try:
        method = getattr(obj, method_name)
        return method()
    except AttributeError:
        return None
    except TypeError:  # callable でなかった
        return None

# よりシンプル:getattr のデフォルト値を使う
def call_method_simple(obj, method_name):
    method = getattr(obj, method_name, None)
    if method is not None and callable(method):
        return method()
    return None

まとめ

LBYL と EAFP はどちらも有効なアプローチであり、状況に応じて使い分けるべきだ。

Python のイディオム

Python コミュニティでは EAFP が好まれる傾向があるが、それは「常に EAFP を使え」という意味ではない。

判断基準

成功率、パフォーマンス、副作用、可読性を総合的に判断する。迷ったら両方書いてみて、より明確な方を選ぶ。

大切なのはコードの意図が明確に伝わることだ。どちらのスタイルでも、一貫性を持って使うことが重要である。