🐍 Hands-on Crash Course

2時間でPythonをざっくり掴む

他言語の経験がある方向けのクラッシュコース。文法の説明より「他言語との差分」「手を動かすコード」に集中します。各セクションをREPLで実際に動かしながら読み進めてください。

各セクションは コア(必須・約2時間)と ◆ EXTRA(深掘り・EXTRA含むと半日)に分かれています。急ぎの場合はコア部分だけで OK です。

⏱ 約2時間(EXTRA含むと半日)
🐍 Python 3.10+
💻 11セクション
🏋️ 演習付き
01Python の世界へ
02型とリテラル
03コレクション
04制御フロー
05関数
06クラスとOOP
07モジュール
08例外処理
09ファイルI/O とマルチスレッド
10仮想環境・pip
11Pythonicな書き方
01

Python の世界へ

⏱ 10分

まずは Python という言語の「性格」を掴みましょう。import this を実行すると現れる Zen of Python が、Python の哲学を一言で表しています。

$ python3 → import thispython
>>> import this
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Readability counts.
There should be one-- and preferably only one --obvious way to do it.

Python の3つの特徴(他言語経験者向け)

🔄
VS 静的型付け言語(Java, Go, C++)

Python は動的型付け。変数に型宣言は不要で実行時に型が決まります。Python 3.5+ では型ヒントdef f(x: int) -> str)を使えますが強制はされません。

🔄
VS C/Java(ブロック構文)

{} の代わりにインデントがブロックを定義します。スペース4つが慣習(PEP 8)。タブとスペースの混在はエラーになります。

REPL の使い方

terminalbash
# ── バージョン確認 ────────────────────────
$ python3 --version
Python 3.12.0

# ── REPL 起動 ─────────────────────────────
# [実務] 新しいライブラリの動作確認、ワンライナーの検証、データの簡易変換
$ python3
>>> 1 + 1
2
>>> print("Hello, Python!")
Hello, Python!
>>> exit()             # または Ctrl+D

# ── スクリプト実行 ────────────────────────
# [実務] バッチ処理、データ変換、CLIツールの実行
$ python3 hello.py
02

型とリテラル

⏱ 20分

Python の基本的な型と、f-string(文字列フォーマットの決定版)を押さえます。

基本型

types.pypython
# ── int ───────────────────────────────────
# 精度無制限の整数。Go/Java の int64 と違いオーバーフローしない
x   = 42
big = 10 ** 100       # 問題なく動く(暗号計算・大きな ID にも使える)
sep = 1_000_000       # アンダースコアは読みやすさのため(値は変わらない)
# [実務] 金融計算、暗号処理、ページネーションのオフセット計算

# ── float ─────────────────────────────────
pi  = 3.14159
sci = 1.5e-3          # 0.0015(指数表記は科学・機械学習系でよく使う)

# ── str ───────────────────────────────────
# イミュータブル(変更不可)。変更したように見えても実は新オブジェクト
s = "Hello"
multi = """
複数行文字列
こう書ける
"""
# → SQL クエリ・HTML テンプレ・メールテンプレの埋め込みに便利
# [実務] ユーザー入力、APIレスポンス、ファイルパスなどほぼ全てに登場

# ── bool ──────────────────────────────────
# True/False は大文字始まり(true/false は NameError)
print(isinstance(True, int))  # True(bool は int のサブクラス)
# → True==1, False==0 なので sum([True, False, True]) == 2

# ── None ──────────────────────────────────
# null / nil / undefined 相当。比較は == より is を使う慣習
print(None is None)   # True
# [実務] DB の NULL、「まだ取得していない」「見つからなかった」を表す

# ── 型変換 ────────────────────────────────
int("42")      # 42    → CSV・フォーム入力の文字列を数値に変換
float("3.14")  # 3.14
bool(0)        # False(空文字""、0、空リスト[]・{}・()もすべて False)
# → if user_list: ... で「空かどうか」を確認できる(len()不要)

f-string — モダンな文字列フォーマット

fstring.pypython
# ── 基本埋め込み ──────────────────────────
name  = "Alice"
score = 98.5

print(f"Hello, {name}!")           # Hello, Alice!
# [実務] ログメッセージ、エラー文、APIレスポンスの生成など至る所で使う

# ── 書式指定(: の後に記法)──────────────
print(f"{score * 1.1:.1f} 点")     # 108.4 点(.1f → 小数点1桁)
# [実務] 金額・スコア・パーセント表示
print(f"{1234567:,}")              # 1,234,567(桁区切り)
print(f"{0.123:.2%}")             # 12.30%(割合表示)
print(f"{'left':<10}|{'right':>10}")
#  →  left      |     right(テーブル整形・CLIのレポート出力)
print(f"{42:08b}")                 # 00101010(2進数・ビットフラグの確認)

# ── デバッグ用 = 記法(Python 3.8+)──────
x = 42
print(f"{x = }")                   # x = 42(変数名と値を同時に出力)
# [実務] print デバッグ時に変数名が自動でつく
# → logging.debug(f"{user_id = }, {response_time = :.3f}s") のように使う
⚠️
整数除算に注意

10 / 33.333...(float)。整数除算は 10 // 33。C/Java の / が整数除算になる点と逆なので注意。

operators.pypython
# ── 除算の落とし穴 ────────────────────────
10 / 3    # 3.3333...   常に float(C/Java の / とは逆!)
10 // 3   # 3           切り捨て除算(整数が欲しいときはこちら)
10 % 3    # 1           剰余(偶数判定: x%2==0、時刻の折り返しなど頻出)
2 ** 10   # 1024        べき乗(^ はビット XOR なので注意)

# ── 負の数の切り捨ては -∞ 方向 ────────────
-7 // 2   # -4(-3.5 → -∞ 方向。C/Java の -7/2=-3 とは違う)
# [実務] ページネーション: total_pages = (count + per_page - 1) // per_page
🏋️ チャレンジ初級

型チェッカーを書く

変数 value を受け取り、型名と値を "int: 42" の形式で返す関数 describe(value) を書いてください。

ヒント: type(value).__name__ で型名の文字列が取れます。

解答python
def describe(value):
    return f"{type(value).__name__}: {value!r}"

print(describe(42))       # int: 42
print(describe("hello"))  # str: 'hello'
print(describe([1,2,3]))  # list: [1, 2, 3]
03

コレクション

⏱ 40分

list・dict・tuple・set の4つが基本。特にリスト内包表記は Python の真骨頂です。

4種類の使い分け早見表

collection_types.pypython
# ── list — 順番あり、重複OK ───────────────
[3, 3, 8]        # → [3, 3, 8] そのまま保持
# [実務] ユーザー一覧、ログ行、処理キュー

# ── tuple — 順番あり、重複OK、変更不可 ────
(3, 3, 8)        # → (3, 3, 8) list と同じ特性だが中身を変えられない
# [実務] 座標・RGB など「意味のある組み合わせ」、dict のキーにも使える

# ── set — 順番なし、重複なし ──────────────
{3, 3, 8}        # → {3, 8} 重複が消える
# [実務] 権限タグの管理、重複除去、集合演算(積・和・差)

# ── dict — キーと値のペア、キーは重複不可 ──
{"a": 1, "a": 2} # → {"a": 2} 同じキーは後で上書きされる
# [実務] APIレスポンス、設定値、DBレコード

# ── どれを使うか迷ったら ──────────────────
# 「順番が大事?」   → list か tuple
# 「変更しない?」   → tuple(辞書のキーにもなれる)
# 「重複を消したい?」 → set
# 「名前でアクセス?」 → dict

list — ミュータブルな配列

list.pypython
# ── 基本操作 ──────────────────────────────
nums = [1, 2, 3, 4, 5]

nums.append(6)        # 末尾追加(O(1))→ キューへの追加、ログの蓄積
nums.insert(0, 0)     # 位置指定挿入(O(n))→ 先頭挿入が頻繁なら deque を使う
nums.pop()            # 末尾削除(O(1))→ 6 を返す
# [実務] append/pop の組み合わせでスタック(後入れ先出し)として使える

# ── スライス(Python の強み)──────────────
# [start:stop:step]  stop は含まない
nums[1:3]             # [2, 3](部分リストの取り出し)
nums[::-1]            # 逆順コピー → パリンドローム判定: s == s[::-1]
nums[::2]             # 1つ飛ばし → 偶数インデックスの要素だけ取得
# [実務] バッチ処理: data[i*size:(i+1)*size] でページ分割

