40. クリーンで読みやすいコードを書く
この本を通して、Python の構文、データ構造、制御フロー、関数(function)、クラス(class)、そして他にも多くのプログラミング概念を学んできました。これで、動くプログラムを書けるようになりました。しかし、動くコードと保守可能(maintainable)なコード——つまり、数か月後や数年後に自分や他人が理解し、変更し、デバッグ(debug)できるコード——の間には重要な違いがあります。
この章では、クリーンで読みやすいコードを書くことに焦点を当てます。Python のコードをプロフェッショナルで保守しやすくするための慣習と実践を学びます。これらは単なる恣意的なルールではありません。コラボレーションを容易にし、バグを減らし、後から自分のコードに戻ったときに理解しやすくする、実戦で検証されたガイドラインです。
40.1) スタイルが重要な理由: コードを読むこと vs. 書くこと
40.1.1) コードは書かれるより読まれることのほうが多い
コードを書くときは、数分や数時間かけてそれを作ります。しかし、そのコードは何度も読まれます。デバッグするとき、新機能を追加するとき、他の開発者がそれを扱うとき、そして数か月後に「これが何をするのか」を思い出そうとするときです。
動作はするものの、スタイルが悪いこのコードを考えてみましょう。
# 警告: 悪いスタイル - デモ用のみ
def c(l):
t=0
for i in l:
t=t+i
return t/len(l)
data=[85,92,78,90,88]
result=c(data)
print(result) # Output: 86.6このコードは完璧に動きます。数値のリストの平均を計算します。しかし、何をしているのかを理解するには注意深い分析が必要です。では、このバージョンと比べてみましょう。
def calculate_average(numbers):
"""Calculate the arithmetic mean of a list of numbers."""
total = 0
for number in numbers:
total = total + number
return total / len(numbers)
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score) # Output: 86.62つ目のバージョンが優れているのはなぜでしょうか?
- 関数名(
calculate_average)が目的を明確に示している - 変数名(
numbers,total,test_scores)が説明的である - docstring が関数の内容を説明している
- 適切なスペースにより構造が明確である
- 誰でも、このコードをじっくり読み込まなくても理解できる
どちらも同一の結果を出しますが、2つ目のバージョンはすぐに理解できます。
重要な洞察: コードは一度書きますが、何十回、何百回も読みます。命名や整形に数秒余分にかけることで、後の何時間もの混乱を防げます。
40.1.2) 可読性はバグを減らす
明確なコードは、各部分が何をしているかを素早く理解できるため、デバッグしやすくなります。変数名が説明的で構造がクリーンなら、ロジックエラーを見つけやすくなります。
# デバッグしにくい - これらの変数は何を表している?
# 警告: 悪いスタイル - デモ用のみ
def process(x, y):
if x > y:
return x * (1 - y)
return x
result = process(100, 0.1)# デバッグしやすい - 何が起きているかが明確
def apply_discount(price, discount_rate):
"""Calculate price after applying discount rate (0.0 to 1.0)."""
discount_amount = price * discount_rate
final_price = price - discount_amount
return final_price
original_price = 100
discount = 0.1 # 10% 割引
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.02つ目のバージョンでは、ロジックがすぐにわかります。「割引額を計算してから、価格から引いている」ということが見て取れます。1つ目のバージョンでは、x と y が何を表しているかを頭の中で追いかけ、x * (1 - y) が何を意味するのかを理解しなければなりません。
40.1.3) 一貫性がコラボレーションを可能にする
チームの全員が同じスタイル規約に従うと、コードは予測可能になります。異なる整形スタイルを解読するための精神的な負担を費やす必要がなく、ロジックの理解に集中できます。
Python には PEP 8 (Python Enhancement Proposal 8) という公式のスタイルガイドがあります。PEP 8 は次の慣習を定義しています:
- 変数、関数、クラスの命名方法
- コードの整形方法(スペース、行の長さ、インデント)
- コメントや docstring を使うタイミング
- import の整理方法
PEP 8 に従うと、あなたのコードは他の Python プログラマにとって見慣れたものになり、コラボレーションがスムーズになります。次のセクションで、重要な PEP 8 のガイドラインを扱います。
40.2) 命名規約: 変数、関数、クラス(PEP 8)
40.2.1) 一般的な命名の原則
良い名前は説明的で曖昧さがないものです。実装を読まなくても、それが何を表し、何をするのかがわかるべきです。
基本原則:
- 省略形ではなく完全な単語を使う(ただし
id,url,htmlのような非常に一般的なものは除く) - 具体的にする:
countよりuser_count、calculateよりcalculate_total_priceのほうが良い - 非常に短いループや数学的な式以外では、1文字の名前を避ける
- 名前に型情報を含めない(Python は動的型付けです)
# 悪い名前 - 何を表しているのか不明
# 警告: 悪いスタイル - デモ用のみ
# 'n' とは何? 数字? 名前? ノード?
# 'd' とは何? 日付? 距離? 期間?
# 'l' とは何? 数字の 1 に見える!
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
# 良い名前 - 明確で説明的
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2例外: 短いループ変数
# 許容される: とても短く、文脈が明確
for i in range(10):
print(i)
for x, y in coordinates:
distance = (x**2 + y**2) ** 0.5
# ただし明確さのためには説明的な名前を優先
for student_index in range(len(students)):
print(students[student_index])
for point_x, point_y in coordinates:
distance = (point_x**2 + point_y**2) ** 0.540.2.2) 変数名と関数名: snake_case
Python では、変数と関数に snake_case を使います。すべて小文字で、単語はアンダースコアで区切ります。
# Variables
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
# Functions
def calculate_tax(amount, rate):
"""Calculate tax on a given amount."""
return amount * rate
def send_email_notification(recipient, message):
"""Send an email to the specified recipient."""
print(f"Sending to {recipient}: {message}")
# Using the functions
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")なぜ snake_case なのですか? 非常に読みやすいからです。アンダースコアが明確な単語の境界を作り、名前をざっと見ても理解しやすくします。calculatetotalprice (読みにくい) と calculate_total_price (一目で明確) を比べてみてください。
40.2.3) 定数名: UPPER_SNAKE_CASE
定数——プログラム実行中に変わるべきではない値——には UPPER_SNAKE_CASE を使います。すべて大文字で、アンダースコアで区切ります。
# Constants at module level
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
def validate_password_length(password):
"""Check if password meets minimum length requirement."""
MIN_PASSWORD_LENGTH = 8 # 関数内の定数
return len(password) >= MIN_PASSWORD_LENGTH
# Using constants
if login_attempts > MAX_LOGIN_ATTEMPTS:
print("Account locked")重要: Python には組み込みの定数構文がありません。いくつかの言語(JavaScript の const や Java の final など)と違い、Python には「変数が変更できない」ことを宣言する方法がありません。
その代わりに、Python プログラマは意図を示すために 命名規約 を使います:
UPPER_SNAKE_CASEは「これは定数にする意図です—変更しないでください」という意味- これは言語機能ではなく、プログラマ同士のコミュニケーション手段です
# Python には定数構文がない - これはただの通常の変数
MAX_LOGIN_ATTEMPTS = 3
# Python は変更を止めない
MAX_LOGIN_ATTEMPTS = 5 # ❌ 技術的には動くが、規約違反
# 命名規約は INTENT(意図) の合図:
# 「UPPERCASE で命名したのは、変更してほしくないことを示すため」ベストプラクティス: 実行中に本当に変わる必要がある値は、定数として命名しないでください:
# この値は変わる - 小文字を使う
max_login_attempts = 3
max_login_attempts = 5 # ✅ OK - 名前が変更可能であることを示す
# この値は絶対に変わらないはず - UPPERCASE を使う
MAX_LOGIN_ATTEMPTS = 3
# 後でコード中で再代入しないこの規約により、あなたの意図が伝わり、バグを避けやすくなります。MAX_LOGIN_ATTEMPTS を見れば、変更しないべきだとわかります。
40.2.4) クラス名: PascalCase
クラス名には PascalCase (CapWords とも呼ばれます)を使います。各単語の先頭を大文字にし、アンダースコアは使いません。
# Class definitions
class Student:
"""Represent a student with name and grades."""
def __init__(self, name):
self.name = name
self.grades = []
class ShoppingCart:
"""Manage items in a shopping cart."""
def __init__(self):
self.items = []
def add_item(self, item):
"""Add an item to the cart."""
self.items.append(item)
class DatabaseConnection:
"""Handle database connection and queries."""
def __init__(self, url):
self.url = url
# Creating instances (note: instances use snake_case variable names)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")なぜクラスには PascalCase なのですか? 関数や変数と見た目で区別できるからです。Student() を見ればクラスのインスタンス生成だとすぐにわかります。calculate_average() を見れば関数呼び出しだとわかります。
40.2.5) プライベート/内部の名前: 先頭のアンダースコア
単一のアンダースコアで始まる名前(_name)は内部用を示します。つまり、外部コードではなく、モジュールやクラスの内部で使うことを意図しています。
Python には「private」としてメソッドや属性をマークする構文がありません(Java や C++ の private とは異なります)。その代わりに Python では、先頭アンダースコア(_name)という 命名規約 を使って意図を伝えます。
_name の意味:
- 「これは内部用のみです」
- 「これはこのクラス/モジュール内で使うために作りました。外部コードのためではありません」
- 「将来のバージョンでいつでも変更される可能性があるので、依存しないでください」
class BankAccount:
"""Represent a bank account with balance tracking."""
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self._balance = initial_balance # 内部属性
def deposit(self, amount):
"""Add money to the account."""
if self._validate_amount(amount): # 内部メソッド
self._balance += amount
def _validate_amount(self, amount):
"""Internal helper to validate transaction amounts."""
return amount > 0
def get_balance(self):
"""Return the current balance."""
return self._balance
# Using the class
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# 技術的には動くが、規約違反
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
# 技術的には動くが、規約違反
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)重要なポイント: Python は _balance へのアクセスや _validate_amount() の呼び出しを防げません。アンダースコアはセキュリティ機能ではなく、プログラマ間の合図です。
この規約が存在する理由
Python はプライバシーを強制できないため、アンダースコアはクラス作者が意図を伝える方法です。
アンダースコアが示すこと:
- 「これは内部実装であり、将来のバージョンで変更される可能性があります」
- 「公開メソッドを使ってください。公開メソッドは安定していることを保証します」
- 「内部詳細に依存すると、ライブラリを更新したときにあなたのコードが壊れるかもしれません」
この規約は 契約(contract) を作ります。クラス作者は内部実装(_ 付きのもの)を自由に変更できますが、公開インターフェースは安定して保つ必要があります。これにより、ユーザーコードを壊さずにライブラリを進化させられます。
40.2.6) 特別な名前: ダブルアンダースコア
先頭と末尾にダブルアンダースコアを持つ名前(__name__)は、Python が定義する特殊メソッドまたはマジックメソッドです。このパターンで独自の名前を作らないでください。Python のために予約されています。
class Point:
"""Represent a 2D point."""
def __init__(self, x, y): # 特殊メソッド: 初期化
self.x = x
self.y = y
def __str__(self): # 特殊メソッド: 文字列表現
return f"Point({self.x}, {self.y})"
def __add__(self, other): # 特殊メソッド: 加算演算子
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Output: Point(1, 2)
print(p1 + p2) # Output: Point(4, 6)第31章で学んだように、これらの特殊メソッドは演算子オーバーロードと Python の組み込み関数との統合を可能にします。
40.2.7) 命名のまとめ表
| 種類 | 規約 | 例 |
|---|---|---|
| 変数 | snake_case | user_name, total_count |
| 関数 | snake_case | calculate_tax(), send_email() |
| 定数 | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| クラス | PascalCase | Student, ShoppingCart |
| 内部/プライベート | _leading_underscore | _balance, _validate() |
| 特殊/マジック | double_underscore | __init__, __str__ |
40.3) コードレイアウト: インデント、スペース、空行
40.3.1) インデント: 4スペース
Python はインデントでコードブロックを定義します。インデントレベルごとに必ず4スペースを使ってください。タブは使わず、タブとスペースを混在させないでください。
def calculate_grade(score):
"""Determine letter grade from numeric score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
# ネストしたインデント: レベルごとに4スペース
def process_students(students):
"""Process a list of student records."""
for student in students:
if student["active"]:
grade = calculate_grade(student["score"])
print(f"{student['name']}: {grade}")
students = [
{"name": "Alice", "score": 92, "active": True},
{"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: Cなぜ4スペースなのですか? Python コミュニティの標準だからです。あなたが出会うほとんどの Python コードは4スペースなので、この規約に従うとエコシステムと一貫したコードになります。
エディタの設定: 現代のコードエディタは、Tab を押したときに4スペースを挿入するよう設定できます。これにより Tab キーの利便性を得つつ、4スペース標準を保てます。
40.3.2) 最大行長: 79文字
PEP 8 は、行を 79文字に制限することを推奨しています(docstring とコメントは最大 99 文字まで)。制約が強いように見えますが、実用的な利点があります:
- 小さい画面でもコードが読みやすい
- ファイルを2つ並べて表示できる
- 複雑な式をより単純な形に分割する動機になる
注: 現代のプロジェクトでは少し長い上限(88、100、120文字)を使うことも多いです。重要なのは、プロジェクト内での一貫性です。上限を決めて守りましょう。
# 長すぎる - 読みにくい
# 警告: 悪いスタイル - デモ用のみ
def calculate_monthly_payment(principal, annual_rate, years):
return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
# 良い - 読みやすい行に分割
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly loan payment using amortization formula."""
monthly_rate = annual_rate / 12
num_payments = years * 12
numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
denominator = (1 + monthly_rate) ** num_payments - 1
return numerator / denominator
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}") # Output: Monthly payment: $1013.37長い行の分割: 行を分割する必要があるときは、丸括弧・角括弧・波括弧の中で暗黙的な継続を使います。
# 長い関数呼び出し
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument
)
# 長いリスト
colors = [
"red", "green", "blue",
"yellow", "orange", "purple",
"pink", "brown", "gray"
]
# 長い文字列
message = (
"This is a very long message that needs to be broken "
"across multiple lines for readability. Python automatically "
"concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.40.3.3) 演算子の周りとカンマの後のスペース
演算子の周りとカンマの後にスペースを入れて、可読性を高めます:
# 悪いスペース - 詰まっていて読みにくい
# 警告: 悪いスタイル - デモ用のみ
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
# 良いスペース - 明確で読みやすい
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
# 式の中のスペース
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
# 関数定義のスペース
def calculate_discount(price, discount_rate, minimum_purchase=0):
"""Calculate discounted price if minimum purchase is met."""
if price >= minimum_purchase:
return price * (1 - discount_rate)
return price例外: キーワード引数やデフォルト引数の = の周りにはスペースを入れません:
# キーワード引数の正しいスペース
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
# デフォルト引数の正しいスペース
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"40.3.4) 論理的な分離のための空行
空行を使って、コードの論理的なセクションを分離します:
トップレベルの関数やクラスの間は空行2行:
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class definition."""
passクラス内のメソッド間は空行1行:
class Student:
"""Represent a student with grades."""
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate the student's grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)関数内でも、論理的なステップを分けるために空行を使います:
def process_order(order_items, customer):
"""Process a customer order and calculate total."""
# 小計を計算
subtotal = 0
for item in order_items:
subtotal += item["price"] * item["quantity"]
# 顧客割引を適用
discount = 0
if customer["is_premium"]:
discount = subtotal * 0.1
# 税を計算
tax = (subtotal - discount) * 0.08
# 最終合計を計算
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}これらの空行は視覚的な「段落」として働き、コード構造がすぐにわかるようになります。
40.3.5) 行末の空白を避ける
行末にスペースを残さないでください。見えませんが、バージョン管理システムや一部のエディタで問題の原因になります。
# 悪い - 見えない行末スペース(説明のため · で表示)
# 警告: 悪いスタイル - デモ用のみ
def calculate(x):···
return x * 2···
# 良い - 行末スペースなし
def calculate(x):
return x * 2多くの現代的なエディタは、ファイル保存時に行末空白を自動削除するよう設定できます。
40.4) ドキュメント: 役立つコメントと docstring の書き方
40.4.1) コメントを書くタイミング
コメントは、コードが何をするか(what)ではなく、なぜそうするのか(why)を説明します。適切に命名された変数や関数であれば、「何をしているか」は自明になるはずです。
# 悪いコメント - 自明なことを書いている
# 警告: 悪いスタイル - デモ用のみ
x = x + 1 # x に 1 を足す
# 良いコメント - なぜを説明している
x = x + 1 # ゼロベースのインデックスに合わせて調整
# 悪いコメント - コードと冗長に重複
# 警告: 悪いスタイル - デモ用のみ
# age が 18 以上かどうかをチェック
if age >= 18:
print("Adult")
# 良いコメント - ビジネスロジックを説明
# 米国の法定飲酒年齢
if age >= 21:
print("Can purchase alcohol")コメントが有効な場面:
- 複雑なアルゴリズムを説明する:
def binary_search(sorted_list, target):
"""Search for target in sorted list using binary search."""
left = 0
right = len(sorted_list) - 1
while left <= right:
# 整数のオーバーフローを避けつつ中央を計算
# (right + left) // 2 は、非常に大きいインデックスだとオーバーフローする可能性がある
mid = left + (right - left) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1 # target は右半分にある
else:
right = mid - 1 # target は左半分にある
return -1 # target が見つからない- 明らかではないビジネスルールを明確にする:
def calculate_shipping_cost(weight, distance):
"""Calculate shipping cost based on weight and distance."""
base_cost = 5.00
# 重い品目向けの送料無料プロモーション(2024年時点の社内ポリシー)
# これによりまとめ買いを促進し、単位あたりの送料を削減する
if weight > 50:
return 0
# 標準レート: 1ポンドあたり $0.50 + 1マイルあたり $0.10
# 2024年Q1に交渉した運送会社契約に基づく
return base_cost + (weight * 0.50) + (distance * 0.10)- 回避策や一時的な解決策をドキュメント化する:
def process_data(data):
"""Process incoming data records."""
# TODO: これは不正な形式のレコードに対する一時的な修正です
# 上流側でデータバリデーションが実装されたら削除してください
if not isinstance(data, list):
data = [data]
for record in data:
# 各レコードを処理
pass40.4.2) 効果的な docstring の書き方
Docstring は、モジュール、クラス、関数をドキュメント化する特別なコメントです。トリプルクォートで囲み、定義内の最初の文として置きます。
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI is calculated as weight in kilograms divided by the square of height in meters.
Args:
weight_kg: Weight in kilograms (float or int)
height_m: Height in meters (float or int)
Returns:
float: The calculated BMI value
Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
return weight_kg / (height_m ** 2)
# docstring にアクセス
print(calculate_bmi.__doc__)
# Output:
# Calculate Body Mass Index (BMI).
#
# BMI is calculated as weight in kilograms divided by the square of height in meters.
# ...単純な関数には1行 docstring:
def square(x):
"""Return the square of x."""
return x * x
def is_even(n):
"""Return True if n is even, False otherwise."""
return n % 2 == 0複雑な関数には複数行 docstring:
def find_prime_factors(n):
"""
Find all prime factors of a positive integer.
This function returns a list of prime numbers that, when multiplied
together, equal the input number. The factors are returned in ascending order.
Args:
n: A positive integer greater than 1
Returns:
list: Prime factors in ascending order
Raises:
ValueError: If n is less than 2
Example:
>>> find_prime_factors(12)
[2, 2, 3]
>>> find_prime_factors(17)
[17]
"""
if n < 2:
raise ValueError("n must be at least 2")
factors = []
divisor = 2
while n > 1:
while n % divisor == 0:
factors.append(divisor)
n = n // divisor
divisor += 1
return factorsクラスの docstring:
class BankAccount:
"""
Represent a bank account with deposit and withdrawal operations.
This class maintains an account balance and provides methods for
depositing and withdrawing money. All transactions are validated to prevent negative balances.
Attributes:
account_number: Unique identifier for the account
balance: Current account balance in dollars
"""
def __init__(self, account_number, initial_balance=0):
"""
Initialize a new bank account.
Args:
account_number: Unique account identifier (string)
initial_balance: Starting balance (default: 0)
"""
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
"""
Add money to the account.
Args:
amount: Amount to deposit (must be positive)
Raises:
ValueError: If amount is not positive
"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount40.4.3) docstring の規約
1行目: 関数/クラスが何をするかの簡潔な要約。1行に収まるべきです。
空行: 要約と詳細説明を分けます。
詳細説明: 関数が何をするか、重要な詳細、使い方を説明します。
Args/Parameters: 各引数の型と目的を列挙します。
Returns: 戻り値の内容と型を説明します。
Raises: 起こりうる例外をドキュメント化します。
Example: 典型的な使い方を示します(任意ですが有用です)。
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
"""
Calculate compound interest on an investment.
Uses the compound interest formula: A = P(1 + r/n)^(nt)
where A is the final amount, P is principal, r is annual rate,
n is compounds per year, and t is time in years.
Args:
principal: Initial investment amount (float)
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Investment period in years (float)
compounds_per_year: Number of times interest compounds annually
(default: 1 for annual compounding)
Returns:
float: Final amount after compound interest
Example:
>>> calculate_compound_interest(1000, 0.05, 10, 12)
1647.0095
"""
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)40.4.4) 将来の作業のための TODO コメント
将来対応すべき箇所を示すには TODO コメントを使います:
def process_payment(amount, payment_method):
"""Process a payment transaction."""
# TODO: 暗号通貨決済のサポートを追加
# TODO: 不正検知チェックを実装
if payment_method == "credit_card":
return process_credit_card(amount)
elif payment_method == "paypal":
return process_paypal(amount)
else:
raise ValueError(f"Unsupported payment method: {payment_method}")多くのエディタは TODO コメントを検索できるため、作業が必要な箇所を簡単に見つけられます。
40.5) コードの整理: import、定数、関数、そして main
40.5.1) 標準的なモジュール構成
よく整理された Python モジュールは、次の構造に従います:
- モジュール docstring: モジュールが何をするかの説明
- import: 標準ライブラリ、サードパーティ、ローカルの順
- 定数: モジュールレベルの定数
- 関数とクラス: メインのコード
- main 実行ブロック: スクリプト実行時に動くコード
"""
student_manager.py
Manage student records including grades and GPA calculations.
This module provides functions for adding students, recording grades,
and calculating grade point averages.
"""
# 標準ライブラリ import
import sys
from datetime import datetime
# サードパーティ import(必要なら)
# import requests
# ローカル import(必要なら)
# from .database import save_student
# 定数
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
# 関数
def calculate_gpa(grades):
"""
Calculate GPA from a list of numeric grades.
Args:
grades: List of numeric grades (0-100)
Returns:
float: GPA on 4.0 scale
"""
if not grades:
return 0.0
average = sum(grades) / len(grades)
# 4.0 スケールに変換
if average >= 90:
return 4.0
elif average >= 80:
return 3.0
elif average >= 70:
return 2.0
elif average >= 60:
return 1.0
else:
return 0.0
def validate_grade(grade):
"""
Check if a grade is within valid range.
Args:
grade: Numeric grade to validate
Returns:
bool: True if grade is valid, False otherwise
"""
return MIN_GRADE <= grade <= MAX_GRADE
# main 実行
if __name__ == "__main__":
# スクリプトが直接実行されたときに動くコード
test_grades = [85, 92, 78, 88]
gpa = calculate_gpa(test_grades)
print(f"GPA: {gpa}") # Output: GPA: 3.040.5.2) import の整理
import は空行で区切って3セクションにまとめます:
- 標準ライブラリ import: Python 組み込みのモジュール
- サードパーティ import: インストールされたパッケージ(
requests,numpyなど) - ローカル import: 自分のモジュール
# 標準ライブラリ import
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
# サードパーティ import
import requests
from flask import Flask, render_template
# ローカルアプリケーション import
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currencyimport のスタイル:
# モジュール全体を import
import math
result = math.sqrt(16) # Output: 4.0
# 特定の項目を import
from math import sqrt, pi
result = sqrt(16) # Output: 4.0
# エイリアス付き import
import numpy as np
array = np.array([1, 2, 3])
# 複数項目を import
from os import path, getcwd, listdirワイルドカード import (from module import *) は避けてください。どこから名前が来ているのかが不明確になります:
# 悪い - sqrt がどこから来たのかわからない
# 警告: 悪いスタイル - デモ用のみ
from math import *
result = sqrt(16)
# 良い - 明示的 import
from math import sqrt
result = sqrt(16)40.5.3) 定数の整理
モジュールレベルの定数は、import の後、上のほうに置きます:
"""Configuration settings for the application."""
import os
# アプリケーション定数
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
# データベース設定
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
# ビジネスルール
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
"homework": 0.3,
"midterm": 0.3,
"final": 0.4
}
def calculate_final_grade(homework, midterm, final):
"""Calculate weighted final grade."""
return (
homework * GRADE_WEIGHTS["homework"] +
midterm * GRADE_WEIGHTS["midterm"] +
final * GRADE_WEIGHTS["final"]
)40.5.4) 論理的な関数の並べ方
関数は論理的な順序で整理します:
- 公開関数を先に: 他のモジュールから使われる想定の関数
- ヘルパー関数は後に: 公開関数を支える内部関数
- 関連する関数を近くに: 一緒に働く関数をまとめる
"""Order processing module."""
# 公開 API 関数
def process_order(order_items, customer):
"""
Process a customer order.
This is the main entry point for order processing.
"""
subtotal = _calculate_subtotal(order_items)
discount = _calculate_discount(subtotal, customer)
tax = _calculate_tax(subtotal - discount)
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}
def validate_order(order_items):
"""Validate that an order contains valid items."""
if not order_items:
return False
for item in order_items:
if not _validate_item(item):
return False
return True
# 内部ヘルパー関数
def _calculate_subtotal(items):
"""Calculate order subtotal (internal use)."""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
def _calculate_discount(subtotal, customer):
"""Calculate customer discount (internal use)."""
if customer.get("is_premium"):
return subtotal * 0.1
return 0
def _calculate_tax(amount):
"""Calculate sales tax (internal use)."""
TAX_RATE = 0.08
return amount * TAX_RATE
def _validate_item(item):
"""Validate a single order item (internal use)."""
required_fields = ["name", "price", "quantity"]
return all(field in item for field in required_fields)公開関数(process_order, validate_order)が先に来て、(先頭が _ の)ヘルパー関数が後に来ている点に注目してください。これにより、どれが主要 API なのかが明確になります。
40.5.5) モジュール内でのクラスの整理
モジュールにクラスが含まれる場合、論理的に整理します:
"""User management system."""
# 定数
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
# 基底クラスを先に
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.role = DEFAULT_ROLE
def can_edit(self, resource):
"""Check if user can edit a resource."""
return resource.owner == self.username
# 派生クラスは基底クラスの後
class AdminUser(User):
"""Administrator with elevated privileges."""
def __init__(self, username, email):
super().__init__(username, email)
self.role = ADMIN_ROLE
def can_edit(self, resource):
"""Admins can edit any resource."""
return True
# 関連するクラスをまとめる
class Resource:
"""Represent a resource that can be owned and edited."""
def __init__(self, name, owner):
self.name = name
self.owner = owner
# クラスに関連するユーティリティ関数
def create_user(username, email, is_admin=False):
"""Factory function to create appropriate user type."""
if is_admin:
return AdminUser(username, email)
return User(username, email)クラス整理の原則:
- 基底クラスを派生クラスより先に置く(読者は先に基底を理解する必要がある)
- 関連するクラスをまとめる(User と Resource は関連している)
- クラスと一緒に働くユーティリティ関数はクラス定義の後に置く
- 各クラスには目的を説明する明確な docstring を付ける
40.6) if __name__ == "__main__" パターン
40.6.1) パターンの理解
Python のすべてのファイルには __name__ という組み込み変数があります。Python は、ファイルがどのように使われるかに応じて、この変数の値を自動的に設定します:
- ファイルを直接実行したとき(例:
python my_script.py)、Python は__name__を"__main__"に設定します - ファイルをモジュールとして importしたとき、Python は
__name__をモジュール名(.pyを除いたファイル名)に設定します
これにより、import されたときではなく、ファイルが直接実行されたときだけ動くコードを書けます:
"""math_utils.py - Mathematical utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# このコードはファイルが直接実行されたときだけ動く
if __name__ == "__main__":
# 関数をテスト
print(f"5 + 3 = {add(5, 3)}") # Output: 5 + 3 = 8
print(f"5 * 3 = {multiply(5, 3)}") # Output: 5 * 3 = 15python math_utils.py を実行すると出力が表示されます。しかし、別のファイルで import すると:
# another_file.py
from math_utils import add, multiply
result = add(10, 20)
print(result) # Output: 30
# math_utils.py のテストコードは実行されないテストコード(if __name__ == "__main__": の中)は、import 時には実行されないことに注目してください。
40.6.2) このパターンが重要な理由
このパターンはいくつかの重要な目的を果たします:
1. テストとデモ: 関数と同じファイルに使用例を含められます:
"""temperature.py - Temperature conversion utilities."""
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
if __name__ == "__main__":
# 関数をデモ
print("Temperature Conversion Examples:")
print(f"0°C = {celsius_to_fahrenheit(0)}°F") # Output: 0°C = 32.0°F
print(f"100°C = {celsius_to_fahrenheit(100)}°F") # Output: 100°C = 212.0°F
print(f"32°F = {fahrenheit_to_celsius(32)}°C") # Output: 32°F = 0.0°C2. 再利用可能なモジュール: 同じファイルを単体スクリプトにも、import 可能なモジュールにもできます:
"""data_processor.py - Process and analyze data files."""
import sys
def load_data(filename):
"""Load data from a file."""
with open(filename) as f:
return [line.strip() for line in f]
def analyze_data(data):
"""Perform analysis on data."""
return {
"count": len(data),
"average_length": sum(len(item) for item in data) / len(data)
}
if __name__ == "__main__":
# スクリプトとして実行されたときは、コマンドライン引数を処理
if len(sys.argv) < 2:
print("Usage: python data_processor.py <filename>")
sys.exit(1)
filename = sys.argv[1]
data = load_data(filename)
results = analyze_data(data)
print(f"Processed {results['count']} items")
print(f"Average length: {results['average_length']:.2f}")スクリプトとして実行できます:
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23また、別のファイルから import できます:
# my_analysis.py
from data_processor import load_data, analyze_data
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")40.6.3) main ブロックのよくあるパターン
パターン1: シンプルなテストケース
"""calculator.py - Basic calculator operations."""
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract b from a."""
return a - b
if __name__ == "__main__":
# 簡易テスト
assert add(2, 3) == 5
assert subtract(10, 4) == 6
print("All tests passed!") # Output: All tests passed!パターン2: main 関数
より複雑なスクリプトでは、main() 関数を定義します:
"""report_generator.py - Generate reports from data."""
import sys
def load_data(filename):
"""Load data from file."""
# Implementation here
pass
def generate_report(data):
"""Generate report from data."""
# Implementation here
pass
def save_report(report, output_file):
"""Save report to file."""
# Implementation here
pass
def main():
"""Main entry point for the script."""
if len(sys.argv) < 3:
print("Usage: python report_generator.py <input> <output>")
return 1
input_file = sys.argv[1]
output_file = sys.argv[2]
try:
data = load_data(input_file)
report = generate_report(data)
save_report(report, output_file)
print(f"Report saved to {output_file}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
# main のステータスコードで終了(0 = 成功、1 = エラー)
sys.exit(main())このパターンにはいくつかの利点があります:
main()関数を独立してテストできる- スクリプトの明確なエントリポイントになる
- 適切な終了コード(成功は0、エラーは0以外)
- スクリプトロジックとモジュール関数の明確な分離
40.6.4) main ブロックのベストプラクティス
main ブロックは焦点を絞る: if __name__ == "__main__" の中は、主にスクリプト実行を扱い、複雑なロジックを入れないようにします:
# 悪い - main ブロックに複雑なロジック
# 警告: 悪いスタイル - デモ用のみ
if __name__ == "__main__":
data = []
for i in range(100):
if i % 2 == 0:
data.append(i * 2)
result = sum(data) / len(data)
print(result)
# 良い - ロジックは関数に、main ブロックは調整役
def generate_even_doubles(limit):
"""Generate doubled even numbers up to limit."""
return [i * 2 for i in range(limit) if i % 2 == 0]
def calculate_average(numbers):
"""Calculate average of numbers."""
return sum(numbers) / len(numbers)
if __name__ == "__main__":
data = generate_even_doubles(100)
result = calculate_average(data)
print(result) # Output: 99.0複雑なスクリプトでは main() 関数を使う: 先に示したように、main() 関数を定義すると、スクリプトがよりテストしやすく整理されます。
スクリプトの使い方をドキュメント化する: スクリプトがコマンドライン引数を受け取るなら、モジュール docstring に記載します:
"""
file_processor.py - Process text files with various operations.
Usage:
python file_processor.py <input_file> <output_file> [--uppercase]
Arguments:
input_file: Path to input file
output_file: Path to output file
--uppercase: Convert text to uppercase (optional)
"""
import sys
def process_file(input_path, output_path, uppercase=False):
"""Process file with specified options."""
with open(input_path) as f:
content = f.read()
if uppercase:
content = content.upper()
with open(output_path, 'w') as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__) # モジュール docstring を表示
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
uppercase = "--uppercase" in sys.argv
process_file(input_file, output_file, uppercase)
print(f"Processed {input_file} -> {output_file}")クリーンで読みやすいコードを書くことは、練習によって身につくスキルです。この章の規約やパターンは恣意的なルールではありません。コードを理解しやすくし、保守しやすくし、デバッグしやすくするために実証された実践です。より多くの Python コードを書くにつれ、これらのパターンは自然に身についていくでしょう。
覚えておいてください: コードは書かれるより読まれることのほうがはるかに多いのです。明確な名前を選ぶ、役立つコメントを追加する、import を適切に整理するために費やす数秒は、後で——自分自身やあなたのコードを扱う他の人にとって——何時間もの混乱を節約します。
次の章では、これらのクリーンコードの実践を土台にしたデバッグとテストの手法を探り、読みやすいコードだけでなく、正しく信頼できるコードを書く助けにしていきます。