綺麗なコードを書くためのTips ~ループとロジックの単純化~
コードの流れを読みやすくする
比較の順番を適切にする
比較文を書く時は,変化する値を左に,定数のようなより安定した値を右に配置する(length>10)
if/elseブロックの並びを適切にする
if/elseブロックは適切な並びにする。具体的には肯定系・単純・目立つものを先に処理する。
これらの優先順はバッティングすることもある。例えば,否定形の条件であっても単純で関心や注意を引く場合もあるため,そういう場面ではそれを先に処理する
深いネストの回避
深いネストはできるだけ避ける,例えば,早く返り値(return)を返すようにする・「ガード節(関数の上部で単純な条件を処理するもの)」が便利。
def divide(a, b):
# ガード節の例
if b == 0:
raise ValueError("Division by zero is not allowed")
if a == 0:
return 0
# 正常な処理
return a / b
print(divide(10, 2)) # 出力: 5.0
print(divide(0, 2)) # 出力: 0
print(divide(10, 0)) # 出力: ValueError: Division by zero is not allowed
また,ループ中で"早めに返り値を返す"と同じようなことを行うにはcontinueを使う。一方,continueは分かりにくくなることも多い。これは,ループを行ったり来たりして確認しなければいけなくなる可能性があるため。だから,複数にネストされたループにおいてcontinueを使う際は注意が必要。
def process_numbers(numbers):
result = []
for number in numbers:
# ガード節
if number < 0:
continue # 負の数はスキップ
result.append(number * 2)
return result
numbers = [10, -5, 3, -2, 7, -1, 8]
processed_numbers = process_numbers(numbers)
print(processed_numbers) # 出力: [20, 6, 14, 16]
巨大な式を分割して分かりやすくする
説明変数を使う
大きな式には,その値を保持する「説明変数」を使う。このメリットとして,簡単な名前で式を表すことでコードを文書化できる,コードの主要な概念を読み手が認識しやすくすることができる。
- Before:
def is_eligible_for_discount(age, is_student, has_membership, total_purchase):
if age < 18 or age > 65 or is_student or has_membership and total_purchase > 100:
return True
return False
age = 20
is_student = False
has_membership = True
total_purchase = 150
print(is_eligible_for_discount(age, is_student, has_membership, total_purchase)) # 出力: True
- After:
def is_eligible_for_discount(age, is_student, has_membership, total_purchase):
is_underage = age < 18
is_senior = age > 65
is_qualified_student = is_student
has_large_purchase_with_membership = has_membership and total_purchase > 100
if is_underage or is_senior or is_qualified_student or has_large_purchase_with_membership:
return True
return False
age = 20
is_student = False
has_membership = True
total_purchase = 150
print(is_eligible_for_discount(age, is_student, has_membership, total_purchase)) # 出力: True
条件式を理解しやすくする
ド・モルガンの法則を使って条件式はできるだけ理解しやすくする(例: notを分配してand/orを反転する)
- Before:
def is_neither_true(a, b):
return not (a or b)
a = False
b = False
print(is_neither_true(a, b)) # 出力: True
- After:
def is_neither_true(a, b):
return not a and not b
a = False
b = False
print(is_neither_true(a, b)) # 出力: True
変数と読みやすさ
不要な変数の削除
できるだけ邪魔な変数を削除する。例えば,中間結果を保持している変数や制御フロー変数はできるだけ削除する。
- Before:
def find_value_bad(lst, target):
# 制御フロー変数
found = False
for item in lst:
if item == target:
found = True
break
if found:
return f"{target} was found in the list."
else:
return f"{target} was not found in the list."
# 使用例
lst = [1, 2, 3, 4, 5]
target = 3
print(find_value_bad(lst, target))
target = 6
print(find_value_bad(lst, target))
- After:
def find_value_good(lst, target):
#制御フロー変数がない形で同様の処理を実装
if target in lst:
return f"{target} was found in the list."
else:
return f"{target} was not found in the list."
# 使用例
lst = [1, 2, 3, 4, 5]
target = 3
print(find_value_good(lst, target))
target = 6
print(find_value_good(lst, target))
変数のスコープは小さくする
変数のスコープはできるだけ小さくする。グローバル変数はどこでどのように使われるのかを追跡するのが難しい且つローカル変数とconflictする可能性もある。
- Before:
class DataProcessorBad:
def __init__(self, data):
self.data = data
self.result = None
def preprocess_data(self):
# self.data を加工する
self.data = [x * 2 for x in self.data]
def compute_result(self):
# self.data に基づいて結果を計算する
self.result = sum(self.data) / len(self.data)
def get_result(self):
# 計算された結果を返す
return self.result
# 使用例
data = [1, 2, 3, 4, 5]
processor_bad = DataProcessorBad(data)
processor_bad.preprocess_data()
processor_bad.compute_result()
print(processor_bad.get_result())
- After:
class DataProcessorGood:
def __init__(self, data):
self.data = data
def preprocess_data(self, data):
# ローカル変数 data を加工する
return [x * 2 for x in data]
def compute_result(self, data):
# ローカル変数 data に基づいて結果を計算する
return sum(data) / len(data)
def process(self):
# 全体の処理をまとめる
processed_data = self.preprocess_data(self.data)
result = self.compute_result(processed_data)
return result
# 使用例
data = [1, 2, 3, 4, 5]
processor_good = DataProcessorGood(data)
print(processor_good.process())
変数の定義位置を下げる
変数がコード上部で複数定義されている場合は,定義位置を下げる。複数の変数が最初に定義されていると,全変数を切り替えて考えなければならないため認知負荷が高くなる。
- Before:
def process_data(data):
result = None
processed_data = []
# データの前処理
for value in data:
processed_data.append(value * 2)
# 結果の計算
total = sum(processed_data)
count = len(processed_data)
if count > 0:
result = total / count
return result
# 使用例
data = [1, 2, 3, 4, 5]
print(process_data(data))
- After:
def process_data(data):
# データの前処理
processed_data = [value * 2 for value in data]
# 結果の計算
if processed_data:
total = sum(processed_data)
count = len(processed_data)
result = total / count
else:
result = None
return result
# 使用例
data = [1, 2, 3, 4, 5]
print(process_data(data))
一度だけ書き込む変数を使う
一度だけ書き込む変数を使う。変数が絶えず変更され続けると値を追跡する難易度が格段に上がる。そのため,同じ変数が複数個所で書き換えられている場合には,リファクタリングを検討する。
また,書き換えを防ぐために変数はできるだけイミュータブルにすると良い。pythonで変数をイミュータブルにするテクニックは以下。
イミュータブルな組み込みデータ型を使用する
- 数値型(int, float, complex)
x = 42
y = 3.14
z = 1 + 2j
- 文字列型(str)
s = "immutable string"
- タプル(tuple)
t = (1, 2, 3)
- frozenset
f = frozenset([1, 2, 3])
namedtuple
を使う
collections.namedtuple
を使用すると,イミュータブルなオブジェクトを簡単に作成できる。
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# p.x = 3 # AttributeError: can't set attribute
frozen=True
を使う
データクラスで Python 3.7以降では、dataclasses
モジュールを使ってイミュータブルなデータクラスを定義できる。
from dataclasses import dataclass
@dataclass(frozen=True)
class ImmutablePoint:
x: int
y: int
p = ImmutablePoint(1, 2)
# p.x = 3 # FrozenInstanceError: cannot assign to field 'x'
types.MappingProxyType
を使う
辞書をイミュータブルにする場合は,types.MappingProxyType
を使う。
from types import MappingProxyType
d = {'key': 'value'}
d_proxy = MappingProxyType(d)
# d_proxy['key'] = 'new value' # TypeError: 'mappingproxy' object does not support item assignment
カスタムイミュータブルクラスを定義する
特殊メソッド__setattr__
や__delattr__
をオーバーライドして、属性の変更を禁止するクラスを作成できる。
class ImmutableClass:
def __init__(self, value):
super().__setattr__('value', value)
def __setattr__(self, name, value):
raise AttributeError(f"Cannot modify attribute '{name}'")
def __delattr__(self, name):
raise AttributeError(f"Cannot delete attribute '{name}'")
obj = ImmutableClass(10)
# obj.value = 20 # AttributeError: Cannot modify attribute 'value'
# del obj.value # AttributeError: Cannot delete attribute 'value'
Discussion