# ── 検索・ソート ──────────────────────────
3 in nums             # True(O(n) 線形探索、頻繁なら set/dict を使う)
nums.sort()           # インプレースソート(元のリストを変更)
sorted(nums, reverse=True)  # 新しいリストを返す(元は変更しない)
# [実務] sorted は関数型パイプラインに組み込みやすく、元データを変えたくない場合に使う

リスト内包表記

comprehension.pypython
# ── 基本構文 ──────────────────────────────
# [式 for 変数 in イテラブル if 条件]
#  ↑何を   ↑何から取り出す  ↑絞り込み条件(省略可)

# 0〜9の二乗リスト(if条件なしの基本形)
squares = [x**2 for x in range(10)]
# → [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 0〜19の偶数だけ(if条件あり)
evens = [x for x in range(20) if x % 2 == 0]
# → [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# ── ネスト(2重ループ)────────────────────
# 外側のforが先、内側のforが後、という順で読む
matrix = [[i*j for j in range(1,4)] for i in range(1,4)]
# → [[1,2,3], [2,4,6], [3,6,9]]
# i=1のとき → [1*1, 1*2, 1*3] → [1, 2, 3]
# i=2のとき → [2*1, 2*2, 2*3] → [2, 4, 6]
# i=3のとき → [3*1, 3*2, 3*3] → [3, 6, 9]

# ── flatten(2重リストを1重に)────────────
nested = [[1,2],[3,4],[5,6]]
flat = [x for sub in nested for x in sub]
# → [1, 2, 3, 4, 5, 6]
# 「subをnestedから取り出して、xをsubから取り出す」
# forが2つ並ぶときは左から右の順で読むと分かりやすい

# ── dict内包表記 ──────────────────────────
# ① まずforループで書く
urls = ["https://example.com", "https://python.org"]
result = {}
for url in urls:
    result[url] = len(url)   # キー=URL、値=文字数 を1件ずつ格納

# ② 内包表記に変換する(意味は全く同じ)
result = {url: len(url) for url in urls}

# 変換ルールはシンプル:
#
#   forループ              内包表記
#   ──────────────────     ──────────────────────────
#   result = {}
#   for 変数 in イテラブル:  →  result = {キー: 値 for 変数 in イテラブル}
#       result[キー] = 値
#
# 読み方のコツ:左から右に読む
result = {url: len(url) for url in urls}
# 「url をキー、len(url) を値として、urls の各 url に対して(dictを作る)」

# [実務] APIレスポンスのリストを id: object の dict に変換するのが典型パターン
users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
id_map = {u["id"]: u for u in users}
# → {1: {"id": 1, "name": "Alice"}, 2: {"id": 2, "name": "Bob"}}
# id_map[1] で O(1) アクセス可能に(リストの線形探索より高速)

# ── set内包表記 ───────────────────────────
# {式 for 変数 in イテラブル}(コロンなし=set)
unique_lens = {len(w) for w in ["cat","dog","elephant"]}
# → {3, 8}
# "cat"と"dog"は両方len=3なので重複が消える、それがset

dict・tuple・set

dict_tuple_set.pypython
# ── dict ─────────────────────────────────
# [実務] APIレスポンス、設定値、DBレコードの表現に常用
person = {"name": "Alice", "age": 30}
person.get("email", "N/A")        # KeyError を出さずデフォルト値を返す
# → アクセス前に key の存在確認が不要(if "key" in d: が不要になる)
person.update({"age": 31})        # 複数キーをまとめて更新
for k, v in person.items(): ...   # key と value を同時に取得(詳細は次のブロック)
merged = {"a": 1} | {"b": 2}      # dict マージ(Python 3.9+)
# → 右辺が優先。デフォルト設定に上書きを重ねる config 合成パターンに便利

# [実務] 環境ごとに設定を上書きする config 合成パターン
base_config = {"debug": False, "port": 8080, "db": "postgresql://localhost/prod"}
dev_config  = {"debug": True,               "db": "sqlite:///dev"}

config = {**base_config, **dev_config}
# → {"debug": True, "port": 8080, "db": "sqlite:///dev"}
#      ↑devで上書き   ↑baseのまま  ↑devで上書き
#
# **dict はキーと値をその場に展開する演算子(スプレッド構文)
# 同じキーが重複した場合は後ろに書いた dict が勝つ
# → base を先、環境固有設定を後に書くのが慣用パターン
# | 演算子(Python 3.9+)でも同じことができる: base_config | dev_config

# ── tuple — イミュータブル ────────────────
# [実務] 座標・RGB・DB の1レコードなど「変わらない組み合わせ」に
# dict のキーには list は使えないが tuple は使える(ハッシュ可能)
point = (3, 4)
x, y  = point                     # アンパック → x=3, y=4
a, *rest = (1, 2, 3, 4)           # a=1, rest=[2,3,4](* で残りをまとめる)
# [実務] header, *rows = csv_lines でヘッダと本文を分離

# ── set — 重複なし・順序なし ──────────────
# [実務] 権限チェック、タグ管理、重複除去、集合演算
a = {1, 2, 3, 4};  b = {3, 4, 5, 6}
a & b   # 積集合 {3, 4}   → 共通の権限・タグを抽出
a | b   # 和集合 {1,2,3,4,5,6}  → 全タグをまとめる
a - b   # 差集合 {1, 2}   → a にあって b にないもの(削除対象の ID など)

# 重複除去(順序は保持されない点に注意)
unique = list(set([1, 2, 2, 3, 3, 3]))  # → [1, 2, 3]

dict のループ — items() / keys() / values()

dict_loop.pypython
person = {"name": "Alice", "age": 30}

# ── items() が返すのはビューオブジェクト ────
# 型は dict_items(リストではない)。タプルのペアとして反復できる。
# イメージ: [("name", "Alice"), ("age", 30)] のように振る舞う
#              ↑ key    ↑ value
# ※ dict が更新されるとビューにも即時反映される(コピーでなく参照)

for k, v in person.items():
    print(k, v)
# → name Alice
# → age 30

# ── タプルのアンパックと呼ばれる書き方 ───────
# for k, v in ... は実はこれを短く書いたもの:
for item in person.items():
    k = item[0]   # タプルの0番目 = key
    v = item[1]   # タプルの1番目 = value
    print(k, v)   # 同じ結果

# JS の Object.entries に対応する:
# Object.entries(person).forEach(([k, v]) => console.log(k, v))
# 「キーと値のペアを順に取り出す」発想は Python も JS も同じ

# ── keys() / values() ──────────────────────
for k in person.keys():
    print(k)   # → name / age
# ※ for k in person: でも同じ(keys() は省略可)
# [実務] 設定項目の一覧表示、必須キーの存在確認

for v in person.values():
    print(v)   # → Alice / 30
# [実務] 値だけ集計したい、重複チェック、全値にバリデーションをかけるとき
🏋️ チャレンジ中級

単語出現頻度カウンタ

文字列を受け取り、各単語の出現回数を辞書で返す word_count(text) を書いてください。

解答python
from collections import Counter

def word_count(text):
    return dict(Counter(text.lower().split()))

# ── 何をやっているか、1ステップずつ ────────
text = "the quick brown fox jumps over the lazy fox"

# ① lower() → 全部小文字に("The" と "the" を同一単語として扱うため)
text.lower()
# → "the quick brown fox jumps over the lazy fox"

# ② split() → スペースで分割してリストに
text.lower().split()
# → ["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "fox"]

# ③ Counter() → 各要素の出現回数を数えてくれる
Counter(text.lower().split())
# → Counter({'the': 2, 'fox': 2, 'quick': 1, 'brown': 1, ...})
# 出現回数の多い順に並べてくれる

# ④ dict() → 普通の dict に変換
dict(Counter(text.lower().split()))
# → {'the': 2, 'fox': 2, 'quick': 1, 'brown': 1, 'jumps': 1, 'over': 1, 'lazy': 1}
04

制御フロー

⏱ 25分

for ループの使い方が他言語と大きく違います。enumeratezipmatch(Python 3.10+)は必須知識。

if 文 / 三項演算子

if.pypython
x = 42

if x > 100:
    print("big")
elif x > 50:
    print("medium")
else:
    print("small")

