Python | OOP:プロパティ(@property)

Python Python
スポンサーリンク

概要(@propertyは「属性のように使える安全な窓口」)

@propertyは、見た目は属性アクセスなのに、裏側で関数(メソッド)が実行される仕組みです。値の取得(getter)、設定(setter)、削除(deleter)に処理や検証を差し込めるため、カプセル化と使い心地を両立できます。初心者は「内部状態は隠して、@property経由で安全に読み書きする」という型をまず身につけましょう。


基本構文(getter/setter/deleterの三位一体)

最小のパターン

@propertyを付けたメソッドが「取得用の属性」になります。必要なら同名の@name.setterで更新ロジックを、@name.deleterで削除ロジックを用意します。これにより、通常の変数アクセスの書き心地で、裏では検証や計算を走らせられます。

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

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

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

    @name.deleter
    def name(self):                 # deleter(必要なら)
        del self._name
Python

property()関数で同じことをする

デコレータではなく、組み込み関数property(fget, fset, fdel, doc)でも同様に作れます。fgetがgetter、fsetがsetter、fdelがdeleterで、docは説明文です。

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
    def del_width(self): del self._w

    width = property(get_width, set_width, del_width, "幅のプロパティ")
Python

設計の要点(不変条件・読み取り専用・計算プロパティ)

不変条件を守る更新の入口

壊してはいけない条件(IDは非空、サイズは正、残高は負にならない)をsetterで徹底します。直接代入させず、@propertyの窓口からしか更新できない設計にして、常に整合した状態を保ちます。

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

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

書き換えられると壊れる属性(計算結果、識別子など)はgetterのみ提供して読み取り専用にします。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  # 読み取り専用の計算プロパティ
Python

Web / API文脈の例(安全な公開面と内部封じ込め)

設定値の安全な公開

APIキーやベースURLは書き換え事故の影響が大きいので、内部に保持しつつ読み取り専用で公開します。これで利用側は「使う」ことだけに集中できます。

class Settings:
    def __init__(self, base_url: str, api_key: str):
        self._base_url = base_url
        self._api_key = api_key

    @property
    def base_url(self) -> str: return self._base_url

    @property
    def api_key(self) -> str: return self._api_key
Python

クライアントの内部詳細を隠して操作だけ見せる

セッションやヘッダー構築はクラス内部に閉じ込め、外部には安全な操作メソッドだけを公開します。タイムアウトなどはpropertyで検証付きにして、無効値の流入を防ぎます。

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

よくある落とし穴(直接代入・脆い公開・デフォルトの罠)と回避

直接代入で検証をバイパスしない

内部用属性(_nameなど)への外部からの直接代入は不変条件を壊します。公開面(@propertyや明確なメソッド)だけを使う運用にし、レビューで徹底します。

「なんでも公開」で壊れやすくしない

内部の中間状態までプロパティ化すると、依存が増えて脆くなります。利用者が必要とする最小限だけを公開し、内部は隠すのが安全です。

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

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

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)  # 防御的コピーで外部からの破壊を防ぐ
Python

例題(注文オブジェクトを@propertyで安全運用)

状態遷移と整合性を“窓口”で守る

注文の状態・品目を内部に閉じ、読み取りはsummaryだけ、更新はメソッドだけにします。これで順序や数量の不変条件を強制できます。

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

    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.append((sku, qty))

    def place(self):
        if not self._items: raise ValueError("商品がない注文は確定不可")
        self._status = "placed"

    def ship(self):
        if self._status != "placed": raise ValueError("未確定の注文は出荷不可")
        self._status = "shipped"

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

まとめ(@propertyは「使いやすさ」と「安全性」を同時に叶える)

@propertyは、属性アクセスの心地よさを保ちながら、取得・設定・削除に検証や整形を差し込める強力な“窓口”です。カプセル化の核として、内部用は_で隠し、公開は@propertyで最小限に。不変条件はsetterや更新メソッドで必ず守り、読み取り専用・計算プロパティを活用する。これを型として徹底すれば、初心者でも「壊れないのに使いやすい」OOP設計を自然に作れます

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