第2部 セキュリティ編
第15章 AIのコードを読み解く技術
第14章で「理解できないコードを本番に入れない」と学びました。ではどうすれば「完全に理解」できるのか。この章は、AIが書いたコードを読み解くための具体的な技術を訓練します。
🎯 この章で学ぶこと
- コードを読む5ステップの手順
- AIがよく使うPythonイディオムの読み下し方
- 実際のAI風コードを解剖する練習
- AIを「先生」として使う質問術
- 「完全に理解した」と言える基準
15.1 AI時代は「書く力」より「読む力」
AIの登場で、コードを「書く」コストは劇的に下がりました。その結果、エンジニアの価値の中心は読んで・検証して・責任を持つ力に移っています。そして朗報があります:コードを読む力は、正しい手順の反復で確実に身につく技術です。センスではありません。
脆弱性診断もコードレビューもインシデント調査も、出発点はすべて「コードを読めること」です。第10章のチェックリストも、読めなければ適用できません。読解力はセキュリティ技術の土台そのものです。この章はAIコードを題材にしますが、身につくのは他人のコード全般を読む力です。
15.2 コードを読む5ステップ
初学者がやりがちな失敗は「1行目から順に、全部を同じ深さで読もうとする」ことです。読み慣れた人は、次の順序で粗く→細かく読みます。
- 目的を1文で言う — 「このコードは、何を受け取って、何を出すものか?」をまず言葉にする。AIに書かせたなら、依頼文がそのまま答えのはず
- 構造を掴む(鳥の目) —
importの一覧、関数・クラスの名前だけを拾い読みする。「部品が何個あって、入口(if __name__ == "__main__":)はどこか」を把握する。この時点で中身は読まない - データの流れを追う(魚の目) — 入力データが、どの関数を通って、どう形を変えて出力に至るか。各変数に「今、何型の何が入っているか」を書き込みながら追う(第2章の型の意識がここで効く)
- 分からない行は「分解して実行」 — 読んで悩まない。REPL(第1章)に1行だけ貼って動かす、
print(type(x), x)を差し込む、内包表記をfor文に書き直してみる。実行结果は最強の説明書 - 疑って検証する — 「空のファイルなら?」「0件なら?」「不正な入力なら?」と壊れ方を予測し、実際に試す。第10章のチェックリストを当てる。ここまでやって初めて「読めた」と言える
読みながら、各関数の横に「入力→出力」を自分の言葉でメモしてください。# この関数: ログ1行 → Entryオブジェクト(形式外ならNone) のように。書けない関数=理解できていない関数、と一目で分かります。
15.3 AIがよく使うイディオム辞典
AIのコードが読めない原因の大半は、ロジックではなく「知らない構文・書き方(イディオム)」です。AIは学習データに多い「今どきの書き方」を好みます。頻出のものを読み下し方つきで整理します。分からない書き方に出会ったら、ここに戻ってきてください。
① 三項演算子(条件式)
state = "稼働中" if server.is_up else "停止中"
# 読み下し: 「is_upが真なら"稼働中"、そうでなければ"停止中"をstateに入れる」
# ifを先に読まず、「値A if 条件 else 値B」のセットで1つの値と捉える
② 内包表記(第4章の発展)
failed = [line for line in logs if "FAILED" in line]
# 読み下し: 「logsの各lineのうち、FAILEDを含むものを集めたリスト」
counts = {user: n for user, n in pairs if n >= 3} # 辞書版もある
# 迷ったら等価なfor文に書き直す:
counts = {}
for user, n in pairs:
if n >= 3:
counts[user] = n
③ ジェネレータ式と sum / any / all
ok_count = sum(e.result == "OK" for e in entries)
# 各要素についてTrue/False(=1/0)を作り、合計する → 「OKの個数」
has_admin = any(e.user == "admin" for e in entries) # 1つでもあればTrue
all_valid = all(len(p) >= 12 for p in passwords) # 全部満たせばTrue
④ アンパックと *args / **kwargs
date, time, rest = line.split(" ", 2) # 3つに分けてそれぞれ代入
first, *others = items # 先頭と「残り全部」に分ける
def log_event(*args, **kwargs):
# args: 位置引数がタプルで、kwargs: キーワード引数が辞書で入る
# 「引数を何個でも受け取れる関数」の書き方
...
⑤ enumerate / zip / sorted(key=...)
for i, line in enumerate(lines, start=1): # 行番号つきループ
print(i, line)
for name, ip in zip(names, ips): # 2つのリストを並走
print(name, ip)
top = sorted(counts.items(), key=lambda x: x[1], reverse=True)
# 読み下し: 「countsの(キー, 値)ペアを、x[1](=値)を基準に降順で並べる」
# lambdaは「その場限りの小さな関数」。lambda x: x[1] は「xを受け取りx[1]を返す」
⑥ 型ヒント
def parse_line(line: str) -> Entry | None:
...
# 「lineはstrを想定、戻り値はEntryまたはNone」という注釈(実行には影響しない)
# 読む側には最高のヒント。「| None」= 失敗時にNoneを返す設計だと分かる
⑦ セイウチ演算子 :=
entries = [e for line in lines if (e := parse_line(line)) is not None]
# 「parse_line(line)の結果をeに代入し、それがNoneでなければ採用」
# 代入と判定を同時に行う書き方。AIが好むが、読みにくければ普通に書き直してよい
⑧ デコレータ
@dataclass # 「下の定義を加工して機能を足す」目印(第8章で登場済み)
class Entry: ...
@functools.lru_cache # 「この関数に結果キャッシュ機能を付ける」
def slow_lookup(host): ...
# @xxx を見たら「この関数/クラスはxxxに包まれて機能が追加される」と読む
⑨ 辞書の get / setdefault / Counter
counts[ip] = counts.get(ip, 0) + 1 # 第4章のイディオム
from collections import Counter
counts = Counter(e.user for e in entries) # 出現回数の集計を1行で
counts.most_common(3) # 上位3件
⑩ pathlib の連鎖
from pathlib import Path
text = (Path("logs") / "access.log").read_text(encoding="utf-8")
# 「logsフォルダの中のaccess.logを、開いて読んで閉じるまで一気に」
# / はパスの連結。openとwithを1つにまとめた便利メソッド
イディオムは短く書けますが、短い=良いではありません。チームの誰も読めないセイウチ演算子より、3行のfor文のほうが安全です。AIに「初学者にも読める書き方に直して」と頼むのは、恥ずかしいことではなく保守性(=セキュリティ)への投資です。
15.4 実践 — AIが書いたコードを解剖する
「アクセスログを集計してJSONで出力して」とAIに頼んだら、こんなコードが返ってきたとします。5ステップで解剖しましょう。
# log_summary.py — AIが生成した想定のコード
import json
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Entry:
time: str
result: str
user: str
def parse_line(line: str) -> Entry | None:
parts = line.split()
if len(parts) < 5 or parts[2] != "LOGIN":
return None
return Entry(time=parts[1], result=parts[3],
user=parts[4].removeprefix("user="))
def summarize(path: Path) -> dict:
lines = path.read_text(encoding="utf-8").splitlines()
entries = [e for line in lines if (e := parse_line(line)) is not None]
failed = Counter(e.user for e in entries if e.result == "FAILED")
ok = sum(e.result == "OK" for e in entries)
return {
"total": len(entries),
"failed_by_user": dict(failed.most_common()),
"ok_ratio": round(ok / len(entries), 2) if entries else 0.0,
}
if __name__ == "__main__":
print(json.dumps(summarize(Path("access.log")),
ensure_ascii=False, indent=2))
ステップ1(目的):「ログファイルを受け取り、総件数・ユーザー別失敗回数・成功率をJSONで出す」。
ステップ2(構造):部品は3つ。データの入れ物 Entry(@dataclass=第8章)、1行を解釈する parse_line、全体を集計する summarize。入口は最後の __main__ ブロック(第7章)。
ステップ3(データの流れ): Path → 全文(str)→ 行のリスト(list[str])→ Entry のリスト → Counter と数値 → 辞書 → JSON文字列。各行の型を追えば、流れは一本道です。
ステップ4(分からない行を分解):たとえば内包表記+セイウチ(⑦)の行は、こう書き直せます。
entries = []
for line in lines:
e = parse_line(line)
if e is not None:
entries.append(e)
ステップ5(疑って検証):ここが本番です。
- ファイルが存在しなければ? →
read_textがFileNotFoundErrorで落ちる。例外処理がない(第6章)。運用ツールにするなら try/except を足すべき - ログが0行なら? →
if entries else 0.0でゼロ除算は回避済み。ここはAIがよく間違える点だが、このコードは対処できている(検証して初めて分かる) - 形式の違う行が混ざったら? →
len(parts) < 5とparts[2] != "LOGIN"で静かにスキップされる。「静かに」で良いのか?破棄した行数を数えて報告するほうが監査的には安全(第6章の「握りつぶし」の議論) removeprefixは Python 3.9 以降。社内の古い環境で動くか?(実行環境の確認)
このように、「読める」の先にある「突っ込める」が目標地点です。AIのコードは平均的にはよく書けていますが、あなたの環境・要件・運用への適合はAIには判断できません。それを判断するのがあなたの仕事です。
15.5 AIを「先生」にする質問術
分からないコードを前にしたとき、AI自身が最高の家庭教師になります。効果的な頼み方の定型文を持っておきましょう。
- 「この行を1行ずつ、初学者向けに説明して」 — まず全体解説をもらう
- 「この内包表記を、普通のfor文に書き換えて」 — イディオムの翻訳。等価なコードを見比べるのが最速の学習
- 「この変数の型と中身の例を、各行ごとに示して」 — ステップ3の補助
- 「このコードが失敗するケースを5つ挙げて」 — ステップ5の壁打ち相手にする
- 「この関数のテストコードを書いて」 — テストは「仕様の言語化」。読解の答え合わせになる
第14章の大原則を思い出してください。AIはコードの説明でも間違えます(それらしく、自信満々に)。説明を読んだら、必ずREPLで動かして裏を取る。「説明と実行結果が一致した」ときだけ、その理解を採用します。最終的な真実は常に実行結果です。
写経+改造 — 理解を定着させる練習法
読んで分かったつもりのコードは、3日で忘れます。定着させる最短の方法は:
- 写経する — コピペせず自分の手で打つ(第1章以来の原則)
- 予測してから改造する — 「しきい値を3から5に変えたら出力はどうなる?」を実行前に紙に書き、実行して答え合わせする
- 白紙から書き直す — 翌日、何も見ずに同じ機能を自分の書き方で再現する。下手でよい。書けなかった部分=理解の穴
15.6 「完全に理解した」と言える基準
「なんとなく分かった」と「完全に理解した」の間には段階があります。本番投入前のセルフチェックとして使ってください。
| レベル | できること | 確認方法 |
|---|---|---|
| 1. 説明できる | コードの目的と各部品の役割を自分の言葉で言える | コードを見ずに同僚に説明してみる |
| 2. 追跡できる | 任意の行で「変数に何型の何が入っているか」を言える | 数行選んで宣言し、printで答え合わせ |
| 3. 予測できる | 入力を変えたときの出力を、実行前に言い当てられる | 3パターン予測→実行→全問正解か |
| 4. 壊し方が分かる | どんな入力・状況でどう失敗するかを列挙できる | 第10章チェックリスト+エッジケースのテスト |
| 5. 再構築できる | 何も見ずに同じ機能を自力で書ける(書き方は違ってよい) | 白紙から書き直してみる |
実務の目安として、自分が責任を持つコードはレベル4までが最低線です(第14章:理解できないコードを本番に入れない)。学習中のコードはレベル5まで往復すると、力が飛躍的に伸びます。
まとめ
- 読む力は技術。5ステップ(目的→構造→データの流れ→分解実行→疑って検証)で粗く→細かく読む
- 読めない原因の大半はイディオム。等価なfor文への「翻訳」で必ず理解できる
- 読んで悩まず、REPLで動かす。実行結果が最強の説明書
- AIに説明・翻訳・テスト作成を頼んでよい。ただしAIの説明も実行して裏を取る
- 「完全に理解」= 説明・追跡・予測・壊し方・再構築の5レベル。本番コードはレベル4が最低線
練習問題
次のAIらしい1行を、①読み下し文にし、②等価なfor文に書き換えてください。
admins = {e.user for e in entries if e.result == "FAILED" and e.user.startswith("adm")}
解答を見る
① 「entriesの各要素eのうち、結果がFAILEDでユーザー名がadmで始まるものの、ユーザー名を集めた集合(重複なし)」。{ } で値だけを並べているので集合内包表記です(第4章)。
admins = set()
for e in entries:
if e.result == "FAILED" and e.user.startswith("adm"):
admins.add(e.user)
AIが生成した次の関数を、5ステップの「ステップ5(疑って検証)」の目で見て、問題点を3つ以上指摘してください。
import json
from pathlib import Path
def load_config(name):
path = Path("configs") / name
data = json.loads(path.read_text())
api_key = data.get("api_key", "sk-default-12345")
return data, api_key
解答を見る
- パストラバーサル(第10章)—
nameが外部入力なら../../etc/passwdのような値でconfigsの外に出られる。resolve()して基準ディレクトリ内か検証すべき。 - 秘密情報のハードコード(第10章)— 既定値
"sk-default-12345"がコードに直書きされている。しかも「キーが無い」という設定ミスが、既定キーで静かに動き続けてしまう。既定値を与えず、無ければ明示的にエラーにすべき。 - 例外処理がない(第6章)— ファイルが無ければ
FileNotFoundError、壊れたJSONならJSONDecodeErrorでそのまま落ちる。捕捉して意味のあるメッセージにするべき。 - 文字コード未指定 —
read_text()にencoding="utf-8"がなく、環境によって挙動が変わる。 - 型ヒント・docstringがない — 戻り値がタプルであることが読み手に伝わりにくい。
「動くコード」と「本番に入れてよいコード」の差が、そのままこのリストです。
次のコードの出力を実行せずに予測し、紙に書いてから実行して答え合わせしてください(レベル3の訓練です)。
counts = {"admin": 5, "root": 2, "tanaka": 8}
top = sorted(counts.items(), key=lambda x: x[1], reverse=True)
for i, (user, n) in enumerate(top[:2], start=1):
print(f"{i}位: {user} ({n}回)")
解答を見る
1位: tanaka (8回)
2位: admin (5回)
読み解き:counts.items() で (ユーザー, 回数) のペアにし、key=lambda x: x[1] で回数を基準に、reverse=True で降順に並べる(イディオム⑤)。top[:2] で上位2件だけをスライス(第4章)し、enumerate(..., start=1) で1から番号を振りながら、タプルを (user, n) にアンパック(イディオム④)しています。1行に4つのイディオムが同居する、いかにもAIらしいコードでした。全部読めたなら、この章は合格です。