# ── 三項演算子(条件式)──────────────────
# 値 if 条件 else 別の値
label = "even" if x % 2 == 0 else "odd"
# [実務] status = "active" if user.is_active else "inactive"
# → 代入やリスト内包表記の中に自然に入れられる

# ── 連鎖比較(Python 独自!)─────────────
# 1 <= x <= 100 は 1<=x and x<=100 と等価
1 <= x <= 100    # True → 範囲チェックが数学的な記法で書ける
# [実務] if 0 <= index < len(array): (境界チェックが1行で済む)

# ── 論理演算子(and / or / not)──────────
# &&, ||, ! ではなく英単語を使う
if x > 0 and x < 100:
    pass  # 「何もしない」プレースホルダー。構文上ブロックが必要だけど処理がまだないとき
# → and/or はショートサーキット評価(左辺が確定したら右辺を評価しない)
# [実務] user = cache.get(key) or fetch_from_db(key)(キャッシュフォールバック)

for ループ — イテレータベース

for.pypython
# ── 基本 for ─────────────────────────────
for i in range(5):               # 0,1,2,3,4
    pass                         # プレースホルダー(ここでは「ループできること」を確認するだけ)

# ── enumerate — インデックス + 値 ─────────
# [実務] 番号付きリストの出力、エラー箇所の行番号特定、進捗表示
for i, fruit in enumerate(["apple","banana"], start=1):
    print(f"{i}. {fruit}")       # 1. apple / 2. banana
# start=1 で1始まりのインデックスに(画面表示やレポートに便利)

# ── zip — 複数イテラブルを並行処理 ─────────
# [実務] ID とデータの対応付け、入力と期待値のペア処理、2つのAPIレスポンスの比較
names  = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
for name, score in zip(names, scores):
    print(f"{name}: {score}")
# → 短い方に合わせて自動で終了(長さを揃える必要がない)

# ── for / else ────────────────────────────
# else は break されなかった場合のみ実行される(他言語にない Python 独自構文)
for i in range(10):
    if i == 5: break
else:
    print("break されなかった")  # 今回は実行されない
# [実務] 「全件走査したが見つからなかった」パターン:
# for item in records:
#     if item.id == target: return item
# else:
#     raise NotFoundError(target)

match 文(Python 3.10+)

match.pypython
# ── match 文(Python 3.10+)──────────────
# switch/case の強化版。「値が等しいか」だけでなく「構造(形)が合うか」でマッチできる
# 上から順に評価し、最初にマッチした case だけ実行される(break 不要)

# ── パターン①: タプルの構造マッチ ─────────
command = ("move", 10, 20)

match command:
    case ("move", x, y):
        # タプルの形が一致 → x=10, y=20 に自動でバインドされる
        print(f"移動: ({x}, {y})")
    case ("fire", direction):
        print(f"発射: {direction}")
    case ("quit",) | ("exit",):
        # | で複数パターンを OR 指定できる
        print("終了")
    case _:
        # _ はワイルドカード(どれにもマッチしない場合のデフォルト)
        print("不明なコマンド")

# ── パターン②: dict の構造マッチ ──────────
# [実務] APIレスポンスの分岐処理。if/elif の連鎖より意図が明確になる
response = {"status": "error", "code": 404, "message": "Not Found"}

match response:
    case {"status": "ok", "data": data}:
        # "status"が"ok"かつ"data"キーが存在するとき → data に値がバインドされる
        # 余分なキー(他のフィールド)があっても問題なくマッチする
        print(f"成功: {data}")
    case {"status": "error", "code": 404}:
        print("ページが見つかりません")
    case {"status": "error", "code": 500}:
        print("サーバーエラー")
    case _:
        print("予期しないレスポンス")

# ── パターン③: 値の直接マッチ ─────────────
# 単純な分岐なら if/elif でも十分だが、match の方が読みやすくなることも
match response["code"]:
    case 200 | 201:
        print("成功")       # | で複数値を OR
    case 400 | 404:
        print("クライアントエラー")
    case code if code >= 500:
        # guard 条件(if の追加条件)でさらに絞り込める
        print(f"サーバーエラー: {code}")
🏋️ チャレンジ初級

FizzBuzz(内包表記で1行)

1〜30のFizzBuzzをリスト内包表記1行で書いてください。

解答python
result = [
    "FizzBuzz" if i%15==0 else "Fizz" if i%3==0 else "Buzz" if i%5==0 else str(i)
    for i in range(1, 31)
]
05

関数

⏱ 30分

Python の関数はファーストクラスオブジェクト。*args**kwargs・デコレータを理解すると表現力が一気に上がります。

基本と型ヒント

functions.pypython
# ── 型ヒント付き関数定義 ──────────────────
# 型ヒントは実行時に強制されないが、IDE補完・mypy 静的解析に活きる
from typing import Optional

def greet(name: str, greeting: str = "Hello") -> str:
    # デフォルト引数は右側から。デフォルト引数の右に非デフォルトは置けない
    return f"{greeting}, {name}!"

# ── 複数の戻り値(実体はタプル)──────────
# [実務] min/max を同時に返す、(成功フラグ, 結果値)、(値, エラー) など
def min_max(nums: list) -> tuple[int, int]:
    return min(nums), max(nums)   # カンマ区切りで暗黙的にタプルになる

low, high = min_max([3, 1, 4, 1, 5, 9])  # アンパックで受け取る

# ── Optional — None を許容する型 ─────────
# Optional[T] は T | None の糖衣構文(Python 3.10+ は T | None と書ける)
# [実務] DBの検索結果(見つからない場合はNone)、キャッシュのヒット/ミス
def find_user(user_id: int) -> Optional[dict]:
    db = {1: {"name": "Alice"}}
    return db.get(user_id)  # なければ None を返す(KeyError にならない)

*args と **kwargs

args_kwargs.pypython
# ── *args — 可変長位置引数 ────────────────
# 引数の数が決まっていない関数に使う。args はタプルとして受け取る
# [実務] sum 系の集約関数、ロギング、テストの assert ヘルパー
def total(*args: float) -> float:
    return sum(args)

total(1, 2, 3, 4, 5)    # 15 → 引数の数は任意

# ── **kwargs — 可変長キーワード引数 ────────
# [実務] HTML タグ生成、ORM クエリビルダ、設定の動的な受け渡し
def create_tag(name: str, **attrs) -> str:
#              ↑普通の引数  ↑ ** をつけると
#                            キーワード引数を全部 dict で受け取る
    attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
    return f"<{name} {attr_str}>"

create_tag("input", type="text", placeholder="名前を入力")
#           ↑ name  ↑ ここから先が attrs にまとめて入る

# 関数の中では attrs がこうなっている:
# attrs = {"type": "text", "placeholder": "名前を入力"}

# " ".join(...) の行を分解すると:
# ① attrs.items() で k, v を取り出して
# ② f'{k}="{v}"' で文字列に変換
#    → ['type="text"', 'placeholder="名前を入力"']
# ③ " ".join(...) でスペースつなぎに
#    → 'type="text" placeholder="名前を入力"'
# ④ f"<{name} {attr_str}>" に埋め込む
#    → ''

# ── *args と **kwargs を同時に使う ─────────
# *args  → 位置引数を何個でもタプルで受け取る
# **kwargs → キーワード引数を何個でも dict で受け取る
def debug(*args, **kwargs):
    print(args)    # → (1, 2, 3)
    print(kwargs)  # → {'name': 'Alice', 'age': 30}

debug(1, 2, 3, name="Alice", age=30)
# カンマの前(位置引数)→ args、name=... 以降(キーワード引数)→ kwargs に入る
# [実務] ラッパー関数や decorator の wrapper(*args, **kwargs) で元の引数をそのまま通すときに使う

# ── アンパックして渡す(逆方向の使い方)──
# リスト/タプルを * で展開して位置引数に、dict を ** で展開してキーワード引数に
nums = [1, 2, 3]
print(*nums)              # print(1, 2, 3) と同じ
config = {"sep": "-", "end": "\n"}
print(*nums, **config)    # print(1, 2, 3, sep="-", end="\n") と同じ
# [実務] func(**config_dict) で設定 dict をそのまま引数として渡せる
◆ EXTRA 深掘りコンテンツ — 時間があるときに

デコレータ

decorator_motivation.pypython
# ── デコレータが必要になる理由 ────────────
# 「処理時間を計測したい」という要件が出たとき、素朴に書くとこうなる:

