到達点は、問題のある場所を測り、入力の境界で不正な値を止めることです。前提は第11〜15回です。
最適化の前に処理量を確認する
遅いからといって、すぐに並列化する必要はありません。全件比較を辞書検索へ変えるだけで、処理量が大幅に減る場合があります。cProfileで関数ごとの時間を調べ、timeitで小さな処理を繰り返し比較します。計測前後で入力と実行環境をそろえます。
import logging
from time import perf_counter
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
def parse_limit(text: str) -> int:
if len(text) > 4 or not text.isascii() or not text.isdecimal():
raise ValueError("件数は半角数字です")
value = int(text)
if not 1 <= value <= 1000:
raise ValueError("件数は1〜1000です")
return value
started = perf_counter()
limit = parse_limit("1000")
result = sum(range(limit))
logging.info("operation=sum status=ok elapsed=%.6f", perf_counter() - started)
print(result)
main.pyでは499500と所要時間のログが出ます。所要時間は実行ごとに変わります。一回の小さな数値を言語の性能の結論にせず、十分な回数と現実の入力で比較します。
境界を守る具体策
入力文字列をevalやexecへ渡して処理しません。JSONならjson、数値ならintのように目的に合うパーサーを使います。外部コマンドを呼ぶならsubprocess.runへ引数リストを渡し、不要なshell=Trueを使いません。SQLはプレースホルダを使います。
ログは調査に必要な操作名、結果、所要時間、追跡用IDへ絞ります。トークンやパスワード、受信本文全体をそのまま記録しないようにします。環境変数に置いた秘密でも、環境全体をログへ出せば漏れます。
ファイルパスを利用者から受け取る機能では、許可した領域を越えられるかを考えます。単純な文字列の前方一致だけでは、似た名前の別ディレクトリやシンボリックリンクを正しく扱えない場合があります。公開サービスでは、そもそも任意パスを受け取らない設計も有効です。
練習と解答
練習:件数を「01」と全角で指定した場合に、なぜ今回の関数は拒否するのでしょうか。
解答:半角数字だけを認める契約としてisasciiも調べているためです。全角を受け入れたいなら、明示的な正規化を先に行う仕様へ変えます。利用者の期待と、パーサーがたまたま認める範囲を混同しないことが重要です。
公式資料
profile、logging、subprocessを参照できます。
Python全20回の目次 | 前の回 | 次の回