try-except を条件分岐代わりに使うパターン(Python)

Python では try-except を条件分岐の代わりに使うパターンがある。特に「許可を求めるより許しを請う方が簡単」(EAFP)の思想に基づき、まず実行してみて失敗したら対処するというスタイルだ。この記事では、try-except を条件分岐的に使う実践パターンを紹介する。

辞書アクセス:KeyError をキャッチ

辞書からキーを取得するとき、キーの存在確認を省略して例外で処理できる。

data = {"name": "Alice", "age": 30}

# 条件分岐スタイル
def get_with_if(data, key):
    if key in data:
        return data[key]
    return "デフォルト"

# try-except スタイル
def get_with_try(data, key):
    try:
        return data[key]
    except KeyError:
        return "デフォルト"

# 最もシンプル:dict.get を使う
def get_with_method(data, key):
    return data.get(key, "デフォルト")

dict.get() が最もシンプルだが、デフォルト値を動的に計算したい場合は try-except が有用だ。

def get_or_compute(cache, key, compute_func):
    try:
        return cache[key]
    except KeyError:
        value = compute_func(key)
        cache[key] = value
        return value

# 使用例
cache = {}
result = get_or_compute(cache, "x", lambda k: expensive_computation(k))

型変換:ValueError をキャッチ

ユーザー入力を数値に変換するとき、try-except パターンが自然だ。

def parse_int_with_if(s):
    # 条件分岐:事前チェックが複雑
    if s.lstrip("-").isdigit():
        return int(s)
    return None

def parse_int_with_try(s):
    # try-except:シンプル
    try:
        return int(s)
    except ValueError:
        return None

# テスト
print(parse_int_with_try("42"))     # 42
print(parse_int_with_try("-10"))    # -10
print(parse_int_with_try("3.14"))   # None
print(parse_int_with_try("hello"))  # None

float への変換も同様だ。

def parse_number(s):
    # まず int を試し、ダメなら float を試す
    try:
        return int(s)
    except ValueError:
        try:
            return float(s)
        except ValueError:
            return None

print(parse_number("42"))    # 42 (int)
print(parse_number("3.14"))  # 3.14 (float)
print(parse_number("abc"))   # None

属性アクセス:AttributeError をキャッチ

オブジェクトの属性が存在するかどうかを確認する場合。

class User:
    def __init__(self, name):
        self.name = name

class Admin(User):
    def __init__(self, name, permissions):
        super().__init__(name)
        self.permissions = permissions

def get_permissions_with_if(user):
    if hasattr(user, "permissions"):
        return user.permissions
    return []

def get_permissions_with_try(user):
    try:
        return user.permissions
    except AttributeError:
        return []

# 使用例
user = User("Alice")
admin = Admin("Bob", ["read", "write"])

print(get_permissions_with_try(user))   # []
print(get_permissions_with_try(admin))  # ["read", "write"]

インデックスアクセス:IndexError をキャッチ

リストの範囲外アクセスを安全に処理する。

def safe_get_with_if(lst, index):
    if 0 <= index < len(lst):
        return lst[index]
    return None

def safe_get_with_try(lst, index):
    try:
        return lst[index]
    except IndexError:
        return None

# 負のインデックスも考慮する場合、if は複雑になる
def safe_get_with_if_full(lst, index):
    if -len(lst) <= index < len(lst):
        return lst[index]
    return None

# try-except なら負のインデックスも自然に処理できる
items = ["a", "b", "c"]
print(safe_get_with_try(items, 10))   # None
print(safe_get_with_try(items, -1))   # "c"
print(safe_get_with_try(items, -10))  # None

ファイル操作:複数の例外をキャッチ

ファイル操作では複数の例外が発生しうるため、try-except が適している。

def read_json_config(path):
    import json
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError:
        return {}  # ファイルがなければ空の設定
    except json.JSONDecodeError:
        return {}  # JSON が壊れていれば空の設定
    except PermissionError:
        raise  # 権限エラーは再送出して呼び出し側に任せる

イテレーション:StopIteration をキャッチ

イテレータから次の要素を安全に取得する。

def first_or_default(iterable, default=None):
    try:
        return next(iter(iterable))
    except StopIteration:
        return default

# 使用例
print(first_or_default([1, 2, 3]))  # 1
print(first_or_default([]))         # None
print(first_or_default([], "empty")) # "empty"

ただし、next() にはデフォルト値を指定できるため、通常はそちらを使う。

def first_or_default_simple(iterable, default=None):
    return next(iter(iterable), default)

複数の例外を一度にキャッチ

関連する複数の例外をまとめて処理できる。

def safe_divide(a, b):
    try:
        return a / b
    except (ZeroDivisionError, TypeError):
        return None

print(safe_divide(10, 2))    # 5.0
print(safe_divide(10, 0))    # None
print(safe_divide(10, "a"))  # None

else 節と finally 節の活用

try-except には elsefinally を組み合わせられる。

def process_data(data):
    try:
        result = transform(data)
    except ValueError as e:
        print(f"変換エラー: {e}")
        return None
    else:
        # 例外が発生しなかった場合のみ実行
        print("変換成功")
        return result
    finally:
        # 成功・失敗に関わらず実行
        cleanup()

else 節は、try ブロック内のコードを最小限に保ちつつ、成功時の処理を明確に分離できる。

パターン:複数の方法を順に試す

複数の変換方法を順に試すパターン。

def parse_date(s):
    from datetime import datetime
    
    formats = [
        "%Y-%m-%d",
        "%Y/%m/%d",
        "%d-%m-%Y",
        "%m/%d/%Y",
    ]
    
    for fmt in formats:
        try:
            return datetime.strptime(s, fmt)
        except ValueError:
            continue
    
    return None  # どのフォーマットにも合わなかった

print(parse_date("2024-01-15"))  # 成功
print(parse_date("01/15/2024"))  # 成功
print(parse_date("invalid"))     # None

注意点:例外を広くキャッチしすぎない

except Exception や素の except: は避けるべきだ。

# 悪い例:すべての例外をキャッチ
def bad_parse(s):
    try:
        return int(s)
    except:  # KeyboardInterrupt なども捕まえてしまう
        return None

# 悪い例:Exception は広すぎる
def still_bad_parse(s):
    try:
        return int(s)
    except Exception:  # プログラミングエラーも隠してしまう
        return None

# 良い例:具体的な例外のみキャッチ
def good_parse(s):
    try:
        return int(s)
    except ValueError:
        return None

まとめ

try-except を条件分岐代わりに使うパターンは Python では一般的だ。

適しているケース

辞書・リストアクセス、型変換、ファイル操作など、失敗の可能性がある操作。複数の例外をまとめて処理したい場合。

注意すべきこと

キャッチする例外は具体的に指定する。プログラミングエラー(TypeError、AttributeError の一部)は通常キャッチしない。パフォーマンスが重要で失敗率が高い場合は事前チェックを検討する。

try-except は Python の EAFP 文化を体現するイディオムであり、適切に使えばコードが簡潔で堅牢になる。