Python基礎

Python関数とスコープの設計術:I/O分離×型ヒントで再利用性とテスト容易性を最大化

関数がどんどん太ってテストしづらい…どう整理すればいい?

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]

戻り値は“名前付き”で返す

複数値はタプルのまま返すより、NamedTupledataclass意味を持たせると可読性が大きく向上します。

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分:改善スプリント

  1. I/Oと計算を分離(関数2つに割る)
  2. 可変デフォルトをNone初期化に置換
  3. 公開関数に型ヒントとdocstringを付与
  4. 純粋関数を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標準ライブラリ
Python標準ライブラリ珠玉の10選|datetime・pathlib・itertoolsで実務が回るチートシート

外部ライブラリを増やさずに、コードの品質と保守性をグッと上げたい…。 標準ライブラリ“だけ”で、どこまで実務が回せる? 今回はそんな悩みを解決するために、Python標準ライブラリの“珠玉の10選”を ...

事故防止・効率化
もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ

CSV/JSON/Excelの読み書き、どこから気をつければいい? 文字化け・先頭ゼロ欠落・壊れたExcel……もう事故らせたくない! 結論:データ仕事の9割はI/O(入出力)。ここを整えるだけで、桁 ...

API入門
API入門:OpenAPI/HTTPの基本と“壊れない”Pythonクライアント設計(コピペOK)

API連携を始めたいけど、何から学べば“壊れない仕組み”になる? OpenAPI?HTTP?タイムアウト?……用語が多すぎて迷子になりがち。 本記事は、HTTPの基礎×OpenAPIの読み方×堅牢なク ...

設計レビューで“壊れないコード”へ(無料体験あり)

あなたのスクリプトを関数分割 → 境界設計 → 型/テストまで添削し、副業納品レベルへ引き上げます。

TechAcademy データサイエンスコース(受講料:174,600円~ ※更に割引あり)

TechAcademy 無料相談

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

キカガク 無料相談

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -Python基礎