start = time.perf_counter()
result1 = slow_sum(10_000_000)
print(f"slow_sum: {time.perf_counter()-start:.4f}s")

start = time.perf_counter()
result2 = fast_sum(10_000_000)
print(f"fast_sum: {time.perf_counter()-start:.4f}s")

start = time.perf_counter()
result3 = call_api("https://...")
print(f"call_api: {time.perf_counter()-start:.4f}s")

# → 同じ3行が関数の数だけ散らばる
# → 計測をやめたくなったら全箇所を直す羽目になる
# → 本来の処理(slow_sum の呼び出し)が計測コードに埋もれて読みにくい

# ── デコレータありだと ─────────────────────
@timer
def slow_sum(n): ...

@timer
def fast_sum(n): ...

@timer
def call_api(url): ...

# やめたいときは @timer を消すだけ。関数の中身は一切触らない

# ── 本質は「横断的関心事の分離」 ─────────
# 本来の処理
def slow_sum(n):
    return sum(range(n))   # ← これだけ書きたい

# 計測・ログ・リトライは「横断的な関心事」
# どの関数にも付けたいけど、関数の中身とは無関係
# → デコレータに切り出すことで、本処理と付加処理を完全に分離できる
decorator.pypython
# ── デコレータの仕組み ─────────────────────
# @timer は slow_sum = timer(slow_sum) と等価
# 関数を「ラップ」して前後に処理を挟む
# [実務] 計測・ログ・認証チェック・キャッシュ・リトライなど横断的関心事に使う
import time, functools

def timer(func):
    @functools.wraps(func)   # func の __name__ などのメタデータを維持
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)    # 元の関数を実行
        print(f"{func.__name__}: {time.perf_counter()-start:.4f}s")
        return result                     # 元の戻り値をそのまま返す
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)     # slow_sum: 0.1234s

# ── 引数付きデコレータ ─────────────────────
# デコレータ自体に引数を渡したいとき → 3層のネストになる
# [実務] @retry(times=3) でAPIの一時的な障害に自動リトライ
def retry(times=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if i == times - 1: raise  # 最後の試行では例外を再送出
        return wrapper
    return decorator

@retry(times=3)
def unstable_api(): ...  # ネットワーク障害などで失敗しうる関数
🏋️ チャレンジ中級

memoize デコレータ

functools.lru_cache を使わず、関数結果をキャッシュする @memoize デコレータを手書きで実装してください。

解答python
import functools

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)

print(fib(40))  # 一瞬で返る
06

クラスとOOP

⏱ 35分

self の明示・ダンダーメソッド・@dataclass など Python 固有の作法を押さえましょう。

クラス基本

class.pypython
# ── クラス変数 vs インスタンス変数 ──────────
# クラス変数: 全インスタンスで共有(カウンタ・定数など)
# インスタンス変数: インスタンスごとに独立(self.xxx で定義)
class Animal:
    count = 0  # クラス変数

    def __init__(self, name: str, sound: str):
        # self を忘れるとローカル変数になる(Python の最初のハマりポイント)
        self.name  = name    # インスタンス変数
        self.sound = sound
        Animal.count += 1   # クラス変数はクラス名経由で更新

    # ── ダンダー(__xx__)メソッド ───────
    # [実務] これを実装すると print/==/>=/len などの組み込み操作が使えるようになる
    def __repr__(self) -> str:
        # Java の toString() と全く同じ概念
        # 未実装だと repr(obj) や REPL での表示が
        # → <__main__.Animal object at 0x10f3a2d50> のようなメモリアドレスになる
        return f"Animal({self.name!r})"

    def __str__(self) -> str:
        # print(obj) / str(obj) の出力。未実装なら __repr__ にフォールバックする
        return self.name

    def __eq__(self, other) -> bool:
        # == 演算子の挙動を定義(未定義だとメモリアドレス比較になる)
        return isinstance(other, Animal) and self.name == other.name

    def speak(self) -> str:
        return f"{self.name} says {self.sound}"

    # ── classmethod vs staticmethod ──────
    @classmethod
    def create_dog(cls, name: str) -> "Animal":
        # cls = クラス自身。継承先でも正しいクラスのインスタンスを返せる
        # [実務] ファクトリメソッド(異なる引数パターンでオブジェクト生成)
        return cls(name, "Woof")

    @staticmethod
    def is_valid_name(name: str) -> bool:
        # self も cls も不要な純粋なユーティリティ関数
        # [実務] バリデーション・変換など、クラスと関連するが状態不要な処理
        return bool(name) and name[0].isupper()
◆ EXTRA 深掘りコンテンツ — 時間があるときに

普通のメソッド / classmethod / staticmethod の違い

method_types.pypython
# ── 3つの違いは「何を第一引数で受け取るか」 ──
class MyClass:
    class_var = "クラス変数"

    def __init__(self, value):
        self.value = value

    # ① 普通のメソッド → self でインスタンスを受け取る
    def instance_method(self):
        return self.value        # インスタンスの変数にアクセスできる

    # ② classmethod → cls でクラス自体を受け取る
    @classmethod
    def class_method(cls):
        return cls.class_var     # クラス変数にアクセスできる

    # ③ staticmethod → 何も受け取らない
    @staticmethod
    def static_method(x, y):
        return x + y             # インスタンスもクラスも関係ない純粋な処理

obj = MyClass("hello")
obj.instance_method()        # インスタンスから呼ぶ
MyClass.class_method()       # クラスから直接呼べる
MyClass.static_method(1, 2)  # クラスから直接呼べる
# Java の static メソッドは staticmethod にそのまま対応する

# ── 実務でよく見るパターン ────────────────
class User:
    def __init__(self, name, email):
        self.name  = name
        self.email = email

    # ① 普通のメソッド → そのインスタンスのデータを使う処理
    def greet(self):
        return f"こんにちは、{self.name}です"

    # ② classmethod → 別の形式からインスタンスを作るとき(ファクトリ)
    @classmethod
    def from_dict(cls, data: dict) -> "User":
        return cls(data["name"], data["email"])
    # User.from_dict({"name": "Alice", "email": "alice@example.com"})
    # → DBのレコード、APIレスポンス、CSVの行など別形式から生成するときに使う

    # ③ staticmethod → クラスに関係するがインスタンス不要な処理
    @staticmethod
    def is_valid_email(email: str) -> bool:
        return "@" in email
    # User.is_valid_email("alice@example.com")

# ── 判断基準 ──────────────────────────────
# self が必要              → 普通のメソッド
# cls が必要(生成・カウントなど) → @classmethod
# どちらも不要             → @staticmethod

継承 / @dataclass / @property

oop.pypython
# ── 継承 ─────────────────────────────────
# [実務] 共通の振る舞いを基底クラスに集約(HTTPクライアント・DBモデルなど)
class Dog(Animal):
    def __init__(self, name: str, breed: str):
        super().__init__(name, "Woof")  # 親の __init__ を呼ぶ(必須)
        self.breed = breed

    def speak(self) -> str:
        # super() で親メソッドを呼び出してから拡張できる
        return f"{super().speak()}! ({self.breed})"

# ── @dataclass — ボイラープレートを削減 ────
# __init__/__repr__/__eq__ を自動生成してくれる
# [実務] APIレスポンスのDTO、設定クラス、値オブジェクトに最適
from dataclasses import dataclass, field

@dataclass
class Point:
    x: float         # フィールド定義だけで __init__ が生成される
    y: float
    z: float = 0.0   # デフォルト値付き

    def distance(self) -> float:
        return (self.x**2 + self.y**2 + self.z**2) ** 0.5

p = Point(3, 4)
print(p)                # Point(x=3, y=4, z=0.0)(__repr__ 自動生成)
print(p == Point(3, 4)) # True(__eq__ 自動生成)

# ── @property — 計算プロパティ ─────────────
# [実務] 単位変換、derived 値(full_name = first + last)、バリデーション付き setter
class Temperature:
    def __init__(self, celsius: float = 0):
        self._c = celsius   # _ = プライベート慣習(強制ではなく「触るな」の合図)

    @property
    def fahrenheit(self) -> float:
        # t.fahrenheit で呼べる(メソッド呼び出しに見えない)
        return self._c * 9/5 + 32

    @fahrenheit.setter
    def fahrenheit(self, v: float):
        # t.fahrenheit = 98.6 で書き込める
        self._c = (v - 32) * 5/9

