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