- Authors

- Name
- Youngju Kim
- @fjvbn20031
目次
1. プロジェクト構成
1.1 srcレイアウト
モダンなPythonプロジェクトではsrcレイアウトが標準です。ソースコードをsrc/ディレクトリ配下に置くことで、未インストール状態での誤ったインポートを防止します。
my-project/
src/
my_package/
__init__.py
core.py
utils.py
tests/
test_core.py
test_utils.py
pyproject.toml
README.md
1.2 pyproject.toml
setup.pyとsetup.cfgはレガシーです。ビルドシステム、依存関係、ツール設定をpyproject.toml一つに統合しましょう。
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "0.1.0"
description = "My awesome package"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"mypy>=1.8",
"ruff>=0.3",
]
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.mypy]
strict = true
python_version = "3.11"
1.3 仮想環境 - venv vs uv
venv(組み込み):
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
uv(Rustベース、非常に高速):
uv venv
uv pip install -e ".[dev]"
# またはlockfileベースのインストール
uv sync
uvはpipの10〜100倍高速です。2024年以降に開始する新規プロジェクトではuvを強く推奨します。
2. 型ヒント
2.1 基本的な型ヒント
Python 3.10以降では、組み込み型をそのままアノテーションとして使用できます。
# Python 3.10+ - typingインポート不要
def greet(name: str) -> str:
return f"Hello, {name}!"
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Union型は | 演算子で表現
def parse_value(value: str | int | None) -> str:
if value is None:
return "empty"
return str(value)
2.2 TypeVarとGeneric
ジェネリックな関数やクラスを作成する際に使用します。
from typing import TypeVar, Generic
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items.pop()
def peek(self) -> T:
if not self._items:
raise IndexError("Stack is empty")
return self._items[-1]
# 使用例
int_stack: Stack[int] = Stack()
int_stack.push(42)
str_stack: Stack[str] = Stack()
str_stack.push("hello")
2.3 Protocol - 構造的サブタイピング
ダックタイピングを型システムで表現します。継承なしでインターフェースを定義できます。
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
def render(shape: Drawable) -> None:
print(shape.draw())
# CircleはDrawableを継承していないが、draw()があるため互換性あり
render(Circle()) # OK
render(Square()) # OK
2.4 mypy実践設定
[tool.mypy]
strict = true
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
# プロジェクト全体をチェック
mypy src/
# 特定ファイルをチェック
mypy src/my_package/core.py
3. デコレータパターン
3.1 functools.wrapsの使用
デコレータを作成する際は必ずfunctools.wrapsを使用してください。元の関数名とdocstringを保持します。
import functools
import time
from typing import Callable, ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def timer(func: Callable[P, R]) -> Callable[P, R]:
"""関数の実行時間を計測するデコレータ"""
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function(n: int) -> int:
"""nまでの合計"""
return sum(range(n))
slow_function(1_000_000)
# 出力: slow_function took 0.0312s
3.2 パラメータ付きデコレータ
def retry(max_attempts: int = 3, delay: float = 1.0):
"""失敗時にリトライするデコレータ"""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_exception: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"Attempt {attempt} failed: {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exception # type: ignore
return wrapper
return decorator
@retry(max_attempts=5, delay=2.0)
def fetch_data(url: str) -> dict:
# ネットワークリクエストのロジック
...
3.3 クラスベースのデコレータ
class CacheResult:
"""結果をキャッシュするクラスデコレータ"""
def __init__(self, ttl_seconds: int = 300):
self.ttl = ttl_seconds
self.cache: dict[str, tuple[float, object]] = {}
def __call__(self, func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
key = str(args) + str(kwargs)
now = time.time()
if key in self.cache:
cached_time, cached_result = self.cache[key]
if now - cached_time < self.ttl:
return cached_result # type: ignore
result = func(*args, **kwargs)
self.cache[key] = (now, result)
return result
return wrapper
@CacheResult(ttl_seconds=60)
def expensive_computation(x: int) -> int:
time.sleep(2) # 高コスト処理のシミュレーション
return x ** 2
4. コンテキストマネージャ
4.1 with文の仕組み
コンテキストマネージャはリソースの取得と解放を自動的に処理します。__enter__と__exit__マジックメソッドを実装します。
class DatabaseConnection:
def __init__(self, connection_string: str):
self.connection_string = connection_string
self.connection = None
def __enter__(self):
print(f"Connecting to {self.connection_string}")
self.connection = self._connect()
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
self.connection.close()
print("Connection closed")
# Falseを返すと例外が伝播される
return False
def _connect(self):
# 実際の接続ロジック
...
with DatabaseConnection("postgresql://localhost/mydb") as conn:
conn.execute("SELECT 1")
# ブロックを出ると自動的に接続が閉じられる
4.2 contextmanagerデコレータ
シンプルなコンテキストマネージャはcontextlib.contextmanagerで作成できます。
from contextlib import contextmanager
import os
@contextmanager
def temporary_directory(path: str):
"""一時ディレクトリを作成し、使用後に削除"""
os.makedirs(path, exist_ok=True)
try:
yield path
finally:
import shutil
shutil.rmtree(path)
with temporary_directory("/tmp/work") as tmpdir:
# tmpdirを使用した作業
print(f"Working in {tmpdir}")
# ブロック終了時にディレクトリが自動削除
4.3 非同期コンテキストマネージャ
from contextlib import asynccontextmanager
import aiohttp
@asynccontextmanager
async def http_session():
"""aiohttpセッションのライフサイクル管理"""
session = aiohttp.ClientSession()
try:
yield session
finally:
await session.close()
async def fetch_url(url: str) -> str:
async with http_session() as session:
async with session.get(url) as response:
return await response.text()
5. ジェネレータとイテレータ
5.1 ジェネレータの基本
ジェネレータは遅延評価(lazy evaluation) によりメモリを効率的に使用します。
# リスト: 全データをメモリにロード
numbers_list = [x ** 2 for x in range(10_000_000)] # ~80MB
# ジェネレータ: 一つずつ生成、ほぼメモリ使用なし
numbers_gen = (x ** 2 for x in range(10_000_000)) # ~120B
def read_large_file(filepath: str, chunk_size: int = 8192):
"""大容量ファイルをチャンク単位で読み取り"""
with open(filepath, "r") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
# 数GBのファイルもメモリの心配なく処理
for chunk in read_large_file("huge_log.txt"):
process(chunk)
5.2 yield from
ジェネレータの合成や委譲に使用します。
def flatten(nested: list) -> list:
"""ネストされたリストをフラット化"""
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
data = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(data)))
# 出力: [1, 2, 3, 4, 5, 6, 7]
def chain(*iterables):
"""複数のイテラブルを一つに連結"""
for iterable in iterables:
yield from iterable
result = list(chain([1, 2], [3, 4], [5, 6]))
# 出力: [1, 2, 3, 4, 5, 6]
5.3 ジェネレータでパイプライン構築
import csv
from typing import Iterator
def read_csv_rows(filename: str) -> Iterator[dict]:
"""CSVファイルを行ごとに読み取り"""
with open(filename) as f:
reader = csv.DictReader(f)
yield from reader
def filter_active(rows: Iterator[dict]) -> Iterator[dict]:
"""アクティブユーザーのみフィルタリング"""
for row in rows:
if row["status"] == "active":
yield row
def extract_emails(rows: Iterator[dict]) -> Iterator[str]:
"""メールフィールドを抽出"""
for row in rows:
yield row["email"]
# パイプライン: ファイル -> フィルタ -> 変換
pipeline = extract_emails(filter_active(read_csv_rows("users.csv")))
for email in pipeline:
send_newsletter(email)
6. 非同期プログラミング
6.1 asyncioの基本
import asyncio
async def fetch_data(url: str, delay: float) -> str:
print(f"Fetching {url}...")
await asyncio.sleep(delay) # ネットワークリクエストのシミュレーション
return f"Data from {url}"
async def main():
# 逐次実行: 3秒
result1 = await fetch_data("api/users", 1.0)
result2 = await fetch_data("api/posts", 1.0)
result3 = await fetch_data("api/comments", 1.0)
# 並列実行: 1秒
results = await asyncio.gather(
fetch_data("api/users", 1.0),
fetch_data("api/posts", 1.0),
fetch_data("api/comments", 1.0),
)
print(results)
asyncio.run(main())
6.2 aiohttpによる非同期HTTP
import aiohttp
import asyncio
async def fetch_url(session: aiohttp.ClientSession, url: str) -> dict:
async with session.get(url) as response:
return await response.json()
async def fetch_all_users(user_ids: list[int]) -> list[dict]:
async with aiohttp.ClientSession() as session:
tasks = [
fetch_url(session, f"https://api.example.com/users/{uid}")
for uid in user_ids
]
return await asyncio.gather(*tasks)
# 100人のユーザー情報を並列で取得
users = asyncio.run(fetch_all_users(list(range(1, 101))))
6.3 Semaphoreで同時実行数を制限
async def rate_limited_fetch(
urls: list[str],
max_concurrent: int = 10,
) -> list[str]:
"""同時リクエスト数を制限する関数"""
semaphore = asyncio.Semaphore(max_concurrent)
async def fetch_with_limit(session: aiohttp.ClientSession, url: str) -> str:
async with semaphore:
async with session.get(url) as resp:
return await resp.text()
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(session, url) for url in urls]
return await asyncio.gather(*tasks)
6.4 イベントループとTaskGroup(Python 3.11+)
async def main():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_data("api/users", 1.0))
task2 = tg.create_task(fetch_data("api/posts", 1.0))
task3 = tg.create_task(fetch_data("api/comments", 1.0))
# すべてのタスクが完了した時点でここに到達
print(task1.result(), task2.result(), task3.result())
Python 3.11で導入されたTaskGroupは、asyncio.gatherよりも優れたエラーハンドリングを提供します。いずれかのタスクで例外が発生すると、残りのタスクは自動的にキャンセルされます。
7. データクラス
7.1 dataclass
from dataclasses import dataclass, field
@dataclass
class User:
name: str
email: str
age: int
tags: list[str] = field(default_factory=list)
is_active: bool = True
def display_name(self) -> str:
return f"{self.name} ({self.email})"
user = User(name="Kim", email="kim@example.com", age=30)
print(user)
# User(name='Kim', email='kim@example.com', age=30, tags=[], is_active=True)
不変dataclass:
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
# p.x = 3.0 # FrozenInstanceErrorが発生!
7.2 NamedTuple
不変データのための軽量で高速な選択肢です。
from typing import NamedTuple
class Coordinate(NamedTuple):
latitude: float
longitude: float
altitude: float = 0.0
coord = Coordinate(37.5665, 126.9780)
lat, lng, alt = coord # アンパック可能
print(coord.latitude) # 属性アクセス
7.3 Pydantic - データバリデーション
外部入力を扱う場合はPydanticが最善です。自動バリデーション、シリアライゼーション、ドキュメント生成を提供します。
from pydantic import BaseModel, EmailStr, Field, field_validator
class UserCreate(BaseModel):
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(ge=0, le=150)
tags: list[str] = []
@field_validator("name")
@classmethod
def name_must_be_capitalized(cls, v: str) -> str:
if not v[0].isupper():
raise ValueError("Name must start with uppercase")
return v
# 正しい入力
user = UserCreate(name="Kim", email="kim@example.com", age=30)
print(user.model_dump_json())
# 不正な入力 -> ValidationError
try:
bad_user = UserCreate(name="", email="not-an-email", age=-5)
except Exception as e:
print(e)
7.4 使い分けガイド
| シナリオ | 推奨 |
|---|---|
| 内部データ受け渡し | dataclass |
| 不変値オブジェクト | NamedTupleまたはfrozen dataclass |
| API入出力、外部データ | Pydantic BaseModel |
| 設定ファイルのパース | Pydantic BaseSettings |
| DBモデル | SQLAlchemy + dataclassまたはPydantic |
8. デザインパターン
8.1 シングルトン - Python流
class DatabasePool:
_instance: "DatabasePool | None" = None
def __new__(cls) -> "DatabasePool":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialize()
return cls._instance
def _initialize(self) -> None:
self.connections: list = []
print("Pool initialized")
# 常に同じインスタンスを返す
pool1 = DatabasePool()
pool2 = DatabasePool()
assert pool1 is pool2
よりシンプルな方法 - モジュールレベル変数:
# db.py
_pool = None
def get_pool():
global _pool
if _pool is None:
_pool = create_pool()
return _pool
8.2 ファクトリパターン
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None: ...
class EmailNotification(Notification):
def send(self, message: str) -> None:
print(f"Email: {message}")
class SlackNotification(Notification):
def send(self, message: str) -> None:
print(f"Slack: {message}")
class SMSNotification(Notification):
def send(self, message: str) -> None:
print(f"SMS: {message}")
def create_notification(channel: str) -> Notification:
factories: dict[str, type[Notification]] = {
"email": EmailNotification,
"slack": SlackNotification,
"sms": SMSNotification,
}
if channel not in factories:
raise ValueError(f"Unknown channel: {channel}")
return factories[channel]()
notif = create_notification("slack")
notif.send("Hello!")
8.3 オブザーバーパターン
from typing import Callable
class EventEmitter:
def __init__(self) -> None:
self._listeners: dict[str, list[Callable]] = {}
def on(self, event: str, callback: Callable) -> None:
self._listeners.setdefault(event, []).append(callback)
def emit(self, event: str, *args, **kwargs) -> None:
for callback in self._listeners.get(event, []):
callback(*args, **kwargs)
# 使用例
emitter = EventEmitter()
emitter.on("user_created", lambda user: print(f"Welcome, {user}!"))
emitter.on("user_created", lambda user: send_email(user))
emitter.emit("user_created", "Kim")
8.4 ストラテジーパターン
from typing import Protocol
class SortStrategy(Protocol):
def sort(self, data: list[int]) -> list[int]: ...
class BubbleSort:
def sort(self, data: list[int]) -> list[int]:
arr = data.copy()
n = len(arr)
for i in range(n):
for j in range(0, n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
class QuickSort:
def sort(self, data: list[int]) -> list[int]:
if len(data) <= 1:
return data
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
class Sorter:
def __init__(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def sort(self, data: list[int]) -> list[int]:
return self._strategy.sort(data)
# 実行時にストラテジーを切り替え
sorter = Sorter(QuickSort())
print(sorter.sort([3, 1, 4, 1, 5, 9]))
9. パフォーマンス最適化
9.1 プロファイリング - cProfile
import cProfile
import pstats
def expensive_function():
total = 0
for i in range(1_000_000):
total += i ** 2
return total
# プロファイリング実行
profiler = cProfile.Profile()
profiler.enable()
expensive_function()
profiler.disable()
# 結果出力
stats = pstats.Stats(profiler)
stats.sort_stats("cumulative")
stats.print_stats(10)
line_profilerによる行単位分析:
pip install line_profiler
@profile # line_profilerデコレータ
def process_data(data: list[int]) -> list[int]:
result = [] # ほぼ0
for item in data: # ループオーバーヘッド
if item % 2 == 0: # 条件チェック
result.append(item * 2) # 実際の処理
return result
9.2 メモ化 - functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# キャッシュなし: O(2^n) -> 非常に遅い
# キャッシュあり: O(n) -> 即座に完了
print(fibonacci(100))
# キャッシュ統計を確認
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
9.3 リスト内包表記 vs forループ
import timeit
# forループ - 遅い
def squares_loop(n: int) -> list[int]:
result = []
for i in range(n):
result.append(i ** 2)
return result
# リスト内包表記 - 速い(Cレベルの最適化)
def squares_comp(n: int) -> list[int]:
return [i ** 2 for i in range(n)]
# ベンチマーク
n = 100_000
t_loop = timeit.timeit(lambda: squares_loop(n), number=100)
t_comp = timeit.timeit(lambda: squares_comp(n), number=100)
print(f"Loop: {t_loop:.3f}s, Comprehension: {t_comp:.3f}s")
# 内包表記は通常20〜30%高速
9.4 その他のパフォーマンスTips
# 1. 文字列結合にはjoin()を使用
words = ["hello", "world", "python"]
result = " ".join(words) # "+" 演算子よりはるかに高速
# 2. メンバーシップテストにはsetを使用
valid_ids = set(range(10000))
if 42 in valid_ids: # O(1) - リストのO(n)より高速
pass
# 3. collectionsの活用
from collections import Counter, defaultdict
# 頻度カウント
word_count = Counter(["apple", "banana", "apple", "cherry", "apple"])
print(word_count.most_common(2)) # [('apple', 3), ('banana', 1)]
# デフォルト辞書
grouped = defaultdict(list)
for item in [("A", 1), ("B", 2), ("A", 3)]:
grouped[item[0]].append(item[1])
10. テスト
10.1 pytestの基本
# tests/test_calculator.py
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
def test_add_zero():
assert add(0, 0) == 0
# テスト実行
pytest tests/ -v
# カバレッジ付き
pytest --cov=src --cov-report=term-missing
10.2 Fixture
import pytest
@pytest.fixture
def sample_users() -> list[dict]:
return [
{"name": "Kim", "age": 30},
{"name": "Lee", "age": 25},
{"name": "Park", "age": 35},
]
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn # テスト実行
conn.close() # クリーンアップ
def test_user_count(sample_users):
assert len(sample_users) == 3
def test_oldest_user(sample_users):
oldest = max(sample_users, key=lambda u: u["age"])
assert oldest["name"] == "Park"
10.3 Parametrize
同じテストを複数の入力で繰り返します。
@pytest.mark.parametrize("input_val, expected", [
("hello", 5),
("", 0),
("python", 6),
(" ", 3),
])
def test_string_length(input_val: str, expected: int):
assert len(input_val) == expected
@pytest.mark.parametrize("a, b, expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
])
def test_add(a: int, b: int, expected: int):
assert add(a, b) == expected
10.4 Mock
外部依存性を分離してテストします。
from unittest.mock import Mock, patch, AsyncMock
class UserService:
def __init__(self, api_client):
self.api = api_client
def get_user(self, user_id: int) -> dict:
response = self.api.get(f"/users/{user_id}")
return response.json()
def test_get_user():
# MockのAPIクライアントを作成
mock_api = Mock()
mock_api.get.return_value.json.return_value = {
"id": 1,
"name": "Kim",
}
service = UserService(mock_api)
user = service.get_user(1)
assert user["name"] == "Kim"
mock_api.get.assert_called_once_with("/users/1")
# patchによるモック
@patch("my_module.requests.get")
def test_fetch_data(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"data": "test"}
result = fetch_data("https://api.example.com")
assert result["data"] == "test"
10.5 非同期テスト
import pytest
@pytest.mark.asyncio
async def test_async_fetch():
result = await fetch_data("https://api.example.com/users/1")
assert "name" in result
@pytest.mark.asyncio
async def test_concurrent_requests():
results = await asyncio.gather(
fetch_data("url1"),
fetch_data("url2"),
)
assert len(results) == 2
まとめ
優れたPythonを書くためのコア原則をまとめます。
- 型ヒントを活用する - ドキュメントとバグ防止ツールを兼ねます
- pyproject.tomlを使う - すべての設定を一つのファイルに統合しましょう
- コンテキストマネージャでリソースを管理する - with文はPythonのコアイディオムです
- ジェネレータでメモリを節約する - 大容量データ処理に不可欠です
- asyncioでI/Oバウンドな処理を高速化する - ネットワーク・ファイル操作に効果的です
- Pydanticで外部データを検証する - ランタイムの安全性を保証します
- パターンを乱用しない - Pythonicに簡潔に書きましょう
- プロファイリングが先、最適化は後 - 推測ではなく計測に基づきましょう
- pytestでテストを書く - fixtureとparametrizeを積極的に活用しましょう
- uv + ruff + mypyの組み合わせは2026年現在、最も効率的なPythonツールチェーンです
Pythonは単なるスクリプト言語ではありません。正しいパターンとツールを活用すれば、大規模プロダクションシステムでも十分に信頼できる言語です。