三項演算子のネストと可読性のトレードオフ(Python)

Python の三項演算子(条件式)は value_if_true if condition else value_if_false という構文で、1 行で条件分岐を表現できる。便利な機能だが、ネストすると急速に可読性が低下する。どこまでが許容範囲で、どこからがリファクタリング対象なのか、具体例を通じて考えていく。

三項演算子の基本

まず基本形を確認しておこう。

# 基本形
status = "adult" if age >= 18 else "minor"

# if-else 文で書いた場合
if age >= 18:
    status = "adult"
else:
    status = "minor"

単純なケースでは三項演算子の方が簡潔で、変数への代入が 1 行で完結するメリットがある。

1 段階のネスト:ギリギリ許容範囲

条件が 3 つに分岐する場合、三項演算子を 1 段階ネストすることになる。

# 1 段階ネスト
grade = "A" if score >= 90 else "B" if score >= 70 else "C"

# 読みやすく整形
grade = (
    "A" if score >= 90 else
    "B" if score >= 70 else
    "C"
)

この程度なら、改行とインデントで整形すれば許容範囲だ。ただし、条件が複雑になると話が変わる。

# 条件が複雑になると厳しい
result = (
    "excellent" if score >= 90 and attendance >= 0.95 else
    "good" if score >= 70 and attendance >= 0.80 else
    "needs_improvement"
)

2 段階以上のネスト:リファクタリング対象

2 段階以上ネストすると、ほぼ確実に可読性が破綻する。

# 悪い例:2 段階ネスト
category = (
    "infant" if age < 1 else
    "toddler" if age < 3 else
    "child" if age < 13 else
    "teenager" if age < 20 else
    "adult"
)

一見整っているように見えるが、else 句がどの if に対応するのか追いづらい。これは辞書やマッピング、あるいは関数で書き直すべきだ。

# 改善案 1:bisect を使う
import bisect

def get_category(age):
    breakpoints = [1, 3, 13, 20]
    categories = ["infant", "toddler", "child", "teenager", "adult"]
    return categories[bisect.bisect(breakpoints, age)]

# 改善案 2:明示的な if-elif
def get_category(age):
    if age < 1:
        return "infant"
    elif age < 3:
        return "toddler"
    elif age < 13:
        return "child"
    elif age < 20:
        return "teenager"
    else:
        return "adult"

ネストの方向による可読性の違い

三項演算子のネストには 2 つの方向がある。else 側にネストするか、条件側にネストするかだ。

# else 側ネスト(比較的読みやすい)
result = "A" if x > 0 else "B" if x == 0 else "C"

# 条件側ネスト(非常に読みにくい)
result = "yes" if (True if condition else False) else "no"

# value 側ネスト(これも厳しい)
result = ("inner_a" if inner_cond else "inner_b") if outer_cond else "outer"

else 側へのネストは比較的読みやすいが、条件側や value 側へのネストは避けるべきだ。

三項演算子とリスト内包表記の組み合わせ

リスト内包表記の中で三項演算子を使うことも多い。

# 許容範囲:単純な変換
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]

# 危険:ネストした三項演算子
labels = [
    "zero" if n == 0 else "positive" if n > 0 else "negative"
    for n in numbers
]

# さらに危険:内包表記自体もネスト
matrix = [
    ["X" if cell else "O" if cell is None else "." for cell in row]
    for row in grid
]

リスト内包表記と三項演算子のネストが組み合わさると、1 行が非常に長くなり、デバッグも困難になる。

# 改善案:関数に切り出す
def cell_symbol(cell):
    if cell:
        return "X"
    elif cell is None:
        return "O"
    else:
        return "."

matrix = [[cell_symbol(cell) for cell in row] for row in grid]

三項演算子と代入式(ウォルラス演算子)

Python 3.8 で導入された代入式 := と三項演算子を組み合わせると、さらに複雑になりうる。

# 微妙な例:代入式と三項演算子
result = value if (value := compute()) is not None else default

# より危険:複数の代入式
output = (
    processed if (processed := transform(data := fetch())) else
    fallback(data)
)

代入式は便利だが、三項演算子と組み合わせると読みにくくなりやすい。

可読性を保つためのガイドライン

三項演算子の使用について、以下のガイドラインが参考になる。

使ってよいケース

単純な 2 分岐で、条件も値も短い場合。1 行に収まり、一目で意味が分かるもの。

避けるべきケース

1 段階を超えるネスト、条件が複雑な場合、リスト内包表記の中でさらにネスト、代入式との複合。

代替手段の比較

三項演算子のネストを避ける代替手段をまとめる。

手法適用場面特徴
if-elif-else複雑な条件分岐最も明確で読みやすい
辞書マッピング離散的な値の変換キーが有限で明確な場合
bisect範囲ベースの分類数値区間での分岐に最適
# 辞書マッピングの例
status_messages = {
    "pending": "お待ちください",
    "approved": "承認されました",
    "rejected": "却下されました",
}
message = status_messages.get(status, "不明なステータス")

# match 文(Python 3.10 以降)
match status:
    case "pending":
        message = "お待ちください"
    case "approved":
        message = "承認されました"
    case "rejected":
        message = "却下されました"
    case _:
        message = "不明なステータス"

まとめ

三項演算子は強力だが、ネストするほど可読性が低下する。1 段階のネストまでを上限とし、それを超える場合は関数への切り出し、辞書マッピング、if-elif-else への書き直しを検討すべきだ。コードは書く時間より読む時間の方が長い。将来の自分や他の開発者のために、可読性を優先しよう。