Python | OOP:setter

Python Python
スポンサーリンク

概要(setterは「値の更新に検証を挟む安全な入口」)

setterは、オブジェクトの属性を更新するときにルール(検証・整合性維持)を必ず通過させるための仕組みです。Pythonでは@propertyと組み合わせて「属性のように自然な書き心地」を保ちつつ、更新時にチェックや副作用を実行できます。重要なのは、不変条件(壊すと困るルール)をここに集約し、直接代入ではなく「必ずsetterを通す」設計にすることです。


基本構文(@property+@name.setterで更新ロジックを定義)

最小の例(検証付きの名前更新)

class Person:
    def __init__(self, name: str):
        self._name = name  # 内部用(直接触らせない)

    @property
    def name(self) -> str:          # 取得
        return self._name

    @name.setter
    def name(self, value: str):     # 更新(検証を挟む)
        if not value.strip():
            raise ValueError("名前は空不可")
        self._name = value
Python

この例では、nameへ代入すると自動的にsetterが呼ばれます。空文字などの不正値を拒否できるため、常に「正しい状態」を保てます。

property()関数で同等の実装をする

class Rectangle:
    def __init__(self, w: float, h: float):
        self._w = w; self._h = h

    def get_width(self) -> float:
        return self._w

    def set_width(self, v: float):
        if v <= 0:
            raise ValueError("widthは正の数")
        self._w = v

    width = property(get_width, set_width)  # getter/setterを束ねる
Python

デコレータが使えない場面でもproperty()で同じことができます。


設計の要点(不変条件・読み取り専用・防御的コピー)

不変条件をsetterに集約する

class BankAccount:
    def __init__(self, balance: int = 0):
        if balance < 0:
            raise ValueError("初期残高は負不可")
        self._balance = balance

    @property
    def balance(self) -> int:
        return self._balance

    def deposit(self, amount: int):
        if amount <= 0:
            raise ValueError("入金は正の額")
        self._balance += amount

    def withdraw(self, amount: int):
        if amount <= 0 or amount > self._balance:
            raise ValueError("引出額が不正")
        self._balance -= amount
Python

残高のように「壊すと致命的」な属性は、更新メソッドやsetterにルールを集中させます。直接代入を避けることで不変条件が守られます。

読み取り専用にして事故を防ぐ

class Rectangle:
    def __init__(self, w: float, h: float):
        if w <= 0 or h <= 0:
            raise ValueError("正の数")
        self._w = w; self._h = h

    @property
    def area(self) -> float:
        return self._w * self._h  # 計算値はsetterを持たない(読み取り専用)
Python

計算結果や識別子などは「取得だけ」にして、外から壊されない形にします。

防御的コピーで外部からの破壊を防ぐ

class Tags:
    def __init__(self, tags: list[str] | None = None):
        self._tags = [] if tags is None else list(tags)

    @property
    def tags(self) -> list[str]:
        return list(self._tags)  # コピーを返す(内部のリストを守る)

    @tags.setter
    def tags(self, value: list[str]):
        self._tags = list(value)  # 新しいリストを受け入れる
Python

可変コレクションは参照をそのまま渡すと外部から書き換えられます。コピーを返す/受け取るのが安全です。


Web / APIでの使いどころ(設定・タイムアウト・モデル整形)

クライアント設定の安全な更新

import requests

class ApiClient:
    def __init__(self, base_url: str, api_key: str, timeout: float = 5.0):
        if not base_url.startswith("https://"):
            raise ValueError("HTTPSのみ許可")
        self._base_url = base_url
        self._timeout = timeout
        self._session = requests.Session()
        self._session.headers.update({"Authorization": f"Bearer {api_key}"})

    @property
    def timeout(self) -> float:
        return self._timeout

    @timeout.setter
    def timeout(self, v: float):
        if v <= 0:
            raise ValueError("timeoutは正の数")
        self._timeout = v

    def get_user(self, uid: str) -> dict:
        r = self._session.get(f"{self._base_url}/users/{uid}", timeout=self._timeout)
        r.raise_for_status()
        return r.json()
Python

通信系は不正な設定が障害へ直結します。setterで必ず検証し、クライアントの整合性を保ちます。

