🐍

【Effective Python 第2版】項目9 while-else, for-else禁止論についての考察

に公開

最近Effective Pythonを読んでいます。
当書には、Pythonを使う上で有用な手法や考え方が90個掲載されています。
『Effective Python』は目次にピンときたら読むべき本 - note.nkmk.com

自分の認識と一致している項目が多いですが、「項目9 forループとwhileループの後のelseブロックは使わない」に関しては異なっていたので、改めてまとめてみようと思います。
なお、私の立場は次の通りです。

  • while-elseは「不要派」
  • for-elseは条件付き許容派

1. while-else, for-elseとは

while-else, for-elseの挙動を簡単に解説します。
それぞれ「while文の条件が不成立になった」「for文のループが最後まで実行された」ときだけ、elseブロックが実行されます。

  • while-elseの例文
value: int = 0
while value < 5:
    print(value)
    value += 2
    if value == 4:
        break
else:
    print('else')
実行結果
0
2
value: int = 1
while value < 5:
    print(value)
    value += 2
    if value == 4:
        break
else:
    print('else')
実行結果
1
3
else
  • for-elseの例文
values: list[int] = [0, 1, 2]
for value in values:
    print(value)
    if value == 2:
        break
else:
    print('else')
実行結果
0
1
2
values: list[int] = [0, 1]
for value in values:
    print(value)
    if value == 2:
        break
else:
    print('else')
実行結果
0
1
else

2. 禁止論の根拠

2.1. elseという単語について

while-else, for-else禁止論の大きな根拠の一つは、elseという単語の意味が混乱をもたらすことにあります。
elseは「それ以外」という意味です。
そのため、「何らかの意味で『正常』とされる状態があって、その状態にならなかったときにelseブロックが実行される」と認識することは自然でしょう。
if文においては、「if(及びelif)の条件が成立すること」を『正常』と捉えることに違和感はないでしょう。
そして、『正常』でなかった場合にelseブロックが実行されます。
しかし、while文で「条件が不成立になってwhile文を抜けること」、for文で「forループが最後まで実行されること」を『正常』と捉えてしまうと、混乱が生じます。

value: int = 1
while value < 5:
    print(value)
    value += 2
    if value == 4:
        break
else:
    # 『正常』でない:breakしたvalue == 4のとき?
    print(value)
    print('else')
実行結果
1
3
5
else
values: list[int] = [0, 1]
for value in values:
    print(value)
    if value == 2:
        break
else:
    # 『正常』でない:ループが最後まで実行されなかったとき?
    print('else')
実行結果
0
1
else

『正常』をこのように捉えると、上記コードではelseブロックは実行されないことになります。
しかし、実際には「条件が不成立になってwhile文を抜けたとき」「forループが最後まで実行されたとき」にelseブロックが実行されるため、真逆の挙動であると感じてしまうのです。

2.2. ループが1回も実行されなかったときの挙動

次のコードを実行した場合、'else'は出力されるでしょうか?

while False:
    pass
else:
    # 実行される?
    print('else')
values: list[int] = []
for value in values:
    pass
else:
    # 実行される?
    print('else')

答えは両方Yesです。
このようなパターンでは「while-elsefor-elseの処理が丸ごとスキップされる」と考えてしまい、直感的でないと感じてしまうようです。

3. なぜelseという単語が採用されたか

禁止論を深掘りする前に、elseが採用された経緯を考察します。

まず大きい理由として考えられるのは、「キーワードを増やしたくなかった」というものでしょう。
iffor, whileのように、言語側で特別な扱いをするため変数名などに使用できない文字列を「キーワード」と言います。
そして、Pythonではキーワードの一覧を次のようにして取得できます。

import keyword
for kw in keyword.kwlist:
    print(kw)
実行結果(Python3.12の場合)
False
None
True
and
as
assert
async
await
break
class
continue
def
del
elif
else
except
finally
for
from
global
if
import
in
is
lambda
nonlocal
not
or
pass
raise
return
try
while
with
yield

この中で、iffor, whileのようにインデントブロックを作成するものは次の11個です。

class
def
elif
else
except
finally
for
if
try
while
with

それでは、while-hoge, for-hogeという構文で処理の分岐を作成できるように設計してみましょう。
もちろん、新しいキーワードを導入することもできます。
しかし、そのキーワードと同じ変数名を使っていたコードは動かなくなってしまいますし、キーワード導入の設計コストも高いでしょう。
何より、上記キーワードと比べてこの分岐が登場する場面は少ないです。
それなら、なるべく既存のキーワードを流用したいところです。

では、上記キーワードで流用できそうなものはあるでしょうか?

