Discordコミュニティで回答と参考記事を頂いたchunさん、cocoatomoさんのおかげで解決と納得へ大きく前進できました。
本当にありがとうございます。この場を借りてお礼をさせていただきます。
事の発端
こちらの記事を拝読していた際、記事のコメントに気になるコードを見つけたことがきっかけです。
x = f"{quit()}"
while True:
print("hello")
記事コメント投稿者さんが仰った通り、このコードを実行しますと、hello
は出力されず終了します。
私はここで少し違和感を持ち、調べてみました。
f-stringについて、公式ドキュメントによりますと、
フォーマット済み文字列リテラル( formatted string literal )または f-string は、接頭辞 'f' または 'F' の付いた文字列リテラルです。これらの文字列には、波括弧 {} で区切られた式である置換フィールドを含めることができます。他の文字列リテラルの場合は内容が常に一定で変わることが無いのに対して、フォーマット済み文字列リテラルは実行時に式として評価されます。
とのこと。つまり、{}内に関数を入れると、その戻り値をフィールドに置換するということです。
関数の戻り値がintであろうがlistであろうがdictであろうが、文字列にフォーマットします。
果てはNoneTypeさえも文字列"None"
にしてしまう、f-stringとは相当に強力なシロモノです。
この強力なf-stringをも破ったquit()関数、一体何を返したのか
これが私の最初の疑問でした。
アプローチその1 type()を見る
まず、quit()がどんな「型」を返したのかがわかれば糸口が見えるのではと思い、以下のコードを実行してみました。
>>> print(type(quit()))
C:\work>
まさかのpythonインタプリタ自体が終了しました
前述の通り、戻り値がNoneTypeであればf-stringに入れたとき文字列"None"
と出力されるので、quit()の戻り値はNoneTypeでは無いだろうなと予測していましたが、まさか結果が出ないどころかインタプリタごと落としてくるとは...
アプローチその2 関数定義部分を見る
typeが取得できなくても、実際のquit()関数のソースを見れば、どんな処理をしているのか判るのでは?と考え、builtin内のquitを覗いてみました。
def quit(code: object = ...) -> NoReturn: ...
returnがそもそもない上に謎のNoReturnなるものがそれが省略や間違いでないことを主張している
関数にはreturnがあるのが当然と思ってた私には早くも万策尽き、Discordコミュニティで質問することにしました。
アプローチ3 教えていただいた
コミュニティの方に返信をいただくことができました。
引用させていただきます。
sys.exit(), exit(), quit()はSystemExit例外を吐いたような気がします
参考: https://docs.python.org/3/library/constants.html
- {}内を評価
- 例外を吐く
結論: 2行目以降は実行されない
って感じだと思います
NoReturnはtyping.NoReturnだと思います
参考: https://docs.python.org/3/library/typing.html
関数の返り値を明示しないと(returnを使用しないと)自動でNoneが返り値になるのでNoReturnでは無いです。
def 関数名() -> None:
pass
つまり、quit()や、sys.exit()、exit()などは、必ず例外を出す関数だそうです。
例外の正体とNoReturn
教えて頂いたSystemExit例外ですが、普通にquit()やsys.exit()を実行しても例外をキャッチできずトレースバックが出ません。
これは、SystemExit例外が、Exceptionではなく、それらが継承している大本のクラス、BaseExceptionを直接継承しているためです。
通常キャッチできる例外、KeyErrorだのTypeErrorだのというのはExceptionクラスを継承しているものだそうで、SystemExitはその中にないのだそうです。
BaseExceptionについて
BaseExceptionはpythonのすべての例外の大本であり、これを直接継承している例外クラスはSystemExit、GeneratorExit、KeyboardInterrupt、Exceptionの4つだけです。
KeyboardInterruptはCtrl+C
のアレですね。
Exceptionは他のすべての例外の基底クラスになっていて、ユーザーが定義する例外もこれを基底にすべきとされています。ミスのあるコードを書いたときキャッチされてTracebackで出力されるのはこのExceptionクラスを継承しているもので、BaseExceptionを直接継承している例外はキャッチされません。
BaseExceptionを補足するにはtracebackモジュールを使います。
冒頭コードの1行目で発生すると予想されるSystemExit例外を捕まえてみましょう。
import traceback
try:
x = f"{quit()}"
print(x)
except BaseException: #念の為BaseExceptionを指定していますが、ただのexcept: でも拾ってくれます
print(traceback.format_exc())
結果は以下の通り
Traceback (most recent call last):
File "c:/work/catchBaseException.py", line 3, in <module>
x = f"{quit()}"
File "C:\Python\Python37\lib\_sitebuiltins.py", line 26, in __call__
raise SystemExit(code)
SystemExit: None
無事BaseExceptionのSystemExitをキャッチできました
NoReturnについて
これはそんなに難しいものではなく、型ヒントつまり期待される結果を表したものだそうです
本来、関数はreturnが明示されていない場合、暗黙的にreturn None
しているそうです。
例えば、以下のコード
def no_return():
pass
x = no_return()
print(type(x))
<class 'NoneType'>
関数no_return()にはreturn None
どころかreturnそのものが設定されていませんが、戻り値のtypeはNoneType。
つまりno_return関数が暗黙的にreturn None
したのです。
しかし、これは関数が最後まで正常に実行され、なおかつreturnが無い場合です。
例外を吐き出して終わるquit()やsys.exit()は、暗黙return Noneさえもしない
関数の型ヒントには、その関数がどんな型の値を返すかという期待値を記述するわけですが、Noneさえも帰ってこないこれらの関数の期待される結果、型ヒントの内容がNoReturn
ということだそうです。
まとめ
- quit()、sys.exit()は、例外SystemExitを発生させる
- SystemExitは大本の例外クラスBaseExceptionを直接継承する特殊なもの
- returnが明示されていない関数は暗黙的に
return None
するが、例外SystemExitが発生することでそれすら行われず、実行結果はNoneすら戻らない - Noneも戻らない関数には、型ヒントにNoReturnを記述する
#参考リンク
公式ドキュメント
https://docs.python.org/ja/3/library/constants.html
https://docs.python.org/ja/3/library/exceptions.html#SystemExit
https://docs.python.org/ja/3/library/typing.html#typing.NoReturn
Qiita
https://qiita.com/ksato9700/items/44caf7bf0329fb987499
https://qiita.com/hayata-yamamoto/items/259238dad038b5bee51c
Pythonjp公式Discord
https://www.python.jp/pages/pythonjp_discord.html