はじめに
「Tidy First?」は、ソフトウェア開発におけるアジャイル手法の先駆者であり、エクストリームプログラミング(XP)の考案者であるケント・ベックがコードを整理し、読みやすくするための手法を解説したものです。コードを小さなまとまりに分割しながら整理することで、管理しやすくすることを目的としています。
また、ソフトウェア設計の基礎理論やシステムの構造・ふるまいの変更、プログラミングの体験向上につながる整理のタイミング、大きな変更を安全に進める方法、人間関係の視点からの設計アプローチについても考察します。整頓を少しずつ試しながら、実際の課題解決につなげていくことが重要であると説いています。
本の構成としては、イントロダクション、第Ⅰ部 整頓、第Ⅱ部 管理術、第Ⅲ部 理論 となっていて、本記事では、『Tidy First?』第Ⅰ部 整頓 の1章~16章に登場するコード整理のテクニックについて、Pythonの具体例を交えながら解説します。(※なお、本記事内では説明の為、コードのコメントがやや冗長になっています。)
ちょっとした工夫で、コードが驚くほどスッキリする。本記事では、知っているようで知らない整理術を紹介し、読みやすく、保守しやすいコードを書くためのヒントをお届けします。
1章 ガード節
ガード節は、「ある条件を満たさない場合は処理を中断し、エラーや例外を返したり、別の分岐に移動させたりすることで、後続の処理をガードする」という考え方・テクニックです。コードの可読性や保守性を高めるために、関数などの冒頭で条件チェックを行い、問題があるときは早めにリターンする書き方を指します。
- ガード節を使わない書き方
def process_number(num):
# まず型チェック
if isinstance(num, int):
# 次に負の数でないかチェック
if num >= 0:
print(f"{num} は有効な数値です。処理を続行します。")
# 以下、主要ロジック
else:
print("エラー: 負の数が与えられました。")
else:
print("エラー: 整数ではありません。")
- ガード節を使った書き方
def process_number(num):
# ガード節(的)書き方
if not isinstance(num, int):
print("エラー: 整数ではありません。")
return
if num < 0:
print("エラー: 負の数が与えられました。")
return
# ここまで条件をクリアできたら、以降の処理を安心して書ける
print(f"{num} は有効な数値です。処理を続行します。")
# 以下、主要ロジック
ただし、ガード節が増えすぎると読みにくくなってしまうので、その場合は単一責任の関数に分割し「入力値の検証」「主要ロジック」などを別々の関数・メソッドに切り分け、各関数で本来やるべき前提条件チェックだけを行うようにすると、ガード節が分散され、1つの関数あたりはスリムになります。
- 入力値の検証と主要ロジックを分割する書き方
def check_number(num):
if not isinstance(num, int):
print("エラー: 整数ではありません。")
return False
if num < 0:
print("エラー: 負の数が与えられました。")
return False
return True
def process_number(num):
# チェック用の関数を利用する
if not check_number(num):
# check_number() が False を返したら、そのまま終了
return
# ここまできたら、num は整数かつ 0 以上と保証される
print(f"{num} は有効な数値です。処理を続行します。")
# 何らかの処理
2章 デッドコード
消しましょう!以上!実行されないコードは消すだけです!
後で必要になるかもと思うかもしれませんが、大抵の場合、そんな時は来ません。もしも来たらGitから過去のコミットを取り出すだけです。いつでも元に戻せるし、なんなら必要な時の状況に合わせて書き直した方が良いコードになるかもしれません。
デッドコードを残しておくことにはメンテナンスコストが伴います。必要がなくなったのに無造作にコードが残っていると、以下のようなデメリットがあります。
- 可読性や保守性の低下
- バグの温床になる可能性(繋がっていると思っていた部分が実は未使用など)
- 新規参入メンバーへの混乱を招く
YAGNIの原則:「You Ain't Gonna Need It」(必要なときに必要なものだけ作る。)
3章 シンメトリーを揃える
「コードのシンメトリーを揃える」とは、コード内にある同様の構造・パターン・要素が見やすく並ぶように配置し、視覚的に整列させることを指します。読み手は、違いは違いであることを期待しするので、同じことを違った書き方をすると、同じことを隠す行為になってしまいます。
やり方を1つ学び外もそのやり方に合わせましょう。
- シンメトリーの揃っていないコード
def getUserName(user_id):
# user_id を使ってデータベースからユーザー名を取得する関数
if(user_id==1):
return "Alice"
elif user_id==2:
return "Bob"
elif (user_id == 3):
return "Charlie"
else : return None
def AddUser(name):
# データベースにユーザーを追加する関数
print("ユーザー {} をDBに追加しました。".format(name))
# mainロジック
userList = []
u1 = getUserName(1); u2=getUserName(2); u3 = getUserName(3)
userList.append(u1)
userList.append(u2)
userList.append(u3)
for usr in userList:
if usr!=None:
AddUser(usr)
else:
print("無効なユーザーです。")
- シンメトリーの揃ったコード
def get_user_name(user_id):
"""
user_id に対応するユーザー名を返す。
存在しない場合は None を返す。
"""
if user_id == 1:
return "Alice"
elif user_id == 2:
return "Bob"
elif user_id == 3:
return "Charlie"
else:
return None
def add_user(name):
"""
データベースにユーザーを追加する(ダミー実装)。
"""
print(f"ユーザー {name} をDBに追加しました。")
def main():
"""
メインロジック。
"""
user_list = []
user_ids = [1, 2, 3, 4]
# 1行にまとめず、処理を読み取りやすく分割
for uid in user_ids:
user_name = get_user_name(uid)
user_list.append(user_name)
for user in user_list:
if user is not None:
add_user(user)
else:
print("無効なユーザーです。")
if __name__ == "__main__":
main()
上記のコードは以下の点に違いがあります。
- 関数・変数名の書き方(命名規則)
- シンメトリーの揃ってないコード: getUserName、AddUser などキャメルケース混在で、一貫性がない
- シンメトリーの揃ったコード: get_user_name、add_user のように、Python で一般的なスネークケースに統一
- インデントやコードブロックの整形
- シンメトリーの揃ってないコード: if や else などでインデント幅が揃っておらず、可読性が低い。また、1行に複数の文 (;区切り) を書いている箇所がある
- シンメトリーの揃ったコード: if 文のネストや for 文の範囲が明確で、余計な改行や1行に複数の処理を詰め込まない
- 関数ごとの責務・ドキュメンテーション
- シンメトリーの揃ってないコード: 関数の説明が短いコメントのみで、詳細な使い方や戻り値の説明が無い
- シンメトリーの揃ったコード: 関数に """ """ で囲んだdocstringがあり、「何を受け取って、何を返すのか」という仕様を明示している
- メイン処理のまとめ方
- シンメトリーの揃ってないコード: 変数定義や関数呼び出しがグローバルスコープに直接書かれている
- 良い例: main() 関数を定義し、その中で「ユーザーIDのリストを扱う処理」→「ユーザーを取得してリストに追加する処理」→「DBに登録 or 無効ユーザー判定」という流れを明示的に示している。また、if name == "main": main() という構成はPythonプログラムで標準的
- その他の可読性向上の工夫
- シンメトリーの揃ってないコード: 文字列の組み立てに format を使ったり、文字列連結が混在している
- シンメトリーの揃ったコード: f-string (f"...") を使用して簡潔に書かれている
- シンメトリーの揃ってないコード: IDが 1, 2, 3 だけで固定され、4 のケースがなかった
- シンメトリーの揃ったコード: ループでユーザーIDのリストを回すことで、ユーザー追加の処理がスケーラブルになっている
4章 新しいインターフェイス、古い実装
あるルーチンを呼び出す必要がるけど、インターフェースのせいで難しく、複雑で混乱を招き、面倒になっています。そんな場合は新しいインターフェースを実装してそれを使いましょう。
以下のサンプルコードでは、既存のライブラリ関数 complex_library_function が非常に複雑なインターフェースを持っており、呼び出し側から直接使うのが面倒・混乱しやすい状況を想定しています。そこで、その関数を内包し、よりわかりやすいインターフェースで利用できるクラス SimpleInterface を実装し、呼び出す側の負担を大幅に軽減する例を示します。
- 複雑なインターフェース
def complex_library_function(config1, config2, config3, callback, error_handler, debug=False):
"""
既存の複雑なライブラリ関数。
引数が多く、コールバックやエラーハンドラーまで必要で、
呼び出し元には負担が大きい。
"""
# 実際には複雑な処理が行われているとする
print(f"Running complex_library_function with:\n"
f" config1={config1}\n"
f" config2={config2}\n"
f" config3={config3}\n"
f" debug={debug}")
# 処理結果やエラーをランダムに発生させる(デモ用)
import random
if random.choice([True, False]):
# 正常時: callbackで結果を返す
callback("SuccessResult")
else:
# エラー時: error_handlerを呼ぶ
error_handler()
# -- 以下、使用例 --
def main():
# 既存のライブラリを直接使う場合
# configをバラバラに指定、コールバックとエラーハンドラーを用意、debugフラグも設定…
def success_callback(result):
print(f"[DirectCall] Success: {result}")
def fail_callback():
print("[DirectCall] Error occurred")
# 直接呼び出す
complex_library_function(
config1="direct_conf1",
config2="direct_conf2",
config3="direct_conf3",
callback=success_callback,
error_handler=fail_callback,
debug=True
)
- 複雑なインターフェースを呼び出す新しいインターフェース
class SimpleInterface:
"""
上記の複雑なライブラリ関数を内包し、より単純なインターフェースを提供するクラス。
"""
def __init__(self, config):
"""
コンストラクタで必要最低限の情報だけを受け取り、
内部で複雑なパラメータを組み立てる責務を負う。
"""
self.config = config
# 例として、ライブラリ関数に必要な3つのパラメータをまとめて生成
self.config1 = config.get('config1', 'default_value1')
self.config2 = config.get('config2', 'default_value2')
self.config3 = config.get('config3', 'default_value3')
def do_something(self, debug=False):
"""
呼び出し元はこのメソッドを呼ぶだけで済むようにする。
複雑なライブラリ関数のコールバックやエラーハンドリングは、
このメソッドの中で完結させる。
"""
complex_library_function(
config1=self.config1,
config2=self.config2,
config3=self.config3,
callback=self._handle_success,
error_handler=self._handle_error,
debug=debug
)
def _handle_success(self, result):
"""
ライブラリ関数の成功コールバックを受け取って処理する。
呼び出し元がコールバックを気にしなくてもいいように隠蔽する。
"""
print(f"[SimpleInterface] Success! Result = {result}")
def _handle_error(self):
"""
ライブラリ関数のエラーハンドラー。
呼び出し元がエラー処理を直接指定しなくても済むようにする。
"""
print("[SimpleInterface] Error occurred!")
# -- 以下、使用例 --
def main():
# 追加したインターフェースから使う方法
interface = SimpleInterface({
"config1": "simple_conf1",
"config2": "simple_conf2",
"config3": "simple_conf3"
})
interface.do_something(debug=True)
新しいインターフェースを実装することで、呼び出す側がとても利用しやすくなりました。
5章 読む順番
プログラムを読むとき、最初から最後までずっと目を通して、ようやく「実はこのキーが必要でした」や「この設定がないと動きません」という情報に行き着くことがあります。もし、その重要な前提を冒頭に提示していれば、読む人は次に出てくるコードをすぐに理解できるようになります。
- 悪い例
- コードを最後まで読んでようやく「重要な前提(APIキーが必要など)」を知ることになる。
- 途中で「なぜこの処理をしているのか」が分からず、読み手に混乱を与えてしまう。
def process_data(data):
"""データを加工して返す。"""
print("[process_data] 処理中...")
# 実はここでAPI_KEYを使ったりするが、どんなAPI_KEYかはコードの最後で判明する
return f"{data}-processed-with-{API_KEY}" # ここでAPI_KEYを利用する
def main():
"""メイン関数。"""
data = "some_data"
result = process_data(data)
print(f"[main] 処理結果: {result}")
if __name__ == "__main__":
# ここではただmain()を呼び出すだけ
main()
# -----------------------------------------------
# ↑ここまで読んできても「API_KEY」が何なのか、
# どこで設定しているのか不明。
# -----------------------------------------------
# 実はここでAPI_KEYを設定している
# しかも「開発者アカウントのトークンが必要である」などの重要情報はここに書かれている。
API_KEY = "YOUR_SECRET_API_KEY"
- 良い例
- 冒頭で「重要な前提(API_KEYなど)」を提示して、後続の処理を読むときに「何をやりたいのか」「何が必要なのか」が明確になる。
- 読み手は必要な背景情報を先に把握できるため、コードの目的や構成を理解しやすくなる。
# 1. まず重要な情報を提示する
# - 開発者アカウントのAPIキーを先に明示
API_KEY = "YOUR_SECRET_API_KEY"
def process_data(data):
"""データを加工して返す。"""
print("[process_data] 処理中...")
# どんなAPI_KEYを使うのか、すでに上で定義されているので読み手は混乱しない
return f"{data}-processed-with-{API_KEY}"
def main():
"""メイン関数。"""
data = "some_data"
result = process_data(data)
print(f"[main] 処理結果: {result}")
プログラムを読みやすく保守しやすいものにするためには、読む人がどのようにコードをたどるかを意識して、重要な情報や前提条件を先に提示することが大切です。そうすることで、コードの意味や目的を早い段階で把握でき、スムーズに処理の流れを理解できるようになります。
6章 凝集の順番
プログラムのある機能を変更しようと思ったら、コードのあちこちを探して修正しないといけない。これは、「同じ振る舞いに関するコードがバラバラに書かれている*状態を意味します。
例えば、ユーザ登録の処理が
- バリデーションの関数
- ロギングの関数
- データベース保存の関数
としていくつも散らばっていれば、どこを直せばいいのか見つけるのも一苦労です。
- 読むときにも、「バリデーションはここだけど、DB保存はどこだっけ?」と、コードの全体を行き来しないと全貌が把握しづらくなります。
このように、関連する処理がバラバラに書かれていると、修正漏れや勘違いが起こりやすく、認知的負荷(理解のために頭を使う負荷)も増えてしまいます。
- 悪い例
- ユーザ情報に関する処理があちこちに散らばっている
- ユーザ登録時に行うバリデーション、ロギング、データベース保存などを修正したい場合、コード全体の複数箇所を探して修正しないといけない
- コードを読む側も「どこで何が起きているのか」把握が難しくなる
# -- バリデーション --
def validate_email(email):
# メールアドレスの簡易バリデーション
if "@" not in email:
raise ValueError("Invalid email format")
def validate_password(password):
# パスワードの簡易バリデーション
if len(password) < 6:
raise ValueError("Password must be at least 6 characters")
# -- ユーザ登録処理の一部 --
def log_user_creation(user_id):
"""ユーザ作成時のログをファイルに出力"""
with open("creation.log", "a") as f:
f.write(f"User created: {user_id}\n")
def save_user_to_database(user):
"""ユーザをデータベースに保存する処理"""
# ここは簡易的にprintで代用
print(f"Saving user to DB: {user}")
# -- メインプログラム --
def main():
# ユーザ登録に必要な情報(例)
email = "example@example.com"
password = "123456"
# バリデーションがバラバラに存在
validate_email(email)
validate_password(password)
# ユーザ用オブジェクトの一例
user = {
"id": 1,
"email": email,
"password": password
}
# ここでログ出力をして…
log_user_creation(user["id"])
# その後にDB保存をして…
save_user_to_database(user)
# ユーザ登録のフローが分散しすぎていて、
# いざ変更があった場合に複数の関数・ファイルを追う必要がある
- 良い例
- ユーザ登録に関する処理を1つのクラス(またはモジュール)にまとめ、まとめて変更できるようにする
- バリデーション、ログ出力、DB保存などが 一つの流れ(凝集) として見通しが良くなり、修正箇所が特定しやすい
class UserRegistrationService:
"""
ユーザ登録に必要な一連の処理(バリデーション、ログ出力、DB保存など)を
ひとつのクラスとしてまとめることで、凝集度を高める。
"""
def __init__(self, logger, db_client):
# ロガーやDBクライアントを注入して使用する例
self.logger = logger
self.db_client = db_client
def register_user(self, email, password):
# 1. バリデーション
self._validate_email(email)
self._validate_password(password)
# 2. DBへ保存
user_id = self.db_client.save_user({"email": email, "password": password})
# 3. ログ出力
self.logger.log(f"User created: {user_id}")
return user_id
def _validate_email(self, email):
if "@" not in email:
raise ValueError("Invalid email format")
def _validate_password(self, password):
if len(password) < 6:
raise ValueError("Password must be at least 6 characters")
# -- ロガーとDBクライアントの例 --
class SimpleLogger:
def log(self, message):
with open("creation.log", "a") as f:
f.write(message + "\n")
class SimpleDatabaseClient:
"""簡易的なDBクライアントのイメージ"""
def save_user(self, user_data):
# 実際にはINSERTなどの処理が入るが、ここでは簡易化
print(f"Saving user to DB: {user_data}")
# ユーザIDを発行した体で返す
return 1
# -- メインプログラム --
def main():
logger = SimpleLogger()
db_client = SimpleDatabaseClient()
registration_service = UserRegistrationService(logger, db_client)
email = "example@example.com"
password = "123456"
user_id = registration_service.register_user(email, password)
print(f"User ID is {user_id}")
- 凝集度が低いと、コードのあちこちに変更が必要になり、修正のたびに迷子になりがちです。
- 凝集度が高いと、関連する処理がまとまっているため、変更が生じても「ここを変えれば大丈夫」という安心感があります。
- コードの可読性と保守性を高めるには、同じ責務・機能を担う処理をなるべく近い場所(クラスやモジュール)に配置することが大切です。
7章 変数宣言と初期化を一緒の場所に移動する
変数を使うタイミングよりもかなり前に「宣言だけ」してしまうことがあります。そして、しばらく別の処理が続いた後に「初期化」する流れをとると、読んでいる人が「この変数はいつ使うの?」「何のため?」と混乱しやすくなります。初期化にたどり着く頃には、変数の名前が示す意図を忘れてしまうことも珍しくありません。
- 悪い例
- 変数 result_list や user_info の宣言がメソッドやファイルの冒頭にあり、初期化するのはずっと後の方。
- 読み手は「最終的にこの変数は何のために使うのか?」をコードの後半まで見ないと分からない。
def process_user_data():
# 変数を先に用意するだけして、何のために使うかも書いていない
result_list = []
user_info = None
# ここで別の処理がいろいろ行われる
# ... (数十行のコードが入るイメージ)
print("[process_user_data] Doing some other stuff ...")
# ようやく user_info を初期化
user_info = {
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
# さらに別の処理(数行~数十行)
# ...
# 最後になって result_list を初期化し始める
result_list.append("Processed user data")
result_list.append(f"User name: {user_info['name']}")
return result_list
- 良い例
- 変数を使う直前に宣言と初期化をまとめて行う。
- 名前が示す役割(user_info や result_list)と、中身(初期値)が一緒に書かれているため、読み手はコードの流れを追いやすい。
def process_user_data():
# 変数の宣言と初期化を使うタイミングでまとめて行う
user_info = {
"name": "Bob",
"age": 25,
"email": "bob@example.com"
}
# user_info が何のためのデータかすぐわかるので読み手は安心して次に進める
# ここで何かデータ処理をしたり、別の関数に渡したりする
processed_name = user_info["name"].upper()
print(f"Processed name: {processed_name}")
# result_list も、何を入れるか明確になってから初期化する
result_list = []
result_list.append("Processed user data")
result_list.append(f"User name: {processed_name}")
result_list.append(f"User email: {user_info['email']}")
return result_list
変数の名前は「何を表すのか」を読み手に伝える重要な手がかりです。一緒に初期化された値を見ると、変数が具体的にどう使われるか、よりはっきりイメージできます。変数宣言と初期化を近い位置で行うことで、コードの可読性を高め、保守もしやすくなります。
8章 説明変数
プログラムを書いていると、最初は小さかった式(計算)が、後から機能や要件が増えてだんだん複雑になってしまうことがあります。すると、その式を見ただけでは「何をしているのか」すぐに分からなくなり、可読性が下がってしまうのです。ややこしくなった式を部分的に切り出して、「意図」を表した変数名をつける方法があります。これを説明変数と呼びます。
- 悪い例
- 「ここは割引の計算? それとも税金?」というのが直感的に分かりにくい。
- 後から「割引を追加したい」「税率を変えたい」ときに、この長い式のどこを修正すればいいか探しにくい。
def calculate_final_price(item_price, discount_rate, shipping_cost, quantity, tax_rate):
# 一行にいろいろ詰め込みすぎた悪い例
final_price = (item_price * quantity * (1 - discount_rate)
+ (shipping_cost * quantity if quantity > 3 else shipping_cost)
+ (item_price * quantity * tax_rate / 100))
return final_price
- 良い例
- 変数名が「どんな計算をしているか」説明してくれる。
- どこをいじれば割引率を変えられるのか、送料を変更できるのかが、ひと目で分かる。
- 将来、計算がさらに複雑になっても「tax の計算だけ変えよう」とポイントを絞って修正しやすい。
def calculate_final_price(item_price, discount_rate, shipping_cost, quantity, tax_rate):
# 1. ベース価格(商品単価 × 個数)
base_price = item_price * quantity
# 2. 割引額(ベース価格 × 割引率)
discount_amount = base_price * discount_rate
# 3. 送料(個数が多いと加算するルール)
shipping_fee = shipping_cost * quantity if quantity > 3 else shipping_cost
# 4. 税金(割引後の価格に税率をかける)
tax = (base_price - discount_amount) * tax_rate / 100
# 5. 全部を足し合わせる
final_price = (base_price - discount_amount) + shipping_fee + tax
return final_price
式が成長して大きくなりそうなときは、「一部を説明変数に切り出す」 ことを意識すると、コードの読みやすさ・修正しやすさが大幅に向上します。読みやすいコードは、自分はもちろん、他の人が理解するときにも非常に役立つものです。
変数名を付けるだけで「ここは何をやっているのか?」という意図が伝わるので、複雑になった計算式ほど、早めに小分けして管理しましょう。
9章 説明定数
プログラムの中で、ある数字や文字列がよく出てきたとします。たとえば、ユーザが見つからなかったときに「404」と返すコード。
「404って何だっけ? 何の数字なの?」と、コードを読む人は疑問に思います。HTTPステータスを知っている人にはわかるかもしれませんが、初めて見る人やたまたまその知識を忘れた人にとっては、不親切です。
さらに、「404」自体を将来「410」や「-1」などに変えたいとき、あちこちの「404」を全部探して書き換える必要があり、保守作業も面倒になります。
- 悪い例
- 「404」という数字だと意味が伝わりにくい
- 変更があった場合、コード内のすべての「404」を探して書き換えなければならない。
def get_user_data(user_id):
# 見つからないとき 404 を返しているが、これだけだとコードを読む人に「?」が生じる
if user_id not in database_mock:
return 404
return database_mock[user_id]
def handle_response(response_data):
# ここでも「404」と比較しているだけで、なぜ404なのかが一目で分からない
if response_data == 404:
print("User not found")
else:
print("User data:", response_data)
- 良い例
- HTTP_NOT_FOUND という定数名を使うだけで「404は『見つからない』というステータスコードなんだ」と即座にわかる
- もし将来「410」に変えたいなら、HTTP_NOT_FOUND = 410 と定義部分だけ変更すればOK
HTTP_NOT_FOUND = 404 # 意味のわかる名前を定義
def get_user_data(user_id):
# 見つからないときに HTTP_NOT_FOUND を返す
if user_id not in database_mock:
return HTTP_NOT_FOUND
return database_mock[user_id]
def handle_response(response_data):
# コードを見るだけで「ユーザが見つからない」という意図が分かる
if response_data == HTTP_NOT_FOUND:
print("User not found")
else:
print("User data:", response_data)
コードを読む人の目線で、数字(または文字列)の意味がすぐ伝わらない場合は、説明定数を導入する。一箇所で定数を定義するため、後から修正・変更が発生しても対応が簡単。プログラムの可読性がぐっと上がり、「この値はいったい何?」という疑問を減らせる。
「404」に限らず、よく使う定数(たとえば 500 エラーや 200 成功など)にも同じ考え方を適用すると、コード全体のわかりやすさがアップします。
10章 明示的なパラメーター
とある関数の処理でグローバル変数やクラスのメンバー変数などを暗黙的に参照していたりすると、その関数の内容も全体の影響範囲も分かりづらくなります。
- いちいちコード全体を見渡して、同名の変数を探さないといけない
- どこでどう変更されるのかがハッキリせず、バグやトラブルが起きやすい
- テストコードを書くときや、別のデータを使いたいときに柔軟な対応がしにくい
こうした問題を避けるために、「その関数が必要とするデータは引数として渡す」というやり方がよく使われます。これを明示的なパラメータと呼びます。
- 悪い例
- グローバル変数 INVENTORY を update_inventory() が直接操作している。
- 引数としては「アイテム名」と「数量」しか書かれていないが、実は「どの辞書(在庫情報)を操作しているか」は暗黙的に決まっている。
- コードを読む人は、INVENTORY がどこで定義されているか探さなければいけないし、テスト時にも INVENTORY の初期化やリセットを考えなければならない。
- 別の場所で INVENTORY を意図せずに書き換えてしまうと、動作が変わってしまう可能性がある
INVENTORY = {
"apple": 10,
"banana": 5,
"orange": 8
}
def update_inventory(item, quantity):
"""
INVENTORY(グローバル変数)を直接更新。
関数の引数だけ見ても、どの在庫情報を操作するか分からない。
"""
if item in INVENTORY:
INVENTORY[item] += quantity
else:
INVENTORY[item] = quantity
def main():
print("[Before Update]", INVENTORY)
# 在庫更新時、「何をどこに更新しているか」は関数だけでは把握しづらい。
update_inventory("banana", 3)
update_inventory("mango", 7)
print("[After Update]", INVENTORY)
if __name__ == "__main__":
main()
- 良い例
- update_inventory(inventory_data, item, quantity) のように、操作対象の在庫データを明示的な引数として受け取る
- コードを見ただけで、「ああ、ここでは inventory という辞書の中にあるアイテムを更新するんだな」とひと目でわかる
- 将来的に「支店Aの在庫」「支店Bの在庫」のように複数の在庫を扱う際にも、関数を使い回せる
- グローバル変数に依存しないので、コードがスリムで保守しやすい
def update_inventory(inventory_data, item, quantity):
"""
明示的に 'inventory_data' を引数に取る。
どの在庫を更新するのか、呼び出し元からハッキリわかる。
"""
if item in inventory_data:
inventory_data[item] += quantity
else:
inventory_data[item] = quantity
def main():
# 操作したい在庫データをここで定義
inventory = {
"apple": 10,
"banana": 5,
"orange": 8
}
print("[Before Update]", inventory)
# 関数に在庫データを渡すことで、何を更新するか明確
update_inventory(inventory, "banana", 3)
update_inventory(inventory, "mango", 7)
print("[After Update]", inventory)
if __name__ == "__main__":
main()
暗黙的に参照しているデータがあると、コードの可読性・保守性は下がり、トラブルを起こしやすくなります。関数を分割し、必要なデータを明示的にパラメータとして渡すだけで、「どのデータをいつ操作しているか」を把握しやすくなります。
11章 ステートメントを小分けにする
書籍の中で著者はこの整頓の事を「整頓大賞シンプル部門」と呼んでいます。
プログラムを書くときに、いくつもの処理が連続して書かれていると、「どこまでが何の処理?」と読み手が混乱しやすくなります。
例えば、
- データのフィルタリング
- 計算
- 保存処理
- ログ出力
これらがずらっと一気に書かれていると、頭に入りづらいので、論理的なまとまりごとに空行やコメントを入れて区切ると、「ここではフィルタリングをしている」「ここからは計算をしている」と明確になります。
def process_and_save_data(input_data):
"""
入力データをフィルタリングして、集計して、ファイルに保存する流れを
ステートメント(処理)ごとに分けて書いた例。
"""
# --- Step 1: フィルタリング ---
filtered_data = []
for item in input_data:
if isinstance(item, int) and item > 0:
filtered_data.append(item)
# (空行で区切って、次のステップを明示)
# --- Step 2: 集計 ---
total = sum(filtered_data)
count = len(filtered_data)
average = total / count if count else 0
# (さらに空行で区切り)
# --- Step 3: 保存 ---
output_filename = "processed_data.txt"
with open(output_filename, "w") as f:
f.write(f"Filtered Data: {filtered_data}\n")
f.write(f"Total: {total}\n")
f.write(f"Count: {count}\n")
f.write(f"Average: {average}\n")
# --- Step 4: 結果を表示 ---
print("[INFO] Data processed and saved successfully.")
print(f"[INFO] Count: {count}, Total: {total}, Average: {average}\n")
大きなコードや複数の処理が連続する部分は、ステートメントを小分けにして空行やコメントを入れると読み手にとって理解しやすい。そして、ステートメントを小分けにすることで、他の整頓がしやすくなる。
以前カンファレンスで、テストコードを書く時にも、「準備、実行、検証と分けておくと良い。」と t_wada さんも仰っていました。
12章 ヘルパーを抽出する
1つの関数の中に、目的がはっきりしているコードブロックがある場合、そこを抽出しましょう。たとえば「データを解析する部分」と「DBに保存する部分」が同じ関数に混在していると、関数が長くなりがちです。そして、後から別の機能を追加しようとすると、どこに手を入れればいいか見つけにくなります。
こういうときは、そのコードブロックをヘルパー関数に切り出し、目的に合った名前をつけましょう。そうすることにより、読みやすく・修正しやすいコードになります。
- ヘルパーを抽出していないコード
- 1つの関数に データ解析 と 保存 の両方が詰まっている
- 関数が長くなり、どこに何の処理が書いてあるのかぱっと見でわかりにくい。
def process_user_data(raw_input):
"""
- 「データ解析」と「DB保存」が1つの関数にまとめられていて分かりにくい。
"""
# データ解析(パース)
print("[process_user_data] Parsing input data...")
user_data = []
for line in raw_input:
parts = line.split(",")
if len(parts) == 3:
username = parts[0].strip()
age = parts[1].strip()
email = parts[2].strip()
if username and age.isdigit() and "@" in email:
user_data.append({
"username": username,
"age": int(age),
"email": email
})
# DBへ保存
print("[process_user_data] Storing data to database...")
with open("user_data_db.txt", "a") as f:
for record in user_data:
f.write(f"{record['username']},{record['age']},{record['email']}\n")
print("[process_user_data] Done.")
- ヘルパーを抽出したコード
- parse_user_data を見れば「データをパースする」とわかるし、store_user_data_to_db を見れば「DBに保存する」とわかります
- 「パースだけロジック変更したい」「保存先を変えたい」などのとき、対応するヘルパー関数のみを修正すればいいので保守性があがる
def parse_user_data(raw_input):
"""
ヘルパー関数(1):
入力文字列をパース(解析)して、ユーザ情報リストを返す。
"""
print("[parse_user_data] Parsing input data...")
parsed_data = []
for line in raw_input:
parts = line.split(",")
if len(parts) == 3:
username = parts[0].strip()
age = parts[1].strip()
email = parts[2].strip()
if username and age.isdigit() and "@" in email:
parsed_data.append({
"username": username,
"age": int(age),
"email": email
})
return parsed_data
def store_user_data_to_db(user_data):
"""
ヘルパー関数(2):
パース済みのユーザ情報をDB(ここではファイル)に保存。
"""
print("[store_user_data_to_db] Storing data to database...")
with open("user_data_db.txt", "a") as f:
for record in user_data:
f.write(f"{record['username']},{record['age']},{record['email']}\n")
def process_user_data(raw_input):
"""
- 「全体の流れ」はここで書き、
実際の解析処理と保存処理はヘルパー関数に任せる。
"""
# 1. データをパース
user_data = parse_user_data(raw_input)
# 2. DBへ保存
store_user_data_to_db(user_data)
print("[process_user_data] Done.")
- 関数の中で、単一の目的を持つブロックを見つけたら、それだけをヘルパー関数に切り出す
- 切り出した関数に意図がわかる名前を付けると、コードを読む人がすぐに理解しやすい
- 処理が増えたときや修正が入ったときも、どこを直せばいいか明確になるため、コード全体の保守性が上がります
13章 ひとかたまり
プログラムを書くときによく言われるのは「関数を小さくまとめる」「責務を分割する」です。もちろんこれは大事な考え方ですが、分割しすぎると、逆に読みにくくなってしまうことがあります。
たとえば、データを読み込んで、パースして、バリデーションして…といった流れを1ステップずつ小さな関数にしていると、どこで何をやっているかを追うのにソースコードを行ったり来たりしなければならなくなります。こうなると、コードを理解するコストが高まってしまいがちです。
- ひとかたまりになっていないコード
def load_data_from_file(file_path):
with open(file_path, "r") as f:
return f.readlines()
def parse_line(line):
parts = line.strip().split(",")
if len(parts) == 3:
return parts[0], parts[1], parts[2]
return None, None, None
def validate_name(name):
return bool(name)
def validate_age(age):
return age.isdigit()
def validate_email(email):
return "@" in email
def assemble_user_record(name, age, email):
return {
"name": name.strip(),
"age": int(age),
"email": email.strip()
}
def process_line(line):
name, age, email = parse_line(line)
if validate_name(name) and validate_age(age) and validate_email(email):
return assemble_user_record(name, age, email)
return None
def process_file(file_path):
lines = load_data_from_file(file_path)
results = []
for line in lines:
record = process_line(line)
if record:
results.append(record)
return results
def main():
file_path = "user_data.csv"
records = process_file(file_path)
print("Processed Records:", records)
if __name__ == "__main__":
main()
- ひとかたまりになっているコード
def process_file_in_one_block(file_path):
"""
- 「ファイルを読み、行ごとに解析してレコードを組み立てる」という
一連の処理を一つのブロックとしてまとめたバージョン。
- 細かいステップをあまりに分割しすぎず、流れが見やすい。
"""
records = []
with open(file_path, "r") as f:
for line in f:
line = line.strip()
parts = line.split(",")
if len(parts) != 3:
continue # 行の形式が正しくない場合はスキップ
name, age, email = parts[0], parts[1], parts[2]
# バリデーション(まとめてチェック)
if not name or not age.isdigit() or "@" not in email:
continue
# データ作成
record = {
"name": name.strip(),
"age": int(age),
"email": email.strip(),
}
records.append(record)
return records
def main():
file_path = "user_data.csv"
records = process_file_in_one_block(file_path)
print("Processed Records:", records)
if __name__ == "__main__":
main()
コードを細分化すること自体は悪くありませんが、過剰に関数を分けすぎると、かえって読みにくくなる場合があります。「この一連の処理はまとめて読んだほうが分かりやすい」と判断できるなら、ひとつの大きめのブロックとしてまとめるのも選択肢のひとつです。
大きすぎる関数は避けたいですが、小さすぎる関数の連打も理解コストを高める場合がある、という点を押さえておきましょう。
14章 説明コメント
コードを読んでいると「この処理には実はこういう理由があるんだよな」という場面に出くわすことがあります。たとえば、ビジネス上の理由や、過去のバグ対応の経緯など、コードだけを見てもわかりにくい背景がある場合です。
こうした理由や意図をコメントに書いておかないと、後からコードを読む人(自分を含め)が悩んでしまうことになります。そのような場合にコードでは表現できない情報は、説明コメントとして書き残すことがとても有効です。
- 説明コメントの無いコード
- 「VIPなら3割引」「土日は追加割引」のように、コード上から分かる“何をしているか”は分かるものの、「なぜそうしているのか」が一切書かれていない
def calculate_discounted_price(price, user_type, day_of_week):
"""
割引価格を計算する関数だが、なぜこの計算をしているかの説明がない。
"""
# VIP
if user_type == "vip":
price *= 0.7
# 土日
if day_of_week in ["Saturday", "Sunday"]:
price *= 0.9
return price
- 説明コメントのあるコード
- 「なぜVIPは3割引なのか?」「なぜ土日は追加割引?」など、コードだけでは見えない背景がコメントで伝わります
- 別の開発者が見たとき、「なるほどそういう理由か」と理解して、勝手に削除したり変更したりしないように気をつけることができます
- もし将来「VIP割引を廃止しよう」という話が出たときにも、コメントを読めば“そもそもなんで導入されていたか”を踏まえたうえで検討できます
def calculate_discounted_price(price, user_type, day_of_week):
"""
割引価格を計算する関数。
コメントで「なぜその計算なのか」背景や理由を書いておく。
"""
# --- VIPユーザは常に3割引 ---
# 当社のVIP会員には特別優遇する方針のため、通常価格の70%の値段にする。
if user_type == "vip":
price *= 0.7
# --- 土日は追加割引 ---
# 週末に買い物をするユーザを増やすキャンペーンとして、10%オフを実施。
if day_of_week in ["Saturday", "Sunday"]:
price *= 0.9
return price
15章 冗長なコメントを削除する
コードの目的はコンピューターに何をさせたいのかを説明すること、それと同時に、人間にも読みやすく伝わるように構造を整理する必要があります。コメントは「コードからは読み取りにくい背景・意図」を補足するのが本来の役割であり、コードと同じ内容を繰り返すだけのコメントは冗長となり得ます。
- 冗長なコメント
- 処理内容がわかりきっているのでコメントに意味はない
def get_price():
# 料金を返す
return price
また、第1章のガード節の適用により今までは冗長ではなかったコメントが、冗長なコメントになることがあります。
- ガード節を適用する前のコード
- このときのelse節のコメントは冗長なコメントではありません(特にif節とelse節が大きく離れている場合、コードの理解を助ける有効なコメントとなります)
def process_user_data(user_type):
if user_type == "vip":
# 何らかの処理のかたまり...
return True
else:
# vip以外は何もせずFalseを返す
return False
- ガード節を適用したコード
- ガード節の適用で順番を入れ替えたことにより「vip以外は何もせずFalseを返す」というコメントは冗長なコメントになります
def process_user_data(user_type):
if user_type != "vip":
# vip以外は何もせずFalseを返す
return False
# 何らかの処理のかたまり...
return True
まとめ
本記事では、「Tidy First?」の第Ⅰ部にかかれている整頓のテクニックをPythonコードを踏まえて説明しました。この記事はここまでですが、「Tidy First?」第Ⅱ部 管理術ではこれらの整頓テクニックを使うタイミングや対象範囲など、第Ⅲ部 理論ではなぜ整頓を行うかについての論理的な事が書かれていて、話が進むにつれてミクロな視点からマクロな視点に展開していく内容になっています。個人的には第Ⅱ部、第Ⅲ部の方が興味深い内容になっているので、この記事を読んで気になった方は、ぜひ書籍を読んでみて下さい。この記事を読んで頂きありがとうございました。
おまけ
第Ⅲ部 26章 オプションのじゃがいもの話が個人的によくわからなかったので、(※洋書の翻訳本あるあるで、例え話がピンとこない)LLMに手伝ってもらって理解した内容を書いておきたいと思います。
コールオプション
- 特定の期日(満期日)までに「原資産をあらかじめ決められた価格(行使価格)で買う権利」です
- コールオプションを買う側は、権利を行使するかしないかを選択できます
- 原資産価格が行使価格より高くなれば、権利を行使して安く買うことができるので利益を得やすくなります
- 原資産価格が行使価格を下回った場合は、権利を行使するメリットがなく、オプションを放棄すれば損失はオプション購入時のプレミアム(オプション代金)のみに限定されます
- コールオプションを売る(ショート・コール)側は、買い手に「買う権利」を提供する代わりにプレミアムを受け取ります。しかし、原資産価格が大きく上昇した場合は、高値で売らなければならない可能性があり、理論上は損失が無限大となるリスクもあります
コールオプションを「商品を予約する権利」としてイメージするとわかりやすいです。たとえば、新作ゲーム機が近いうちに値上がりしそうだと噂があるとします。
-
予約金(= オプションのプレミアム)を支払う
- お店に「今の価格でゲーム機を買う権利を押さえたい」と伝え、少額の“予約金”を払っておきます
- これがコールオプションの「プレミアム(オプション代金)」にあたるイメージです
-
値段が上がったら権利を行使
- もしその後、本当にゲーム機の値段が大きく上がったとしたら、あなたは“予約したときの価格”で買うことができます
- つまり、世の中では値上がりしているのに、自分は安い価格で買えるのでおトクになります
-
値段が下がったり、欲しくなくなったら
- ゲーム機の価格が逆に下がったり、買う気がなくなった場合、権利は放棄できます
- その場合は予約金(= プレミアム)は戻ってきませんが、それ以上の損はありません
ポイント
- 価格が上がれば、安く買えるので利益が出る
- 価格が下がれば、市場で買った方が安いので、オプションは放棄して損失をプレミアムに限定できる
- 欲しくなくなった場合も、強制的に買う必要はない(損失はプレミアムのみ)
このように、コールオプションの買い手のリスクはプレミアムに限定されており、利益の可能性は市場価格の上昇次第というのが特徴です。
状況 | 価格の変化 | コールオプションの選択 | 結果 |
---|---|---|---|
値段が上がった | 市場価格 > 行使価格 | 権利を行使(安く買える) | 利益が出る(市場価格との差額 - プレミアム) |
値段が下がった | 市場価格 < 行使価格 | 権利を放棄(買わない) | 損失はプレミアムのみ |
欲しくなくなった | 価格変動に関係なし | 権利を放棄(買わない) | 損失はプレミアムのみ |