意味が通らない
class
def
for
try
while
with

分岐条件は明確だから、わざわざ書かなくても…
elif
if

分岐条件に関わらず実行されそう
finally

残り
else
except

2つにまで絞られました。
英単語としての意味は、elseは「(何か別のものがあって)それ以外」、exceptは「何かを指定して、それを除外する」という意味になります。
今回は2択の条件分岐ですから、elseの方が状況に合いそうです。
よって、elseが第一候補になると言えます。

4. while-elseについての考察

4.1. while-elseが「本質的に」必要な場面

while-elseで実現したいのは、「while文の条件が不成立になってループを抜けたこと」と、「それ以外の要因でループを抜けたこと」を区別し、while文を抜けた後の処理を分岐させることです。
while文を条件不成立以外で抜ける構文にはbreak, return, raiseがあります。
yieldは「一時停止」であって抜けてはいないので除外します)
このうちreturnraiseに関しては関数自体を終了させるため、while文を抜けた後の処理は実行されません。
よって、ここではbreakだけを考えればよいです。

そして、breakwhile文「直下」に配置するとwhile文の存在意義を破壊します。

次の2つは同じ処理
while condition:
    function()
    break


if condition:
    function()

このことから、while文においてbreakは何らかの条件分岐の中に存在すると考えてよいでしょう。
そのため、while文は次の形式に一般化できます。
try-exceptなども条件分岐の一種なので、ここでは代表としてif文で表現します)

while condition:
    if break_condition:
        break_function()
        break

もしbreakを含むif文が無ければ、次のようになります。

while condition:
    pass

この場合、while文を抜けるにはconditionが不成立になるしかなく、「while文の条件が不成立になってループを抜けた」しか起こり得ません。
つまり「それ以外の要因でループを抜けた」が起きないため区別する必要が無く、while-elseも必要ありません。

以上の考察から、while-elseが本質的に必要とされるのは「while文の中にbreakを含んだ条件分岐があるとき」であると言えます。

while condition:
    if break_condition:
        break_function()
        break

ここにelseブロックを繫げると次のようになります。

while condition:
    if break_condition:
        break_function()
        break
else:
    else_function()

4.2. while-elseの可読性の低さ

前節で導出したwhile-elseの一般形について、私は「言語化できない読みづらさ」を感じていたのですが、最近ようやく説明できるようになってきました。

このwhile-else文を理解するには、次の6つが必要です。

  1. conditionの理解
  2. break_conditionの理解
  3. break_functionの理解
  4. else_functionの理解
  5. elseブロック実行条件の理解
  6. ループを抜ける条件の理解

elseブロック実行条件は、not conditionとなることです。
コードにはconditionしか書いてないので、自分の頭の中で反転させる必要があります。
更に、breakがあるということはwhile文を抜けた後の処理があるということですから、ループを抜ける条件も理解する必要があります。
その条件は、not condition or break_conditionです。
頭の中でconditionを反転させたうえ、break_conditionorを取らなければなりません。
この辺で私の頭はパンクします。

4.3. while-elseの代替表現

以上を踏まえ、while-elseの代替として次のような表現を提案します。
つまり、while文の条件をTrueで固定するのです。

while True:  # ここをTrueで固定
    if not condition:  # while condition:から移動
        else_function()  # elseブロック内の処理を移動
        break
    if break_condition:
        break_function()
        break

while-elseの一般形と変数名を揃えると上の通りですが、変数名を整理すると次のような表現になります。

while True:
    if break_condition_1:  # break_condition_1 = not condition
        break_function_1()  # break_function_1 = else_function
        break
    if break_condition_2:  # break_condition_2 = break_condition
        break_function_2()  # break_function_2 = break_function
        break

このwhile文を理解するには、次の5つが必要です。

  1. break_condition_1の理解
  2. break_function_1の理解
  3. break_condition_2の理解
  4. break_function_2の理解
  5. ループを抜ける条件の理解

ループを抜ける条件は、break_condition_1 or break_condition_2です。
while-elseのときのように、頭の中で反転させる必要がありません。
break_condition_1break_condition_2もコードに書かれているのをそのまま読めばよいのです。
そして気づいたでしょう。
この表現形式では、elseブロックは不要なのです。
while True:と書いたことにより、while文を抜けるにはbreakを書くしかありません。
while文の条件が不成立になってループを抜けたこと」と「それ以外の要因でループを抜けたこと」を区別する必要自体が無くなるのです。

5. for-elseについての考察

5.1. for-elseが「本質的に」必要な場面

while-elseと同様に、for-elseについても考えてみましょう。
for-elseの一般形は次のように表現することができます。