@property を使う3つのメリット

property_benefits.pypython
# ── ① 外から見るとただの変数、内部では計算してる ──
# Java の getAge() を age で呼べるようにした感じ
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14 * self.radius ** 2

c = Circle(5)
c.area()   # @property なし → ()が必要、「計算してる感」が出る
c.area     # @property あり → ()不要、変数みたいに自然に読める

# ── ② 値を守れる(setter でバリデーション)──
class Person:
    def __init__(self, age):
        self._age = age   # _ でプライベートを示す

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("年齢は0以上にしてください")
        self._age = value

p = Person(30)
p.age = -1   # → ValueError(self._age に直接代入だと防げない)
# [実務] DBに保存する前の値の検証、型チェック、範囲チェックに使う

# ── ③ 後から変更しても呼び出し側に影響しない ──
# 最初はただの変数として作っていた
class User:
    def __init__(self, first, last):
        self.full_name = f"{first} {last}"   # 変数として持ってた

# 後から「毎回計算したい(firstやlastが変わる可能性がある)」に変わっても
class User:
    def __init__(self, first, last):
        self.first = first
        self.last  = last

    @property
    def full_name(self):
        return f"{self.first} {self.last}"

# 呼び出し側は一切変更不要
user.full_name   # どちらのバージョンでも同じ書き方で動く

# ── まとめ ────────────────────────────────
# 記載が短くなる          → おまけ程度
# 変数みたいに読める      → 可読性が上がる
# setter でバリデーション → 不正な値を防げる
# 後から変更しやすい      → 呼び出し側に影響しない
🏋️ チャレンジ中級

Stack クラスを実装する

push()pop()peek()is_empty()__len__ を持つ Stack を実装してください。

解答python
class Stack:
    def __init__(self): self._data = []
    def push(self, item): self._data.append(item)
    def pop(self):
        if self.is_empty(): raise IndexError("empty stack")
        return self._data.pop()
    def peek(self):
        if self.is_empty(): raise IndexError("empty stack")
        return self._data[-1]
    def is_empty(self): return len(self._data) == 0
    def __len__(self):  return len(self._data)
    def __repr__(self): return f"Stack({self._data})"
07

モジュールと標準ライブラリ

⏱ 25分

Python の標準ライブラリは「バッテリー同梱」で非常に充実しています。よく使うモジュールを手早く押さえましょう。

stdlib.pypython
# ── collections ───────────────────────────
from collections import Counter, defaultdict, deque

# Counter: 要素の出現回数を自動でカウント
words = "apple banana apple cherry apple".split()
Counter(words).most_common(2)      # [('apple', 3), ('banana', 1)]
# [実務] アクセスログの集計、投票結果のカウント、テキスト頻度分析

# ── deque — 先頭への追加が速い ───────────
# list で先頭に追加すると全要素を右にずらす → O(n)
lst = ["b", "c"]
lst.insert(0, "a")   # 遅い: 全要素を右にずらしてから挿入

# deque なら先頭にポインタを付け足すだけ → O(1)
dq = deque(["b", "c"])
dq.appendleft("a")   # 速い

# ── maxlen と組み合わせると「最新N件」が自動で維持される ──
log = deque(maxlen=5)

log.appendleft("1件目")   # → ['1件目']
log.appendleft("2件目")   # → ['2件目', '1件目']
log.appendleft("3件目")   # → ['3件目', '2件目', '1件目']
log.appendleft("4件目")   # → ['4件目', '3件目', '2件目', '1件目']
log.appendleft("5件目")   # → ['5件目', '4件目', '3件目', '2件目', '1件目']

log.appendleft("6件目")   # 5件を超えたら末尾の古いものが自動で消える
# → ['6件目', '5件目', '4件目', '3件目', '2件目']
#     ↑最新が先頭                          ↑1件目が自動削除

# list で同じことをやろうとすると自分で管理が必要
lst.insert(0, "6件目")    # 遅い O(n)
if len(lst) > 5:
    lst.pop()              # 自分で古いものを削除しなければならない

# まとめ:
# 先頭に追加     → appendleft で O(1)
# 古いものを削除 → maxlen が自動でやってくれる
# [実務] 直近 N 件のアクセスログ保持、スライディングウィンドウ、BFS のキュー

# ── re — 正規表現 ─────────────────────────
# [実務] ログのパース、バリデーション、データクレンジング
import re
re.findall(r"\d{3}-\d{4}-\d{4}", "Call 090-1234-5678")
# → ['090-1234-5678'](電話番号の抽出)
re.sub(r"\s+", " ", "too   many   spaces")
# → "too many spaces"(余分な空白の正規化)

# ── datetime ──────────────────────────────
# [実務] ログのタイムスタンプ処理、期限計算、スケジューリング
from datetime import datetime, timedelta
now       = datetime.now()
next_week = now + timedelta(days=7)           # 日時の加算
datetime.fromisoformat("2024-01-15T10:30:00") # ISO文字列のパース(APIレスポンスに多い)

# ── itertools ─────────────────────────────
# [実務] データパイプライン、組み合わせ生成、大量データの遅延処理
from itertools import chain, combinations, islice

list(chain([1,2], [3,4]))           # [1,2,3,4](複数イテラブルを連結)
list(combinations("ABC", 2))        # [('A','B'),('A','C'),('B','C')]
# → 特徴量の組み合わせ探索、権限セットの全パターン生成
list(islice(range(1000), 5))        # [0,1,2,3,4](先頭 N 件だけ取得)
# → 無限イテレータから必要な分だけ取り出すとき
◆ EXTRA 深掘りコンテンツ — 時間があるときに

pathlib — ファイルパス操作

pathlib.pypython
# ── pathlib — ファイルパス操作 ────────────
# os.path より直感的なオブジェクト指向 API。/ 演算子でパスを結合できる
from pathlib import Path

# ── 相対パス vs 絶対パス ──────────────────
# 相対パス → スクリプトを実行したディレクトリ(カレントディレクトリ)が基点
p = Path("hello.txt")           # → ./hello.txt
p = Path("output/hello.txt")    # → ./output/hello.txt

# 絶対パス → そのまま解釈される
p = Path("/tmp/hello.txt")      # → /tmp/hello.txt
p = Path.home() / "hello.txt"   # → /Users/atsushi/hello.txt(ホームディレクトリ)

# 今どこを指しているか確認したいとき
p = Path("hello.txt")
print(p.resolve())   # 絶対パスで表示
# → /Users/atsushi/projects/myapp/hello.txt

# ── 実務でよく使うパスパターン ────────────
# ① スクリプトと同じディレクトリ(一番安全)
#    __file__ = このスクリプト自身のパス
#    .parent  = そのファイルがあるディレクトリ
#    → cd でどこから実行しても必ず同じ場所を指す
p = Path(__file__).parent / "config.json"

# ② ホームディレクトリ配下
p = Path.home() / ".myapp" / "settings.json"   # → ~/.myapp/settings.json

# ③ 一時ファイル(テスト・中間出力など)
import tempfile
# mktemp() は競合状態の危険があるため非推奨
# 代わりに TemporaryDirectory / NamedTemporaryFile を使う
with tempfile.TemporaryDirectory() as tmp:
    p = Path(tmp) / "work.json"   # with を抜けると自動削除

# ── 基本操作 ──────────────────────────────
p = Path("data") / "config.json"   # / 演算子でパス結合(OS非依存で \ vs / を気にしなくていい)
p.exists()                          # → True / False(存在確認)
p.parent                            # → Path("data")(親ディレクトリ)
p.name                              # → "config.json"(ファイル名)
p.stem                              # → "config"(拡張子なし)
p.suffix                            # → ".json"(拡張子)
p.mkdir(parents=True, exist_ok=True)  # ディレクトリを作成(親も含めて)
p.write_text("hello", encoding="utf-8")
p.read_text(encoding="utf-8")

# ── ファイル検索 ──────────────────────────
list(Path(".").glob("*.py"))        # カレントの .py ファイル一覧
list(Path(".").glob("**/*.py"))     # 再帰的にすべての .py ファイルを取得
# [実務] バッチ処理対象のファイル収集、ログファイルのローテーション管理

defaultdict — グルーピングの定番パターン

defaultdict.pypython
from collections import defaultdict

