2時間でPythonをざっくり掴む
他言語の経験がある方向けのクラッシュコース。文法の説明より「他言語との差分」と「手を動かすコード」に集中します。各セクションをREPLで実際に動かしながら読み進めてください。
各セクションは コア(必須・約2時間)と ◆ EXTRA(深掘り・EXTRA含むと半日)に分かれています。急ぎの場合はコア部分だけで OK です。
Python の世界へ
⏱ 10分まずは Python という言語の「性格」を掴みましょう。import this を実行すると現れる Zen of Python が、Python の哲学を一言で表しています。
>>> 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つの特徴(他言語経験者向け)
Python は動的型付け。変数に型宣言は不要で実行時に型が決まります。Python 3.5+ では型ヒント(def f(x: int) -> str)を使えますが強制はされません。
{} の代わりにインデントがブロックを定義します。スペース4つが慣習(PEP 8)。タブとスペースの混在はエラーになります。
REPL の使い方
# ── バージョン確認 ────────────────────────
$ python3 --version
Python 3.12.0
# ── REPL 起動 ─────────────────────────────
# [実務] 新しいライブラリの動作確認、ワンライナーの検証、データの簡易変換
$ python3
>>> 1 + 1
2
>>> print("Hello, Python!")
Hello, Python!
>>> exit() # または Ctrl+D
# ── スクリプト実行 ────────────────────────
# [実務] バッチ処理、データ変換、CLIツールの実行
$ python3 hello.py
型とリテラル
⏱ 20分Python の基本的な型と、f-string(文字列フォーマットの決定版)を押さえます。
基本型
# ── 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 — モダンな文字列フォーマット
# ── 基本埋め込み ──────────────────────────
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 / 3 は 3.333...(float)。整数除算は 10 // 3 → 3。C/Java の / が整数除算になる点と逆なので注意。
# ── 除算の落とし穴 ────────────────────────
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__ で型名の文字列が取れます。
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]
コレクション
⏱ 40分list・dict・tuple・set の4つが基本。特にリスト内包表記は Python の真骨頂です。
4種類の使い分け早見表
# ── 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 — ミュータブルな配列
# ── 基本操作 ──────────────────────────────
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 は関数型パイプラインに組み込みやすく、元データを変えたくない場合に使う
リスト内包表記
# ── 基本構文 ──────────────────────────────
# [式 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 ─────────────────────────────────
# [実務] 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()
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) を書いてください。
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}
制御フロー
⏱ 25分for ループの使い方が他言語と大きく違います。enumerate・zip・match(Python 3.10+)は必須知識。
if 文 / 三項演算子
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 ─────────────────────────────
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 文(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行で書いてください。
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)
]
関数
⏱ 30分Python の関数はファーストクラスオブジェクト。*args・**kwargs・デコレータを理解すると表現力が一気に上がります。
基本と型ヒント
# ── 型ヒント付き関数定義 ──────────────────
# 型ヒントは実行時に強制されないが、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 — 可変長位置引数 ────────────────
# 引数の数が決まっていない関数に使う。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 をそのまま引数として渡せる
デコレータ
# ── デコレータが必要になる理由 ────────────
# 「処理時間を計測したい」という要件が出たとき、素朴に書くとこうなる:
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)) # ← これだけ書きたい
# 計測・ログ・リトライは「横断的な関心事」
# どの関数にも付けたいけど、関数の中身とは無関係
# → デコレータに切り出すことで、本処理と付加処理を完全に分離できる
# ── デコレータの仕組み ─────────────────────
# @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 デコレータを手書きで実装してください。
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)) # 一瞬で返る
クラスとOOP
⏱ 35分self の明示・ダンダーメソッド・@dataclass など Python 固有の作法を押さえましょう。
クラス基本
# ── クラス変数 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()
普通のメソッド / classmethod / staticmethod の違い
# ── 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
# ── 継承 ─────────────────────────────────
# [実務] 共通の振る舞いを基底クラスに集約(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つのメリット
# ── ① 外から見るとただの変数、内部では計算してる ──
# 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 を実装してください。
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})"
モジュールと標準ライブラリ
⏱ 25分Python の標準ライブラリは「バッテリー同梱」で非常に充実しています。よく使うモジュールを手早く押さえましょう。
# ── 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 件だけ取得)
# → 無限イテレータから必要な分だけ取り出すとき
pathlib — ファイルパス操作
# ── 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 — グルーピングの定番パターン
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レスポンスをカテゴリ別に集約するなど、集計・分類処理全般で使う
例外処理
⏱ 30分Python は「EAFP スタイル」(許可を求めるより許しを請え)を好みます。try/except を積極的に使いましょう。
# ── 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接続のクローズ、ロックの解放など後始末処理に使う
コンテキストマネージャ — 前処理・後処理を保証する
# ── そもそも 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():)
# ── @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版 → シンプルに書けるので普段はこちらが主流
カスタム例外 — エラーの種類で処理を分ける
# ── カスタム例外なしだと ──────────────────
# 全部 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: は KeyboardInterrupt(Ctrl+C)や SystemExit まで飲み込み、プログラムを強制終了できなくなります。except Exception: はこれらを捕捉しないので安全ですが、それでも幅が広すぎます。できるだけ具体的な例外クラスを指定しましょう。
ファイルI/O とマルチスレッド
⏱ 35分with 文でファイルを安全に開閉。JSON・CSV の読み書きは実務で頻出です。
# ── テキストファイル ──────────────────────
# 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 を使うと列順が保証され、フィールド名でアクセスできる
一行ずつ読み込む(大きなファイル向け)
# ── テキスト — 一行ずつ ───────────────────
# 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 との違い — 同期がデフォルト
# ── 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 を並列化する
# ── なぜ 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
仮想環境とpip
⏱ 10分プロジェクトごとに依存パッケージを隔離する仮想環境は必須の習慣です。
# ── 仮想環境の作成と有効化 ────────────────
# [実務] プロジェクトごとにパッケージを隔離。チーム全員が同じ環境を再現できる
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 に戻る
Rust 製の超高速パッケージマネージャ uv が急速に普及中。uv init・uv add requests で pip + venv を丸ごと置き換えられます。
Pythonicな書き方
⏱ 20分「Pythonらしい」イディオムを身につけると、コードが短く・速く・読みやすくなります。
# ── アンパックと多重代入 ──────────────────
# [実務] 座標・色・ペア値の分解、関数の複数戻り値の受け取り
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) を書いてください。
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の主要概念を網羅しました。あとは実際のプロジェクトで使いながら定着させていきましょう。 公式ドキュメント →