モデルでの最小保証

class UserModel:
    def __init__(self, data: dict):
        uid = data.get("id")
        if not uid:
            raise ValueError("idは必須")
        self._id = str(uid)
        self._name = data.get("name") or "unknown"

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, v: str):
        if not v.strip():
            raise ValueError("名前は空不可")
        self._name = v

    def to_dict(self) -> dict:
        return {"id": self._id, "name": self._name}
Python

APIレスポンスの「揺れ」を内部で吸収し、setterで整合性を維持したまま公開します。


深掘りトピック(副作用の扱い・後方互換・イミュータブル設計)

setterの副作用は「小さく・予測可能」に

ログ記録やキャッシュ更新などを入れることはありますが、ネットワークI/Oのような重い副作用は避けるのが無難です。更新は速く、失敗時は明確な例外にすることで予測可能性が保てます。

既存の公開属性をプロパティ化してもコードは壊れない

昔は単純な属性だったnameを、今後の検証のために@property+setterへ置き換えても、呼び出し側はそのまま「obj.name」「obj.name=…」で動き続けます。これがプロパティの大きな利点です。

あえてsetterを持たない(イミュータブルに寄せる)

重要な設定や識別子は生成時に確定し、以降は書き換え不可にする設計も安全です。dataclass(frozen=True)や「setterを用意しない」方針で、更新は新インスタンス作成で表現します。

from dataclasses import dataclass

@dataclass(frozen=True)
class PriceDTO:
    amount: float
    currency: str = "JPY"
Python

よくあるつまずきと回避(直接代入・デフォルトの罠・過剰公開)

内部属性へ直接代入して検証をバイパスする

「_nameへ直接書く」ようなコードは不変条件を壊します。公開面(@propertyや明確な更新メソッド)だけを使う運用にし、コードレビューで徹底します。

可変デフォルト引数の共有事故

listやdictをデフォルトにすると全インスタンスで共有されます。Noneを受けて内部で新規生成し、各インスタンスが独立した状態を持つようにします。

なんでもsetterを作って脆くする

公開面は最小限に。中間状態までsetterで露出すると依存が増え、壊れやすくなります。利用者が本当に必要な操作だけに絞るのが鉄則です。


例題(注文オブジェクトでsetterを活用して安全運用)

数量の検証・状態遷移の整合性を維持する

class Order:
    def __init__(self, order_id: str):
        if not order_id:
            raise ValueError("order_id必須")
        self._id = order_id
        self._items: dict[str, int] = {}
        self._status = "init"  # init, placed, shipped

    @property
    def status(self) -> str:
        return self._status

    @status.setter
    def status(self, v: str):
        valid = {"init", "placed", "shipped"}
        if v not in valid:
            raise ValueError("不正な状態")
        # 設定可能な遷移のみ許可(例: init→placed→shipped)
        if (self._status, v) not in {("init", "placed"), ("placed", "shipped")}:
            raise ValueError("不正な遷移")
        self._status = v

    def add_item(self, sku: str, qty: int):
        if self._status != "init":
            raise ValueError("確定後は追加不可")
        if not sku or qty <= 0:
            raise ValueError("SKUと数量が不正")
        self._items[sku] = self._items.get(sku, 0) + qty

    @property
    def summary(self) -> dict:
        return {"id": self._id, "items": dict(self._items), "status": self._status}
Python

statusのsetterで遷移ルールを強制し、itemsは公開メソッド経由でしか変更できないようにして、注文の整合性を守ります。


まとめ(setterは「更新の唯一の入口」。不変条件をここで守る)

setterは、更新時に検証・整合性維持・防御的コピーを挟み、オブジェクトを常に正しい状態へ保つための入口です。@propertyで自然な書き心地を維持しつつ、壊せないルール(不変条件)を確実に通過させる。Web/APIでは設定やタイムアウトの検証に有効で、モデルの最小保証にも役立つ。副作用は小さく、公開面は最小限に。必要なら読み取り専用やイミュータブル設計を選ぶ。これを型として徹底すれば、初心者でも「使いやすいのに頑丈」なOOPが自然に書けます。

タイトルとURLをコピーしました