三項演算子のネストと可読性のトレードオフ(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 への書き直しを検討すべきだ。コードは書く時間より読む時間の方が長い。将来の自分や他の開発者のために、可読性を優先しよう。



