
関数がどんどん太ってテストしづらい…どう整理すればいい?
globalやprintが混ざっていて、ユニットテストを書く気力が出ない…
この記事では、関数とスコープの設計を“純度×境界×型”の視点で整え、再利用性とテスト容易性を一気に高める手順を、実務でそのまま使えるコード付きで解説します。
この記事で身に付く力
この記事で身に付く力
- 副作用を端に寄せる関数設計(純度)と、I/Oと計算の分離(境界)
- 型ヒントで意図をコード化(型)し、テスト容易性を上げる
- DI(依存注入)/キャッシュ/部分適用/dataclassの実務レシピ
- LEGBスコープや可変デフォルトの落とし穴の回避
関数設計のコア原則:“純度×境界×型”
純度(Pure Function)は、同じ入力に対して常に同じ出力を返し、副作用(I/Oや状態変更)を持たない関数です。これを守るために、I/Oは端へ、計算は中心への原則で境界を引き、型ヒントで意図をコードに埋め込みます。まずは関数署名から整えましょう。
関数署名の黄金則:(データ, ルール/設定, *, オプション…, 依存)
必須は位置引数、任意は「*
」以降のキーワード専用に。読みやすく、変更に強い関数になります。
from typing import Iterable, Sequence
def top\_k(values: Sequence\[float], k: int, \*, descending: bool = True) -> list\[float]:
"""値の上位k件を返す(安定ソート不要想定)。"""
if k <= 0:
return \[]
sorted\_vals = sorted(values, reverse=descending)
return sorted\_vals\[:k]
戻り値は“名前付き”で返す
複数値はタプルのまま返すより、NamedTuple
やdataclass
で意味を持たせると可読性が大きく向上します。
from typing import Iterable, NamedTuple
class Stat(NamedTuple):
mean: float
std: float
def describe(xs: Iterable\[float]) -> Stat:
xs = list(xs)
m = sum(xs) / len(xs)
v = sum((x - m) \*\* 2 for x in xs) / len(xs)
return Stat(mean=m, std=v \*\* 0.5)
I/Oは端へ:計算は中心へ
テストが難しくなる最大要因は、1つの関数にI/Oと計算が混在すること。悪い例と良い例を見比べてみます。
# 悪い例:I/Oと計算が混在しテスト不能
def analyze\_and\_save(path: str) -> None:
rows = open(path).read().splitlines() # I/O
nums = \[int(r) for r in rows]
avg = sum(nums) / len(nums) # 計算
print(avg) # I/O
open("out.txt", "w").write(str(avg)) # I/O
# 良い例:I/Oを端に、計算を中心に
from pathlib import Path
def read\_numbers(p: Path) -> list\[int]: # I/O
return \[int(x) for x in p.read\_text().splitlines()]
def mean(xs: list\[int]) -> float: # 計算(純粋)
return sum(xs) / len(xs)
def write\_text(p: Path, text: str) -> None: # I/O
p.write\_text(text)
# “アプリ層”でつなぐだけ
nums = read\_numbers(Path("in.txt"))
write\_text(Path("out.txt"), f"{mean(nums):.2f}")
テストでは計算関数だけを対象にし、I/Oはモックや一時ディレクトリで最小限確認すればOKです。
(内部リンク:ファイル操作:CSV/JSON/Excelの読み書き / 内部リンク:単体テストpytest入門)
スコープ(LEGB)を3分で把握
Local → Enclosing → Global → Builtins の順に名前解決が行われます。global
の乱用はテスト容易性を損なうため原則禁止。クロージャで外側の変数を更新したい場合のみnonlocal
を使います。
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
return inc
可変デフォルトの罠は必ず回避
デフォルト引数に[]
や{}
を置くと、同じオブジェクトが共有され、バグの温床になります。None
初期化に置き換えましょう。
# NG
def append_item(x, bucket=[]):
bucket.append(x)
return bucket
# OK
def append\_item(x, bucket=None):
if bucket is None:
bucket = \[]
bucket.append(x)
return bucket
実務レシピ:DI/キャッシュ/部分適用/dataclass
現場ですぐ使える4本柱。どれも“純度”を保ったままメンテ性を上げる定番テクニックです。
from collections.abc import Callable
import json
def fetch\_json(url: str, get: Callable\[\[str], str]) -> dict:
"""HTTPクライアント(get)を注入してテスト容易に"""
return json.loads(get(url))
# 本番
import urllib.request
fetch\_json("[https://ex](https://ex)", lambda u: urllib.request.urlopen(u).read().decode())
# テスト(ネットに出ない)
def fake\_get(\_):
return '{"ok"\:true}'
assert fetch\_json("x", fake\_get)\["ok"] is True
from functools import lru_cache
@lru\_cache(maxsize=1024)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n-1) + fib(n-2)
from functools import partial
def discount(price: int, rate: float, \*, cap: int | None = None) -> int:
y = int(price \* (1 - rate))
return min(y, cap) if cap is not None else y
black\_friday = partial(discount, rate=0.3, cap=10000)
black\_friday(12000) # => 10000
from dataclasses import dataclass
from datetime import date
from typing import Iterable
@dataclass(frozen=True)
class Order:
id: int
price: int
created: date
def total\_price(orders: Iterable\[Order]) -> int:
return sum(o.price for o in orders)
置き場所のルール:迷子にならない分割
役割でモジュールを分けるとレビュー・テストが一気に楽になります。
- ETL/整形:
transform.py
(純粋関数)、I/Oはio.py
- 可視化:
plot.py
(図1関数=1意思決定) - 自動化スクリプト:
main.py
は引数パースとワイヤリングだけ。計算はcore.py
- 学習ノート:Notebookで関数化→テスト→再利用へ(内部リンク:Jupyter Notebookの基本)
まずは45分:改善スプリント
- I/Oと計算を分離(関数2つに割る)
- 可変デフォルトを
None
初期化に置換 - 公開関数に型ヒントとdocstringを付与
- 純粋関数を2本だけpytestで守る(内部リンク:単体テストpytest入門)
pytest最小セット(コピペ)
# tests/test_core.py
from core import mean
def test\_mean():
assert mean(\[1,2,3]) == 2
assert mean(\[10,0]) == 5
pip install pytest
pytest -q
FAQ
よくある質問
- Q:戻り値は辞書とdataclassどちらが良い?
A:外部境界(API/JSON)は辞書、内部計算はdataclassやNamedTupleで型安全に。 - Q:global/nonlocalは本当に避けるべき?
A:テスト容易性が下がるため原則不可。必要ならDIに置き換え。 - Q:ユーティリティ関数が増えて迷子になる。
A:用途別モジュールに役割で分割(core/transform/io/plot
)。
まとめ:関数は“小さな契約書”
- 膨張する関数やスコープ事故は、純度×境界×型で防げる
- I/Oは端・計算は中心で分離し、テストしやすく
- 署名の黄金則、DI、キャッシュ、partial、dataclassで実務を加速
- まずは45分スプリントで一歩進める
関連・内部リンク
-
-
【超入門】Pythonの始め方:インストールからHello World|最短で“動く”環境づくり
Pythonは入れる順番と箱(仮想環境)の作り方さえ外さなければ、初心者でも短時間で動きます。この記事では、Windows / macOS / LinuxのOS別に、標準Python+venvを中心と ...
-
-
『読める・速い・止まる』Pythonの制御構文|if / for / whileの設計術
if / for / while、実務ではどれから直せば読みやすくなる? 分岐がぐちゃぐちゃ、ループが遅い、while True が止まらない…その悩み、今日で終わらせましょう。 この記事は、pyth ...
-
-
Python実務の型:例外処理と構造化ログでエラーに強いコードを書く
例外処理って、結局どこまでやれば“実務で困らない”の? ログも整えるのって大変そう…最低限の型、ください! この記事は、pythonbunseki.comの実務トーンで「防ぐ→気づく→復旧する」をコー ...
-
-
【コピペOK】pytestで“壊れないPython”を作る12ステップ
「昨日は動いてたのに、今日は壊れた…」 データ分析やETL、機械学習のコードで多発するこの悲劇。実は“テスト不在”が9割です。 本記事は、pytestで“壊れないPython”を作るための実務ガイド。 ...
-
-
Python標準ライブラリ珠玉の10選|datetime・pathlib・itertoolsで実務が回るチートシート
外部ライブラリを増やさずに、コードの品質と保守性をグッと上げたい…。 標準ライブラリ“だけ”で、どこまで実務が回せる? 今回はそんな悩みを解決するために、Python標準ライブラリの“珠玉の10選”を ...
-
-
もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ
CSV/JSON/Excelの読み書き、どこから気をつければいい? 文字化け・先頭ゼロ欠落・壊れたExcel……もう事故らせたくない! 結論:データ仕事の9割はI/O(入出力)。ここを整えるだけで、桁 ...
-
-
API入門:OpenAPI/HTTPの基本と“壊れない”Pythonクライアント設計(コピペOK)
API連携を始めたいけど、何から学べば“壊れない仕組み”になる? OpenAPI?HTTP?タイムアウト?……用語が多すぎて迷子になりがち。 本記事は、HTTPの基礎×OpenAPIの読み方×堅牢なク ...
設計レビューで“壊れないコード”へ(無料体験あり)
あなたのスクリプトを関数分割 → 境界設計 → 型/テストまで添削し、副業納品レベルへ引き上げます。
TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

株式会社キカガク AI人材長期育成コース(受講料:237,600円~)

最近のコメント