💡

綺麗なコードを書くためのTips ~ループとロジックの単純化~

2024/05/30に公開

コードの流れを読みやすくする

比較の順番を適切にする

比較文を書く時は,変化する値を左に,定数のようなより安定した値を右に配置する(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で変数をイミュータブルにするテクニックは以下。

イミュータブルな組み込みデータ型を使用する

  1. 数値型(int, float, complex)
x = 42
y = 3.14
z = 1 + 2j
  1. 文字列型(str)
s = "immutable string"
  1. タプル(tuple)
t = (1, 2, 3)
  1. 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'
GitHubで編集を提案

Discussion