第1部 Python基礎編
第4章 データ構造 — リスト・辞書・タプル・集合
大量のデータを整理して持つための「入れ物」たちを学びます。実務のスクリプトはこの4つの使い分けが主役です。
🎯 この章で学ぶこと
- リスト:順番のあるデータの列
- 辞書:キーと値のペア
- タプルと集合:変更できない列と、重複のない集まり
- 内包表記によるスマートなデータ加工
4.1 リスト(list)— 順番のあるデータの列
リストは複数の値を順番に並べて持つ入れ物です。角かっこ [ ] で作ります。
users = ["tanaka", "suzuki", "sato"]
print(users[0]) # tanaka(先頭は0番)
print(users[-1]) # sato(マイナスは後ろから)
print(len(users)) # 3(要素数)
users.append("yamada") # 末尾に追加
users.remove("suzuki") # 値を指定して削除
print(users) # ['tanaka', 'sato', 'yamada']
スライス — 範囲の切り出し
>>> nums = [10, 20, 30, 40, 50]
>>> nums[1:3] # 1番から3番の手前まで
[20, 30]
>>> nums[:2] # 先頭から2番の手前まで
[10, 20]
>>> nums[2:] # 2番から最後まで
[30, 40, 50]
並べ替えと集計
response_times = [120, 85, 340, 95, 2100] # ミリ秒
print(max(response_times)) # 2100 — 最も遅い応答
print(min(response_times)) # 85
print(sum(response_times)) # 2740
print(sorted(response_times)) # [85, 95, 120, 340, 2100]
4.2 辞書(dict)— キーと値のペア
辞書は「名前(キー)で値を引ける」入れ物です。波かっこ { } で作ります。実務データのほとんどは辞書の形をしています。
account = {
"username": "tanaka",
"department": "情報システム部",
"is_admin": False,
"last_login": "2026-07-07",
}
print(account["username"]) # キーで値を取り出す
account["is_admin"] = True # 値の変更
account["mfa_enabled"] = True # 新しいキーの追加
# キーが無いかもしれない時は get() が安全(無ければNoneや既定値)
print(account.get("phone", "未登録")) # 未登録
辞書のループでは items() でキーと値を同時に取り出せます。
for key, value in account.items():
print(f"{key}: {value}")
実践 — ユーザーごとの失敗回数を数える
「キーが存在するかどうか」で処理を分けるのは辞書の定番パターンです。
failed_users = ["admin", "root", "admin", "tanaka", "admin"]
counts = {}
for user in failed_users:
if user in counts:
counts[user] = counts[user] + 1
else:
counts[user] = 1
print(counts) # {'admin': 3, 'root': 1, 'tanaka': 1}
この「出現回数のカウント」は、ログ解析・不正検知の最重要パターンです。第13章で本格的に活用します。
4.3 タプル(tuple)— 変更できないリスト
タプルは丸かっこ ( ) で作る、後から変更できない並びです。
>>> server = ("192.168.1.10", 443)
>>> server[0]
'192.168.1.10'
>>> server[0] = "10.0.0.1"
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
変更されては困るデータ(設定値、座標、IPとポートの組など)をタプルにしておけば、うっかり書き換えるバグをPython自身が防いでくれます。「変更の必要がないものは変更できなくしておく」──これはセキュリティの最小権限の原則と同じ発想です。
4.4 集合(set)— 重複のない集まり
集合は重複を許さない入れ物で、「重複除去」と「比較(差分)」が得意です。
# アクセス元IPの重複を除いて種類数を数える
ips = ["10.0.0.5", "10.0.0.9", "10.0.0.5", "10.0.0.7", "10.0.0.5"]
unique_ips = set(ips)
print(unique_ips) # {'10.0.0.5', '10.0.0.9', '10.0.0.7'}
print(len(unique_ips)) # 3
# 集合同士の演算
allowed = {"tanaka", "suzuki", "sato"} # 許可リスト
actual = {"tanaka", "suzuki", "yamada"} # 実際にアクセスした人
print(actual - allowed) # {'yamada'} — 許可されていないのにアクセスした人!
print(allowed - actual) # {'sato'} — 許可されているが使っていない人
print(allowed & actual) # {'tanaka', 'suzuki'} — 両方に含まれる人
「人事台帳にいる社員の集合」と「システムにアカウントがある人の集合」の差分を取れば、退職済みなのに残っているアカウント(=攻撃者に悪用されやすい放置アカウント)が一瞬で見つかります。IT部門の定例業務である棚卸し作業は、まさに今日学んだ set の引き算です。Excelで目視確認していた作業が3行のPythonになります。
4.5 内包表記 — ループを1行で書く
「リストから条件に合うものを選んで新しいリストを作る」処理は頻出なので、Pythonには専用の短い書き方があります。
logs = [
"LOGIN OK user=tanaka",
"LOGIN FAILED user=admin",
"LOGIN FAILED user=root",
]
# 通常の書き方
failed = []
for line in logs:
if "FAILED" in line:
failed.append(line)
# 内包表記なら1行
failed = [line for line in logs if "FAILED" in line]
print(failed)
読み方は「logs の各 line のうち、FAILED を含むものを集めたリスト」。最初は通常のfor文で書き、慣れてきたら内包表記に書き換える、で構いません。
4.6 どの入れ物を使うべきか
| 入れ物 | 特徴 | 実務での典型例 |
|---|---|---|
list | 順序あり・変更可・重複可 | ログの行、処理対象ファイルの一覧 |
dict | キー→値で引ける | ユーザー情報、設定、集計結果 |
tuple | 順序あり・変更不可 | (IP, ポート) の組、固定の設定値 |
set | 重複なし・集合演算 | ユニークなIPの数、許可リストとの差分 |
まとめ
- リストは順序つきの列。インデックスは0始まり、スライスで切り出せる
- 辞書はキー→値。
get()を使うと存在しないキーでも安全 - タプルは不変。変更されたくないデータを守れる
- 集合は重複除去と差分・共通部分の計算が得意
- 内包表記で「絞り込んで集める」が1行で書ける
練習問題
リスト ports = [22, 80, 443, 3389, 8080] から、1024未満のポート(ウェルノウンポート)だけを内包表記で取り出してください。
解答を見る
ports = [22, 80, 443, 3389, 8080]
well_known = [p for p in ports if p < 1024]
print(well_known) # [22, 80, 443]
人事台帳 employees = {"tanaka", "suzuki", "sato"}、システムのアカウント accounts = {"tanaka", "suzuki", "sato", "yamada", "test_user"} があります。「台帳に存在しないアカウント」を集合演算で求めてください。
解答を見る
employees = {"tanaka", "suzuki", "sato"}
accounts = {"tanaka", "suzuki", "sato", "yamada", "test_user"}
orphans = accounts - employees
print(orphans) # {'yamada', 'test_user'}
test_user のような「検証用に作って消し忘れたアカウント」は実際のインシデントでも侵入口としてよく悪用されます。
次のIPアドレスのリストから、辞書を使って「IPごとの出現回数」を数え、最も多く出現したIPを表示してください。
ips = ["10.0.0.5", "10.0.0.9", "10.0.0.5", "10.0.0.7", "10.0.0.5", "10.0.0.9"]
解答を見る
ips = ["10.0.0.5", "10.0.0.9", "10.0.0.5", "10.0.0.7", "10.0.0.5", "10.0.0.9"]
counts = {}
for ip in ips:
counts[ip] = counts.get(ip, 0) + 1 # 無ければ0から数え始める
print(counts) # {'10.0.0.5': 3, '10.0.0.9': 2, '10.0.0.7': 1}
top_ip = max(counts, key=counts.get)
print(f"最多アクセス元: {top_ip} ({counts[top_ip]} 回)")
counts.get(ip, 0) + 1 は「無ければ0、あれば今の値に1を足す」という定番イディオムです。同一IPからの異常な回数のアクセスは攻撃の兆候かもしれません。