Python | OOP:属性

Python Python
スポンサーリンク

概要(属性は「オブジェクトが持つデータ」。設計と使い分けが要)

属性はクラスやインスタンスが持つデータで、振る舞い(メソッド)と対になる要素です。まず理解すべきは、インスタンス属性とクラス属性の違い、可変なデフォルトの落とし穴、アクセス制御の慣習(_name, __name)、そして@propertyによる安全な読み書きです。これらを押さえると、Web/APIコードでも「壊れない・読みやすい」状態管理ができます。


インスタンス属性とクラス属性(共有か個別かの線引き)

使い分けの基本

インスタンス属性は「各オブジェクトごとに異なる値」。クラス属性は「全インスタンスで共有したい定数や設定」。可変データは原則インスタンス側に置き、クラス属性は不変な値に限定すると事故が減ります。

class Counter:
    unit = "回"          # クラス属性(共有する定数)
    def __init__(self):
        self.n = 0       # インスタンス属性(個別の状態)

c1, c2 = Counter(), Counter()
c1.n += 1
c2.n += 2
print(c1.n, c2.n, Counter.unit)  # 1 2 回
Python

可変データの共有事故を避ける

クラス属性にlistやdictを置くと、全インスタンスで「同じもの」を共有します。意図せず連動するため、可変はインスタンス属性へ。

class Bag:
    items = []  # NG: 全インスタンスで共有され続ける

class SafeBag:
    def __init__(self):
        self.items = []  # OK: 個別に持つ
Python

初期化で属性を作る(initと型ヒント・バリデーション)

initで正しい状態を用意する

属性はinitでself.xxxとして定義し、前提チェックを入口で行います。型ヒントを添えると契約が明確になり、補完や静的チェックが効きます。

class User:
    def __init__(self, user_id: str, name: str | None = None):
        if not user_id:
            raise ValueError("user_idは必須です")
        self.user_id = user_id
        self.name = name or "unknown"
Python

デフォルトに可変を使わない

Noneを受けて中で新規生成する型にすれば、安全です。

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

アクセス制御の慣習(公開・内部・名前マングリング)

命名で意図を伝える

先頭にアンダースコアを付けると「内部用」の意図を示せます。外部から触れるな、ではなく「触ると壊れやすい」というサインです。

class Account:
    def __init__(self, balance: int):
        self._balance = balance  # 内部属性(直接操作は推奨しない)
Python

名前マングリング(__name)で衝突回避

クラス内で先頭に二重アンダースコア(__secret)を付けると、クラス名に基づいて変形され、意図しない上書きや衝突を避けられます。

class Secure:
    def __init__(self):
        self.__token = "secret"  # 外部からは Secure._Secure__token としてしか触れない
Python

公開面は@propertyで整える

直接書き換えられると壊れる属性は@propertyで守り、setterで検証します。

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

    @property
    def width(self) -> float:
        return self._w

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

    @property
    def area(self) -> float:
        return self._w * self._h
Python

属性の仕組み(探索順序・dict・slots)

属性探索の順序を理解する

アクセス時は「インスタンス→クラス→親クラス…」の順に探されます。インスタンスに同名があればそちらが優先され、無ければクラス属性が使われます。これを利用して「デフォルトはクラス属性、上書きはインスタンス属性」が可能です。

class Config:
    timeout = 5.0  # デフォルト

cfg = Config()
print(cfg.timeout)  # 5.0
cfg.timeout = 10.0  # インスタンスに上書き
print(cfg.timeout)  # 10.0
Python

dictで中身を覗く

インスタンスの属性は通常dictに入ります。どんな属性があるかの確認に便利です。

class A:
    def __init__(self):
        self.x = 1
a = A()
print(a.__dict__)  # {'x': 1}
Python

slotsで属性を制限・軽量化

大量生成する小さなオブジェクトでは、許可する属性名をslotsで固定するとメモリ削減・タイプミス防止になります。

class Point:
    __slots__ = ("x", "y")
    def __init__(self, x: float, y: float):
        self.x = x; self.y = y
Python

Web / API文脈の例(設定・モデル・クライアントの属性設計)

設定は不変に寄せる

設定値はクラス全体で共有したいならクラス属性、またはインスタンス生成後は読み取り専用の@propertyで守ります。

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

モデルは「最小の保証」を属性で表現

APIレスポンスの揺れを吸収し、内部で扱いやすい形にするのが役目です。

class UserModel:
    def __init__(self, data: dict):
        self.user_id = str(data.get("id", ""))
        self.name = data.get("name") or "unknown"

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

クライアントは再利用する属性(セッション・ヘッダー)

共通の送信設定を属性に持ち、各メソッドから利用するとミスが減ります。

import requests

class ApiClient:
    def __init__(self, base_url: str, api_key: str, timeout: float = 5.0):
        self.base_url = base_url
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})
Python

よくあるつまずきと回避(共有事故・隠蔽・更新ルール)

クラス属性の共有ミス

全インスタンスで値が連動して驚くケースは、可変データをクラス属性に置いたため。可変は必ずインスタンス属性で個別管理に切り替えます。

隠蔽し過ぎて使いづらい

全部を_で固めると、利用側が拡張できず窮屈になります。基本は「で内部を示し、公開面は@property」で十分です。

更新の入り口を一箇所に

値の整合性が重要なら、setterやupdateメソッドを通して変更させます。直接書き換えを避けると、バリデーションの抜け漏れを防げます。

class Profile:
    def __init__(self, name: str):
        self._name = name

    def rename(self, new_name: str):
        if not new_name.strip():
            raise ValueError("名前は空不可")
        self._name = new_name
Python

例題(属性の設計を通して安全な小さなアプリを作る)

設定→クライアント→サービスの属性設計

from dataclasses import dataclass
import requests

@dataclass(frozen=True)
class Settings:
    base_url: str
    api_key: str

class Client:
    def __init__(self, s: Settings, timeout: float = 5.0):
        self.timeout = timeout
        self.base_url = s.base_url
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {s.api_key}"})

    def get(self, path: str) -> dict:
        r = self.session.get(f"{self.base_url}{path}", timeout=self.timeout)
        r.raise_for_status()
        return r.json()

class UserService:
    def __init__(self, client: Client):
        self.client = client

    def user(self, uid: str) -> dict:
        data = self.client.get(f"/users/{uid}")
        u = UserModel(data)
        return u.to_dict()
Python

この構成は、可変と不変を属性で適切に分け、再利用しやすくテストしやすい状態を作ります。


まとめ(属性は「どこで持ち、どう守るか」を決めるのが肝)

属性の核心は、個別の状態はインスタンス属性、共有したい定数はクラス属性という線引きです。initで正しい初期化を行い、可変のデフォルトを避け、内部用は_で示す。重要な値は@propertyで守って検証し、更新の入口を用意する。探索順序とdict/slotsを理解して、動作とコストを制御する。これを型として徹底すれば、初心者でも「安全で見通しの良い」属性設計が自然にできるようになります。

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