for value in values:
    if break_condition:
        break_function()
        break
else:
    else_function()

5.2. try-except-elseにおけるelse

ここで一度、if文以外でelseが登場するという意味で仲間であるtry-except-elseについて考えてみましょう。
tryブロックの処理中にexceptブロックで指定された例外(例ではZeroDivisionError)が発生した場合はexceptブロックが実行され、例外が発生しなかった場合はelseブロックが実行されます。
ZeroDivisionError以外の例外が発生した場合はその時点で処理全体が終了します)

value: int = 1
try:
    divided = 1 / value
except ZeroDivisionError:
    print('except')
else:
    print('else')
実行結果
else

ではtry-except-elseelseにおいて、elseの意味「それ以外」の「それ」とは何を意味するのでしょうか?
もしelsetryと紐づいているなら、「tryが最後まで実行されること」、つまり「例外が発生しないこと」が「それ」と考えることができます。
しかしこの場合、「それ」でないとは「例外が発生すること」になってしまいます。
これはexceptの役割ですよね。
では、elseexceptと紐づいているなら?
exceptが実行されること」、つまり「例外が発生すること」が「それ」となり、「それ」でないとは「例外が発生しないこと」になります。
これはtry-except-elseの挙動と一致する解釈です。

ここから何が言えるのかというと、elseの意味「それ以外」の「それ」はいくつかの解釈をすることができるが、正解は文脈から判断する必要があるということです。

5.3. for-elseではなく、for-break-elseである

そもそもfor-elseという言い方をしていますが、これは適切なのでしょうか?

for value in values:
    if break_condition:
        break_function()
        break
else:
    else_function()

上のコードで、elseブロックが実行される条件は次のいずれかです。

  • valuesが空である
  • valuesが空でない、かつbreak_conditionが成立しないままループが最後まで実行された

ですが、よく考えてみてください。
上の2つの条件は「breakが実行されなかったとき」と一言で表現できます。
それなら、前節でtry-except-elseelsetryではなくexceptに紐づいていることを見たように、for-elseelseforではなくbreakに紐づいていると解釈した方がよいのではないでしょうか?
このように考えれば、「breakしたらelseに入らず、breakしなかったらelseに入る」とすっきり理解することができます。
つまり、for-elseではなくfor-break-elseとして捉えるべきなのです。

実際、Python公式ドキュメントにも次のように書かれています。

ループの else 句は、 if 文の else よりも try 文の else に似ています。
try(注:脱字) 文の else 句は例外が発生しなかった時に実行され、
ループの else 句は break されなかった場合に実行されます。
https://docs.python.org/ja/3.13/tutorial/controlflow.html#else-clauses-on-loops

さらっと書かれているので読み飛ばされがちですが、try-except-elsefor-break-elseの類似に関してはもっと強調すべきでしょう。
更に注目すべきなのは、try-elseexcept無し)という記述がSyntaxErrorになるという事実です。

value: int = 1
try:
    divided = 1 / value
else:
    print('else')
実行結果
SyntaxError: expected 'except' or 'finally' block

exceptが無ければ、elseの意味「それ以外」の「それ」が、「tryが例外無く実行されること」になり、elseブロックが実行される条件は「tryで例外が発生したとき」となってしまいます。
これでは、try-elseexcept無し)とtry-except-elseelseの意味が真逆になってしまいます。

……「真逆になる」?聞き覚えがありますね?
そう、for-elseでも似たようなことが起きているのです。
for-elsebreak無し)を見たとき、elseの意味「それ以外」の「それ」を「forループが最後まで実行されること」と捉え、elseブロックが実行される条件を「forループが最後まで実行されないこと」と考えてしまったのは、本質的に重要なbreakを見落としていることが原因なのです。

そして理想を言えば、breakの無いfor文でelseブロックを使ったとき、SyntaxErrorにしてほしかった。

6. まとめ

  • while-elseが本質的に必要なのは、breakを含む条件分岐があるときだけである。
  • しかし可読性の観点から、while True:形式で表現した方がよい。
  • for-elseが本質的に必要なのは、breakを含む条件分岐があるときだけである。
  • elseforではなくbreakに紐づいているため、for-break-elseと言うべきである。
  • try-except-elsefor-break-elseには強い類似性がある。

breakが無くたって、for-elseを書くことはできます。
しかし、それはelseの本質的な使い方であるとは言えません。
for-break-elseの形式に限定してelseを使用することが、「Protected属性名の先頭は_にする」のように一般的な了解を得られる日が来るのを望んでいます。

7. 参考

Discussion