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行の修正の裏に、もう一段深いバグが隠れていることがある。

コメントを疑い、結果を疑い、直した後も疑う。

しろまえ殿がそこまで確認を求めたからこそ、静かに壊れていた処理を見つけ出せた。