# ── 普通の dict でグルーピングしようとすると ──
employees = [
    ("営業", "Alice"), ("開発", "Bob"),
    ("営業", "Charlie"), ("開発", "Dave"),
]

result = {}
for dept, name in employees:
    if dept not in result:      # キーがあるか毎回確認が必要
        result[dept] = []       # なければ空リストを作る
    result[dept].append(name)
# → {"営業": ["Alice", "Charlie"], "開発": ["Bob", "Dave"]}

# ── defaultdict だと ──────────────────────
result = defaultdict(list)      # キーがなければ自動で list() を作る

for dept, name in employees:
    result[dept].append(name)   # キーの存在確認が不要
# → {"営業": ["Alice", "Charlie"], "開発": ["Bob", "Dave"]}

# if dept not in result の確認が消えてすっきりする

# ── 引数は「デフォルト値を作る関数」────────
defaultdict(list)   # キーがなければ []  を作る → グルーピング
defaultdict(int)    # キーがなければ 0   を作る → カウント集計
defaultdict(str)    # キーがなければ ""  を作る
defaultdict(set)    # キーがなければ set() を作る → 重複なしのグルーピング

# int だとカウントに使える
counter = defaultdict(int)
for dept, name in employees:
    counter[dept] += 1          # キーがなくても 0 から始まる
# → {"営業": 2, "開発": 2}

# ── 普通の dict との挙動の違い ─────────────
d  = {}
d["存在しないキー"]              # → KeyError

dd = defaultdict(list)
dd["存在しないキー"]             # → [] が作られて KeyError にならない

# [実務] イベントログを顧客 ID でグルーピング、タグ別にレコードをまとめる、
#        APIレスポンスをカテゴリ別に集約するなど、集計・分類処理全般で使う
08

例外処理

⏱ 30分

Python は「EAFP スタイル」(許可を求めるより許しを請え)を好みます。try/except を積極的に使いましょう。

exceptions.pypython
# ── try / except / else / finally ─────────
# [実務] ファイル読み込み・API呼び出し・DB操作など失敗しうる処理全般に
import json

def load_json(path: str) -> dict:
    try:
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError:
        return {}                # ファイルなし → 空dict を返してアプリを継続
    except json.JSONDecodeError as e:
        print(f"JSONパースエラー: {e}")
        return {}                # JSON 構文エラー → エラー内容をログに出す
    else:
        return data              # 例外がなかった場合のみここを通る(正常系)
    finally:
        print("完了")            # 例外の有無・return に関わらず必ず実行
        # [実務] DB接続のクローズ、ロックの解放など後始末処理に使う

◆ EXTRA 深掘りコンテンツ — 時間があるときに

コンテキストマネージャ — 前処理・後処理を保証する

context_manager.pypython
# ── そもそも with 文とは ──────────────────
# ファイル操作でよく見るやつがコンテキストマネージャ
with open("file.txt") as f:
    data = f.read()
# with ブロックを抜けたら自動で f.close() される

# ── なぜ必要か(with なしの問題)────────────
f = open("file.txt")
data = f.read()
# ここで例外が起きたら close() が呼ばれない!
f.close()             # ← 到達しない可能性がある

# with あり → 例外が起きても close() は必ず呼ばれる
with open("file.txt") as f:
    data = f.read()   # 例外が起きても抜けたときに必ず閉じられる

# ── @contextmanager で自分で作れる ─────────
# 「前処理 → 本処理 → 後処理」を保証する仕組みを自作できる
import time
from contextlib import contextmanager

@contextmanager
def timer():
    start = time.perf_counter()   # ① 前処理(with に入ったとき)
    yield                          # ② ここで with ブロックの中身が実行される
    print(f"{time.perf_counter()-start:.3f}s")  # ③ 後処理(with を抜けたとき)

with timer():
    time.sleep(0.05)   # → 0.050s

# yield の前後がポイント:
# yield より前 → with に入ったときに実行
# yield より後 → with を抜けたときに実行(例外が起きても)

# ── yield で値を渡す → as で受け取れる ──────
@contextmanager
def temp_dir():
    import tempfile, shutil
    path = tempfile.mkdtemp()
    try:
        yield path         # ← path を渡してる
    finally:
        shutil.rmtree(path)  # 必ず削除される

with temp_dir() as d:      # ← yield で渡した path が d に入る
    print(d)               # → /tmp/abc123 みたいな一時ディレクトリのパス

# open() の with 文も内部的に同じ構造:
# yield f → as f で受け取っている

# yield だけの場合は何も渡さないので as は不要
with timer():              # as なし
    time.sleep(0.05)

# ── try/finally で例外が起きても後処理を保証 ──
@contextmanager
def measure(name):         # name は何でも渡せる文字列
    start = time.perf_counter()
    try:
        yield              # 例外が起きてもここを抜けたら finally へ
    finally:
        elapsed = time.perf_counter() - start
        print(f"{name}: {elapsed:.3f}s")   # 例外時も必ず表示

with measure("Anthropic API"):
    call_api()             # → Anthropic API: 0.823s

with measure("DB query"):
    db.execute(...)        # → DB query: 0.012s

endpoint = "/v1/messages"
with measure(f"API {endpoint}"):
    call_api()             # → API /v1/messages: 0.823s(変数も渡せる)

# ── 実務パターン — DB トランザクション ──────
# [実務] 複数のDB操作をまとめて「全部成功 or 全部なかったこと」にしたいとき
@contextmanager
def transaction(db):
    try:
        yield db
        db.commit()        # 正常終了したら commit
    except:
        db.rollback()      # 例外が起きたら rollback
        raise

with transaction(db) as db:
    db.execute("INSERT ...")
    db.execute("UPDATE ...")
    # 両方成功したら commit、どちらかが失敗したら rollback

# ── デコレータとの使い分け ────────────────
# デコレータ            → 関数に対して前後処理を挟む(@timer)
# コンテキストマネージャ → コードブロックに対して前後処理を挟む(with timer():)
context_manager_class.pypython
# ── @contextmanager の正体 ────────────────
# 実は with 文に対応するにはクラスに __enter__ / __exit__ を実装する方法が本来の形
# @contextmanager はそれをシンプルに書けるようにした糖衣構文

# クラスで書くと(本来の形)
class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self          # as で受け取る値(yield の値に対応)

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"{time.perf_counter()-self.start:.3f}s")
        return False         # False = 例外を握りつぶさない(そのまま伝播させる)

with Timer() as t:
    time.sleep(0.05)         # → 0.050s

# @contextmanager で書くと(同じ動作)
@contextmanager
def timer():
    start = time.perf_counter()
    yield                    # __enter__ はここまで
    print(f"{time.perf_counter()-start:.3f}s")  # __exit__ はここから

# 対応関係:
# クラス版の __enter__      ↔  @contextmanager 版の yield より前
# クラス版の yield で返す値 ↔  yield の値(as で受け取れる)
# クラス版の __exit__       ↔  @contextmanager 版の yield より後

# どちらで書いても動きは全く同じ。使い分けはシンプル:
# クラス版          → __exit__ の引数(exc_type, exc_val)で例外情報を細かく制御したいとき
# @contextmanager版 → シンプルに書けるので普段はこちらが主流

カスタム例外 — エラーの種類で処理を分ける

custom_exceptions.pypython
# ── カスタム例外なしだと ──────────────────
# 全部 Exception で投げるしかなく、受け取る側も種類が分からない
raise Exception("nameが空です")
raise Exception("APIの呼び出しに失敗しました")
raise Exception("権限がありません")

try:
    process()
except Exception as e:
    print(e)  # 種類が分からないので同じ処理しかできない

# ── カスタム例外ありだと ──────────────────
# 継承ツリーを作って「種類」を持たせる
class AppError(Exception): ...        # アプリ全体の基底(これをキャッチすれば全部拾える)
class ValidationError(AppError): ...  # 入力値の問題
class AuthError(AppError): ...        # 認証・権限の問題
class ApiError(AppError): ...         # 外部 API の問題

# ── raise する側(投げる側)────────────────
# 問題を検知した場所で、その種類に合った例外を raise する
def validate_user(data):
    if not data.get("email"):
        raise ValidationError(field="email", message="必須項目です")
    if not data.get("name"):
        raise ValidationError(field="name", message="必須項目です")

def check_permission(user):
    if not user.is_authenticated:
        raise AuthError("ログインが必要です")
    if not user.has_role("admin"):
        raise AuthError("管理者権限が必要です")

