finallyのreturnが、tryの失敗を握りつぶしていた話
finallyのreturnが、tryの失敗を握りつぶしていた話
0件のはずがない。
なのに、ずっと0件だった。
しろまえ殿は、外部サイトから写真データを取り込む処理の実行結果を見ていた。
毎回「取得成功」の顔をして、中身は空っぽで返ってくる。
これはおかしい、としろまえ殿は思った。
クルーに調査を任せた。
「仕様が変わった」というコメントを、まず疑った
対象のコードには、古いコメントが残っていた。
「ページ送りのclass名(HTML〈Webページの構造を書く言語〉の部品に付けるラベルの一種)が変わったから、ここは直した」
そう書いてある。一見、もっともらしい。
しろまえ殿は、このコメントをそのまま信じるなとクルーに伝えた。
書かれた時点の記録は、今も正しいとは限らない。
クルーは実際のサイトへ、最小限のアクセスで確認した。
結果は、コメントと逆だった。
古いclass名は、今もそのまま動いていた。直したはずの箇所は、直す必要が無かった。
たとえば、去年貼られた「この先通行止め」の看板を信じて遠回りしたら、実はとっくに撤去されていて、道は普通に通れた。そういう話だ。
コメントは、書かれた時点のことしか教えてくれない。
本当の原因は、たった1文字だった
コメントに引きずられず、コードを一から洗い直させた。
写真を取りに行くURL(サイト上の場所を示す文字列)のパスが、古いままだった。
サイト側の仕様変更で、そのパスはとっくに404(ページが存在しないという応答)を返すようになっていた。
パスを1箇所だけ直した。
取得件数は、0件から7件に変わった。
しろまえ殿は、直しただけで報告を受け取らなかった。
直す前と直した後、両方を実際に走らせて、0件→7件の変化を確認させた。
もう一つ、隠れていたバグ
原因は見つかった。だが、しろまえ殿はそこで終わらせなかった。
同じ処理のコードを、もう一度洗わせた。
すると、別のバグが出てきた。
try(まず試してみる処理)とexcept(失敗したときの処理)、そしてfinally(成功でも失敗でも必ず実行される後始末処理)。
この3つを組み合わせたコードで、exceptの中には「失敗として返す」処理が書いてあった。
ところが、finallyの中にも、無条件のreturn文(処理結果を呼び出し元へ返す命令)があった。
Pythonの仕様では、finallyのreturnが優先される。
exceptが「失敗です」と返したつもりでも、finallyの戻り値がそれを丸ごと上書きしてしまう。
つまり、コードの上では正しくエラー処理を書いているのに、呼び出し元には常に「成功」しか伝わらない状態になっていた。
これには、正直ぞっとした。
エラーを検知する仕組み自体が、静かに機能していなかったということだ。
finally側のreturn文を削除させ、正常系のreturnをtry/exceptの後ろへ移させた。
わざとエラーを起こしてみて、今度こそ失敗が失敗として伝わることも確認させた。
気をつけたいこと
- レガシーコードのコメントは、書かれた時点の話でしかない。今のサイト・今の環境で本当にそうかは、別に確認する。
- 0件という結果は、それ自体を疑う。「正常に0件」なのか「実は取得に失敗して0件」なのか、混同しない。
- finally節に無条件のreturnを置くと、try/exceptのreturnが黙って上書きされる。Pythonの言語仕様なので、書き方の癖として覚えておく価値がある。
- 直した後は、直る前の状態と比べて数字の変化を確認する。「直したつもり」で終わらせない。
小さな1行の修正の裏に、もう一段深いバグが隠れていることがある。
コメントを疑い、結果を疑い、直した後も疑う。
しろまえ殿がそこまで確認を求めたからこそ、静かに壊れていた処理を見つけ出せた。