【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-else
やfor-else
の処理が丸ごとスキップされる」と考えてしまい、直感的でないと感じてしまうようです。
3. なぜelseという単語が採用されたか
禁止論を深掘りする前に、else
が採用された経緯を考察します。
まず大きい理由として考えられるのは、「キーワードを増やしたくなかった」というものでしょう。
if
やfor
, 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
この中で、if
やfor
, 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
は「一時停止」であって抜けてはいないので除外します)
このうちreturn
とraise
に関しては関数自体を終了させるため、while
文を抜けた後の処理は実行されません。
よって、ここではbreak
だけを考えればよいです。
そして、break
はwhile
文「直下」に配置するとwhile
文の存在意義を破壊します。
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つが必要です。
-
condition
の理解 -
break_condition
の理解 -
break_function
の理解 -
else_function
の理解 - elseブロック実行条件の理解
- ループを抜ける条件の理解
else
ブロック実行条件は、not condition
となることです。
コードにはcondition
しか書いてないので、自分の頭の中で反転させる必要があります。
更に、break
があるということはwhile
文を抜けた後の処理があるということですから、ループを抜ける条件も理解する必要があります。
その条件は、not condition or break_condition
です。
頭の中でcondition
を反転させたうえ、break_condition
とor
を取らなければなりません。
この辺で私の頭はパンクします。
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つが必要です。
-
break_condition_1
の理解 -
break_function_1
の理解 -
break_condition_2
の理解 -
break_function_2
の理解 - ループを抜ける条件の理解
ループを抜ける条件は、break_condition_1 or break_condition_2
です。
while-else
のときのように、頭の中で反転させる必要がありません。
break_condition_1
もbreak_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-else
のelse
において、else
の意味「それ以外」の「それ」とは何を意味するのでしょうか?
もしelse
がtry
と紐づいているなら、「try
が最後まで実行されること」、つまり「例外が発生しないこと」が「それ」と考えることができます。
しかしこの場合、「それ」でないとは「例外が発生すること」になってしまいます。
これはexcept
の役割ですよね。
では、else
がexcept
と紐づいているなら?
「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-else
のelse
がtry
ではなくexcept
に紐づいていることを見たように、for-else
のelse
はfor
ではなく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-else
とfor-break-else
の類似に関してはもっと強調すべきでしょう。
更に注目すべきなのは、try-else
(except
無し)という記述がSyntaxError
になるという事実です。
value: int = 1
try:
divided = 1 / value
else:
print('else')
SyntaxError: expected 'except' or 'finally' block
except
が無ければ、else
の意味「それ以外」の「それ」が、「try
が例外無く実行されること」になり、else
ブロックが実行される条件は「try
で例外が発生したとき」となってしまいます。
これでは、try-else
(except
無し)とtry-except-else
でelse
の意味が真逆になってしまいます。
……「真逆になる」?聞き覚えがありますね?
そう、for-else
でも似たようなことが起きているのです。
for-else
(break
無し)を見たとき、else
の意味「それ以外」の「それ」を「for
ループが最後まで実行されること」と捉え、else
ブロックが実行される条件を「for
ループが最後まで実行されないこと」と考えてしまったのは、本質的に重要なbreak
を見落としていることが原因なのです。
そして理想を言えば、break
の無いfor
文でelse
ブロックを使ったとき、SyntaxError
にしてほしかった。
6. まとめ
-
while-else
が本質的に必要なのは、break
を含む条件分岐があるときだけである。 - しかし可読性の観点から、
while True:
形式で表現した方がよい。 -
for-else
が本質的に必要なのは、break
を含む条件分岐があるときだけである。 -
else
はfor
ではなくbreak
に紐づいているため、for-break-else
と言うべきである。 -
try-except-else
とfor-break-else
には強い類似性がある。
break
が無くたって、for-else
を書くことはできます。
しかし、それはelse
の本質的な使い方であるとは言えません。
for-break-else
の形式に限定してelse
を使用することが、「Protected属性名の先頭は_
にする」のように一般的な了解を得られる日が来るのを望んでいます。
7. 参考
- Effective Python 第2版
- Why does python use 'else' after for and while loops? - StackOverflow
Discussion