def call_external_api(url):
    response = requests.get(url)
    if response.status_code != 200:
        raise ApiError(f"API エラー: {response.status_code}")

# ── except する側(受け取る側)────────────
# raise した例外の種類に応じて処理が分岐する
# → raise した場所がどこであっても、型でキャッチできる
try:
    validate_user(data)    # ValidationError を raise するかも
    check_permission(user) # AuthError を raise するかも
    call_external_api(url) # ApiError を raise するかも
except ValidationError as e:
    return f"入力エラー: {e.field} を確認してください"  # ユーザーに返す
except AuthError:
    redirect("/login")                                   # ログイン画面へ
except ApiError:
    alert_oncall()                                       # オンコールに通知
except AppError:
    log("予期しないアプリエラー")                         # それ以外のアプリエラー

# ── フィールド情報を持たせる ──────────────
class ValidationError(AppError):
    def __init__(self, field: str, message: str):
        super().__init__(f"{field}: {message}")
        self.field = field   # どのフィールドが問題かを属性として持てる

raise ValidationError(field="email", message="形式が正しくありません")

# 受け取る側で
except ValidationError as e:
    e.field   # → "email"(問題フィールドを特定できる)
    str(e)    # → "email: 形式が正しくありません"

# ── 継承構造を使った柔軟なキャッチ ─────────
# 親クラスで受けるほど広くキャッチできる
except ValidationError:  # Validation だけ
except AppError:         # アプリのエラー全部
except Exception:        # 予期しないエラーも含め全部

# ── 実務パターン例(API クライアント)──────
# [実務] ライブラリやサービスの境界でこういう構造を作っておくと
#        呼び出し側が「何が起きたか」を型で判断できる
class AnthropicError(Exception): ...
class RateLimitError(AnthropicError): ...   # レート制限
class TokenLimitError(AnthropicError): ...  # トークン上限

try:
    call_api()
except RateLimitError:
    time.sleep(60)        # 待ってリトライ
except TokenLimitError:
    truncate_prompt()     # プロンプトを短くする
⚠️
裸の except は避ける

何も書かない except:KeyboardInterrupt(Ctrl+C)や SystemExit まで飲み込み、プログラムを強制終了できなくなります。except Exception: はこれらを捕捉しないので安全ですが、それでも幅が広すぎます。できるだけ具体的な例外クラスを指定しましょう。

09

ファイルI/O とマルチスレッド

⏱ 35分

with 文でファイルを安全に開閉。JSON・CSV の読み書きは実務で頻出です。

file_io.pypython
# ── テキストファイル ──────────────────────
# with を使うとブロック終了時にファイルが自動クローズされる(例外発生時も)
# [実務] 設定ファイルの読み書き、ログ出力、レポート生成
with open("data.txt", "w", encoding="utf-8") as f:
    f.write("1行目\n")
    f.writelines(["2行目\n", "3行目\n"])  # リストをまとめて書き込む

with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()          # ファイル全体を文字列で取得
    # for line in f:            # メモリ効率的な反復(GB 級の大きいファイル向け)
    #     process(line.strip()) # strip() で末尾の改行を除去

# ── JSON ──────────────────────────────────
# [実務] APIの設定ファイル、フロント↔バック間のデータ交換、キャッシュ保存
import json

data = {"user": "Alice", "scores": [85, 92, 78]}

# 書き込み: ensure_ascii=False → 日本語をそのまま保存(あ にしない)
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

with open("data.json", encoding="utf-8") as f:
    loaded = json.load(f)

# ファイルを経由せず文字列で変換(APIレスポンス生成、テストのフィクスチャ作成など)
json_str = json.dumps(data, ensure_ascii=False)   # dict → str
parsed   = json.loads(json_str)                   # str → dict

# ── CSV ───────────────────────────────────
# [実務] 売上データ、ユーザーリスト、機械学習の入力データ
import csv

with open("users.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name","age"])
    writer.writeheader()    # ヘッダ行を書く
    writer.writerows([{"name":"Alice","age":30},{"name":"Bob","age":25}])
    # DictWriter を使うと列順が保証され、フィールド名でアクセスできる
◆ EXTRA 深掘りコンテンツ — 時間があるときに

一行ずつ読み込む(大きなファイル向け)

line_by_line.pypython
# ── テキスト — 一行ずつ ───────────────────
# f.read() はファイル全体をメモリに展開する
# for line in f は1行ずつ読むのでどんな大きさのファイルでも O(1) メモリ
with open("access.log", encoding="utf-8") as f:
    for line in f:
        line = line.strip()      # 末尾の改行を除去
        if not line:             # 空行をスキップ
            continue
        process(line)

# ── CSV — 一行ずつ ────────────────────────
# DictReader を使うと各行が {"name": "Alice", "age": "30"} の dict になる
import csv

with open("users.csv", encoding="utf-8") as f:
    reader = csv.DictReader(f)    # 1行目をヘッダとして自動で使う
    for row in reader:            # 1行ずつ dict として取り出せる
        print(row["name"], row["age"])
        # 値は全部 str なので数値が必要なら int(row["age"]) に変換

# ── JSONL — 一行ずつ(JSON Lines 形式)──────
# JSONL = 1行に1つの JSON オブジェクトが並ぶ形式
# ログ・機械学習データセット・イベントストリームでよく使われる
#
# events.jsonl の中身のイメージ:
# {"event": "click", "user_id": 1, "ts": "2024-01-01T10:00:00"}
# {"event": "purchase", "user_id": 2, "ts": "2024-01-01T10:01:00"}
# {"event": "click", "user_id": 1, "ts": "2024-01-01T10:02:00"}

import json

