🎯 この章で学ぶこと

  • コードを読む5ステップの手順
  • AIがよく使うPythonイディオムの読み下し方
  • 実際のAI風コードを解剖する練習
  • AIを「先生」として使う質問術
  • 「完全に理解した」と言える基準

15.1 AI時代は「書く力」より「読む力」

AIの登場で、コードを「書く」コストは劇的に下がりました。その結果、エンジニアの価値の中心は読んで・検証して・責任を持つ力に移っています。そして朗報があります:コードを読む力は、正しい手順の反復で確実に身につく技術です。センスではありません。

🛡️ セキュリティの視点 — 読めないコードは監査できないコード

脆弱性診断もコードレビューもインシデント調査も、出発点はすべて「コードを読めること」です。第10章のチェックリストも、読めなければ適用できません。読解力はセキュリティ技術の土台そのものです。この章はAIコードを題材にしますが、身につくのは他人のコード全般を読む力です。

15.2 コードを読む5ステップ

初学者がやりがちな失敗は「1行目から順に、全部を同じ深さで読もうとする」ことです。読み慣れた人は、次の順序で粗く→細かく読みます。

  1. 目的を1文で言う — 「このコードは、何を受け取って、何を出すものか?」をまず言葉にする。AIに書かせたなら、依頼文がそのまま答えのはず
  2. 構造を掴む(鳥の目)import の一覧、関数・クラスの名前だけを拾い読みする。「部品が何個あって、入口(if __name__ == "__main__":)はどこか」を把握する。この時点で中身は読まない
  3. データの流れを追う(魚の目) — 入力データが、どの関数を通って、どう形を変えて出力に至るか。各変数に「今、何型の何が入っているか」を書き込みながら追う(第2章の型の意識がここで効く)
  4. 分からない行は「分解して実行」 — 読んで悩まない。REPL(第1章)に1行だけ貼って動かす、print(type(x), x) を差し込む、内包表記をfor文に書き直してみる。実行结果は最強の説明書
  5. 疑って検証する — 「空のファイルなら?」「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(疑って検証):ここが本番です。

このように、「読める」の先にある「突っ込める」が目標地点です。AIのコードは平均的にはよく書けていますが、あなたの環境・要件・運用への適合はAIには判断できません。それを判断するのがあなたの仕事です。

15.5 AIを「先生」にする質問術

分からないコードを前にしたとき、AI自身が最高の家庭教師になります。効果的な頼み方の定型文を持っておきましょう。

⚠️ AIの「解説」も検証する

第14章の大原則を思い出してください。AIはコードの説明でも間違えます(それらしく、自信満々に)。説明を読んだら、必ずREPLで動かして裏を取る。「説明と実行結果が一致した」ときだけ、その理解を採用します。最終的な真実は常に実行結果です。

写経+改造 — 理解を定着させる練習法

読んで分かったつもりのコードは、3日で忘れます。定着させる最短の方法は:

  1. 写経する — コピペせず自分の手で打つ(第1章以来の原則)
  2. 予測してから改造する — 「しきい値を3から5に変えたら出力はどうなる?」を実行前に紙に書き、実行して答え合わせする
  3. 白紙から書き直す — 翌日、何も見ずに同じ機能を自分の書き方で再現する。下手でよい。書けなかった部分=理解の穴

15.6 「完全に理解した」と言える基準

「なんとなく分かった」と「完全に理解した」の間には段階があります。本番投入前のセルフチェックとして使ってください。

レベルできること確認方法
1. 説明できるコードの目的と各部品の役割を自分の言葉で言えるコードを見ずに同僚に説明してみる
2. 追跡できる任意の行で「変数に何型の何が入っているか」を言える数行選んで宣言し、printで答え合わせ
3. 予測できる入力を変えたときの出力を、実行前に言い当てられる3パターン予測→実行→全問正解か
4. 壊し方が分かるどんな入力・状況でどう失敗するかを列挙できる第10章チェックリスト+エッジケースのテスト
5. 再構築できる何も見ずに同じ機能を自力で書ける(書き方は違ってよい)白紙から書き直してみる

実務の目安として、自分が責任を持つコードはレベル4までが最低線です(第14章:理解できないコードを本番に入れない)。学習中のコードはレベル5まで往復すると、力が飛躍的に伸びます。

まとめ

練習問題

問題 15-1

次の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)
問題 15-2

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
解答を見る
  1. パストラバーサル(第10章)— name が外部入力なら ../../etc/passwd のような値で configs の外に出られる。resolve() して基準ディレクトリ内か検証すべき。
  2. 秘密情報のハードコード(第10章)— 既定値 "sk-default-12345" がコードに直書きされている。しかも「キーが無い」という設定ミスが、既定キーで静かに動き続けてしまう。既定値を与えず、無ければ明示的にエラーにすべき。
  3. 例外処理がない(第6章)— ファイルが無ければ FileNotFoundError、壊れたJSONなら JSONDecodeError でそのまま落ちる。捕捉して意味のあるメッセージにするべき。
  4. 文字コード未指定read_text()encoding="utf-8" がなく、環境によって挙動が変わる。
  5. 型ヒント・docstringがない — 戻り値がタプルであることが読み手に伝わりにくい。

「動くコード」と「本番に入れてよいコード」の差が、そのままこのリストです。

問題 15-3

次のコードの出力を実行せずに予測し、紙に書いてから実行して答え合わせしてください(レベル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らしいコードでした。全部読めたなら、この章は合格です。