with open("events.jsonl", encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        event = json.loads(line)  # 1行ずつ dict に変換
        print(event["event"], event["user_id"])

# [実務] 普通の JSON(json.load)はファイル全体を一括でパースするが
#        JSONL は1行ずつなので GB 級のログでもメモリを使い切らない

JS との違い — 同期がデフォルト

sync_vs_async.pypython
# ── JS と Python で挙動が逆 ───────────────────
#
# JS → 非同期がデフォルト(結果を待たずに次へ進む)
#
#   console.log("1番目")
#   fetch(url)           // 待たずに次へ進む
#   console.log("2番目") // fetch の結果が来る前に実行される
#   // → 1番目 → 2番目 → (fetch の結果)
#
# Python → 同期がデフォルト(結果が返るまでここで止まる)
#
import requests
print("1番目")
requests.get(url)        # 結果が返るまでここで止まる
print("2番目")           # requests が終わってから実行される
# → 1番目 → (待つ) → 2番目

# ── Python で非同期にしたいときは明示的に書く ──
# asyncio + await を使う(JS の async/await と同じ考え方)
import asyncio
import httpx                           # 非同期対応の HTTP クライアント

async def main():
    print("1番目")
    async with httpx.AsyncClient() as client:
        response = await client.get(url)   # await で「ここだけ非同期」
    print("2番目")

# asyncio.run(main()) で実行

# ── まとめ ─────────────────────────────────────
# JS     → 非同期がデフォルト。同期にしたければ await を書く
# Python → 同期がデフォルト。非同期にしたければ asyncio/await を書く
#
# ThreadPoolExecutor も同じ発想:
#   デフォルトの直列処理を、明示的に並列化している

マルチスレッドで I/O を並列化する

concurrent_io.pypython
# ── なぜ I/O にマルチスレッドが有効か ────────
# ファイル読み込み・APIコール・DBクエリは「待ち時間」が大部分を占める
# CPU はその間ただ待っているだけ → もったいない
#
# 直列(1つずつ): [─api1(0.8s)─][─api2(0.8s)─][─api3(0.8s)─] → 2.4秒
# 並列(同時に):  [─api1(0.8s)─]
#                  [─api2(0.8s)─]  → 約0.8秒(最も遅いものに揃う)
#                  [─api3(0.8s)─]
#
# Python には GIL(グローバルインタープリタロック)があり
# 同時に実行できる Python コードは1スレッドだけ
# → CPU 計算はスレッドを増やしても速くならない
# → でも I/O 待ちの間は GIL が解放されるので他のスレッドが動ける
# → I/O 処理はマルチスレッドで有効

from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

# ── executor.map — 最もシンプルな使い方 ──────
# 通常の map() と同じ使い方で、内部で並列に動く
# 「関数」と「引数のリスト」を渡すだけ
def read_file(path):
    return Path(path).read_text(encoding="utf-8")

files = ["log1.txt", "log2.txt", "log3.txt", "log4.txt"]

with ThreadPoolExecutor(max_workers=4) as executor:
    # with を抜けると全スレッドの完了を待ってからクローズする
    results = list(executor.map(read_file, files))
    # map は元のリストの順番通りに結果を返す(完了した順ではない)
# → results = ["log1の中身", "log2の中身", "log3の中身", "log4の中身"]

# max_workers の目安:
# I/O 系(API・ファイル・DB) → CPU コア数に関係なく増やせる(10〜50も珍しくない)
# CPU 系(計算)              → CPU コア数まで(それ以上は効果なし)

# ── 複数 API を並列で叩く ─────────────────────
import requests

def fetch(url):
    response = requests.get(url, timeout=10)
    response.raise_for_status()   # 4xx/5xx なら例外を出す
    return response.json()

urls = [
    "https://api.example.com/users/1",
    "https://api.example.com/users/2",
    "https://api.example.com/users/3",
]

with ThreadPoolExecutor(max_workers=10) as executor:
    responses = list(executor.map(fetch, urls))
# 3つの API コールが同時に走る → 直列の約 1/3 の時間で完了
# [実務] ユーザー一覧の詳細を一括取得、複数エンドポイントのヘルスチェック

# ── submit — エラー処理を個別にしたいとき ────
# map は1つでも例外が起きると全体が止まる
# submit は Future オブジェクトを返し、結果を1つずつ取り出せる
#
# Future = 「まだ完了していないかもしれない結果の入れ物」
# submit を呼んだ瞬間に処理が始まり、result() を呼んだときに完了を待つ

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = {executor.submit(fetch, url): url for url in urls}
    # この時点で3つのリクエストが同時に走り始めている

for future, url in futures.items():
    try:
        result = future.result()   # 完了するまでここでブロック(待つ)
        print(f"{url}: OK")
    except Exception as e:
        print(f"{url} 失敗: {e}")  # 1つ失敗しても他の結果はそのまま使える

# ── CPU ヘビーな処理は ProcessPoolExecutor ────
# スレッドは同一プロセス内で動くため GIL の制限を受ける
# ProcessPoolExecutor は別プロセスを立ち上げるので GIL を回避できる
# → 画像変換・動画処理・大規模な数値計算に有効
# → プロセス間のデータのやりとりにオーバーヘッドがあるので
#   小さいデータを大量に処理するより、重い処理を少数並列化するのに向く
from concurrent.futures import ProcessPoolExecutor

def heavy_compute(n):
    return sum(i**2 for i in range(n))   # CPU ヘビーな計算

with ProcessPoolExecutor() as executor:  # デフォルト: CPU コア数分のプロセスを起動
    results = list(executor.map(heavy_compute, [10**6, 10**6, 10**6]))

# ── 使い分けまとめ ────────────────────────────
# ファイル・API・DB(I/O 待ちが多い) → ThreadPoolExecutor
# 計算・画像処理(CPU が主役)        → ProcessPoolExecutor
10

仮想環境とpip

⏱ 10分

プロジェクトごとに依存パッケージを隔離する仮想環境は必須の習慣です。

terminalbash
# ── 仮想環境の作成と有効化 ────────────────
# [実務] プロジェクトごとにパッケージを隔離。チーム全員が同じ環境を再現できる
python3 -m venv .venv            # .venv ディレクトリに仮想環境を作成

source .venv/bin/activate        # macOS / Linux で有効化
.venv\Scripts\activate           # Windows で有効化

# プロンプトが (.venv) に変わったら有効
# → この状態の pip install は .venv/lib/ にのみインストールされる

# ── パッケージ管理 ────────────────────────
pip install requests             # 最新バージョンをインストール
pip install "requests>=2.28"     # バージョン制約をつけてインストール
pip install -r requirements.txt  # ファイルに書いたパッケージを一括インストール
# [実務] CI/CD や新メンバーのセットアップで requirements.txt を使う

pip freeze > requirements.txt    # 現在のパッケージ一覧をファイルに書き出す
pip list                         # インストール済みパッケージ一覧を表示
deactivate                       # 仮想環境を無効化してシステム Python に戻る
💡
uv(最近のトレンド)

Rust 製の超高速パッケージマネージャ uv が急速に普及中。uv inituv add requests で pip + venv を丸ごと置き換えられます。

11

Pythonicな書き方

⏱ 20分

「Pythonらしい」イディオムを身につけると、コードが短く・速く・読みやすくなります。

pythonic.pypython
# ── アンパックと多重代入 ──────────────────
# [実務] 座標・色・ペア値の分解、関数の複数戻り値の受け取り
a, b = 1, 2
a, b = b, a                    # スワップ(temp変数不要)
first, *middle, last = [1,2,3,4,5]
# → first=1, middle=[2,3,4], last=5
# [実務] header, *rows = csv_lines(先頭行をヘッダとして分離)

# ── ジェネレータ — メモリ効率的 ──────────
# [] をつけるとリスト(全要素をメモリに保持)
# () をつけるとジェネレータ(1つずつ生成するため O(1) メモリ)
total = sum(x**2 for x in range(1_000_000))
# [実務] 大規模ファイルの行処理、DBの全レコードを逐次処理するとき

# ── any / all ─────────────────────────────
nums = [2, 4, 6, 8]
all(x % 2 == 0 for x in nums)  # True(全て偶数?)
# [実務] バリデーション: all(v > 0 for v in values)(全部正数か)
any(x > 5     for x in nums)   # True(5以上が存在?)
# [実務] 権限チェック: any(p in user.permissions for p in required)

# ── dict アンパック(設定合成に便利)──────
# 右辺が優先。「デフォルト値に上書きを重ねる」パターン
defaults  = {"debug": False, "port": 8080}
overrides = {"port": 9090, "host": "0.0.0.0"}
config = {**defaults, **overrides}
# → {"debug": False, "port": 9090, "host": "0.0.0.0"}
# [実務] テスト用フィクスチャ生成、環境変数による設定の上書き

# ── walrus operator := (Python 3.8+)──────
# 代入と評価を同時に行う。「計算してからチェック」が1行になる
import re
data = "User: Alice, Age: 30"
if m := re.search(r"Age: (\d+)", data):
    print(f"年齢: {m.group(1)}")  # マッチした場合のみ実行
# [実務] while chunk := f.read(8192): ...(ファイルのチャンク読み込み)

# ── sorted / min / max の key 引数 ─────────
# key= に関数を渡すと任意の基準でソート・選択できる
users = [{"name":"Bob","age":25},{"name":"Alice","age":30}]
youngest = min(users, key=lambda u: u["age"])
# → {"name": "Bob", "age": 25}
# [実務] API結果を更新日時でソート、最安値商品の抽出

# ── str.join(ループ連結より高速)─────────
# + で文字列を繰り返し結合すると O(n²)。join は O(n)
",".join(map(str, [1, 2, 3]))   # "1,2,3"
# [実務] CSV行の組み立て、タグのカンマ区切り出力
次のステップ

ここまでマスターしたら次は:型ヒント + mypy(静的解析)、pytest(テスト)、asyncio(非同期)、pydantic(バリデーション)がおすすめです。

🏋️ 最終チャレンジ総合

ミニ CSV 集計ツール

CSV を読み込み、指定カラムの合計・平均・最大・最小を返す summarize(path, column) を書いてください。

解答python
import csv, io

def summarize(path_or_file, column: str) -> dict:
    if isinstance(path_or_file, str):
        f, close = open(path_or_file, encoding="utf-8"), True
    else:
        f, close = path_or_file, False
    try:
        values = [float(row[column]) for row in csv.DictReader(f)]
    finally:
        if close: f.close()
    return {"count": len(values), "sum": sum(values),
            "mean": sum(values)/len(values),
            "min": min(values), "max": max(values)}

sample = io.StringIO("name,score\nAlice,85\nBob,92\nCharlie,78\n")
print(summarize(sample, "score"))
# {'count': 3, 'sum': 255.0, 'mean': 85.0, 'min': 78.0, 'max': 92.0}

おつかれさまでした

Pythonの主要概念を網羅しました。あとは実際のプロジェクトで使いながら定着させていきましょう。 公式ドキュメント →