Python | OOP:private 的記述

Python Python
スポンサーリンク
  1. 概要(Pythonの“private”的記述は「強制」ではなく“合図”と“窓口設計”)
  2. 慣習による“内部用”合図(_name と「外から直接触らない」約束)
    1. 単一アンダースコア(_name)は「内部用」の合図
  3. 名前マングリング(__name)で“強めに隠す”仕組みと注意点
    1. 二重アンダースコア(__secret)はクラス名に基づいて変形される
  4. propertyで「安全な窓口」を作る(読み取り専用・検証付き書き込み)
    1. 読み取り専用や検証付きのsetterで不変条件を守る
  5. 実務的な“private”設計(APIクライアント・設定・モデルの隠蔽)
    1. クライアントの内部詳細(セッション・ヘッダー)を隠す
    2. 設定は不変寄せで“private的”に公開
    3. モデルで揺れるレスポンスを吸収してから公開
  6. 強めの“非公開”が必要なときの選択肢(slots・frozen dataclass・合成)
    1. slotsで属性追加を禁止し、メモリも削減
    2. dataclass(frozen=True)で実質的な読み取り専用
    3. 合成(has-a)で内部機能をクラス外に隔離
  7. よくある落とし穴と対策(“隠すだけ”で満足しない・過度な __・ログ漏れ)
    1. “隠す”だけでは安全にならない
    2. 二重アンダースコアの過剰使用でテストが困難に
    3. デバッグで秘密をprintする誤習慣
  8. 例題(“private的”設計で安全な注文ドメイン)
    1. 更新の入口を一箇所に集約し、不変条件を守る
  9. まとめ(Pythonの“private”は「隠す記号」より“窓口+不変条件”で守る)

概要(Pythonの“private”的記述は「強制」ではなく“合図”と“窓口設計”)

PythonにはJavaのprivateのような強制的なアクセス修飾子はありません。代わりに「内部用」を示す命名規約(先頭にアンダースコア)と、二重アンダースコアによる名前マングリング(擬似的な隠蔽)、そして@propertyで安全な読み書きの窓口を作る——この3本柱で“壊れない設計”を実現します。要点は「直接触らせず、必ず窓口を通る」ことと、「不変条件を更新の入口で守る」ことです。


慣習による“内部用”合図(_name と「外から直接触らない」約束)

単一アンダースコア(_name)は「内部用」の合図

Pythonでは、先頭にアンダースコアを付けた属性・メソッドを“内部用”として扱う慣習があります。外部からアクセスは可能ですが、「触ると壊れやすい」ことを伝えるサインです。モジュールレベルでも、from module import * の対象外になるため、公開面を絞る効果があります。

class Account:
    def __init__(self, balance: int):
        self._balance = balance  # 内部用(直接いじらせない)
    def deposit(self, amount: int):
        if amount <= 0:
            raise ValueError("入金は正の額")
        self._balance += amount
    @property
    def balance(self) -> int:
        return self._balance
Python

このように“内部用”をアンダースコアで示し、公開面(depositやbalance)だけを触ってもらうと、不変条件を崩されません。


名前マングリング(__name)で“強めに隠す”仕組みと注意点

二重アンダースコア(__secret)はクラス名に基づいて変形される

クラス内で先頭に二重アンダースコアを付けると、属性名は _ClassName__secret の形に変換され、偶発的な上書きや衝突を避けられます。外部からの直接アクセスがしにくくなるため、“強めの隠蔽”として使えます。

class Secure:
    def __init__(self):
        self.__token = "secret"  # 外からは見えにくい
    def masked(self) -> str:
        t = self.__token
        return t[:4] + "..."

s = Secure()
# s.__token は AttributeError
print(s._Secure__token)  # 実はこうすれば触れてしまう(やってはいけない)
Python

このように「完全に触れない」わけではないため、“悪用防止”ではなく“衝突回避・誤操作防止”として理解してください。テストやモックが必要なクラスでは、二重アンダースコアの多用は避け、単一アンダースコア+明確な窓口のほうが扱いやすいです。


propertyで「安全な窓口」を作る(読み取り専用・検証付き書き込み)

読み取り専用や検証付きのsetterで不変条件を守る

内部属性を直接公開せず、@propertyで読み取り専用にしたり、setterで検証を挟むのが“Python流のprivate”です。更新ロジックを一箇所に集約し、必ず条件を満たすようにします。

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

“直接代入”を禁じるだけでなく、「必ずこの窓口を通る」という運用にすると、壊れない状態管理が自然に保てます。


実務的な“private”設計(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}"})
    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

設定は不変寄せで“private的”に公開

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

モデルで揺れるレスポンスを吸収してから公開

最低限の保証を内部に閉じ、to_dictなどの“安全な表現”だけを外へ渡します。

class UserModel:
    def __init__(self, data: dict):
        uid = data.get("id")
        if not uid:
            raise ValueError("idは必須")
        self._uid = str(uid)
        self._name = data.get("name") or "unknown"
    def to_dict(self) -> dict:
        return {"id": self._uid, "name": self._name}
Python

強めの“非公開”が必要なときの選択肢(slots・frozen dataclass・合成)

slotsで属性追加を禁止し、メモリも削減

大量インスタンスで“余計な属性の混入”を防ぎたいときに有効です。タイプミスで新しい属性が生まれる事故も防げます。

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

dataclass(frozen=True)で実質的な読み取り専用

生成後に書き換え不可のデータ容器が作れます。更新は“新インスタンス作成”で表現します。

from dataclasses import dataclass

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

合成(has-a)で内部機能をクラス外に隔離

「見せたくない機能(キャッシュ、暗号化鍵管理など)」は専用クラスに隔離し、窓口クラスはその結果だけを受け取る設計にすると、意図しないアクセスを構造的に防げます。


よくある落とし穴と対策(“隠すだけ”で満足しない・過度な __・ログ漏れ)

“隠す”だけでは安全にならない

アンダースコアも名前マングリングも、悪意あるアクセスを完全には防げません。鍵やトークンなどは「そもそもログや例外に出さない」「保存しない」運用のほうが重要です。

二重アンダースコアの過剰使用でテストが困難に

クラス内部からしか触れない設計は、モックや差し替えが難しくなります。単一アンダースコア+明確な窓口のほうが現実的です。

デバッグで秘密をprintする誤習慣

マスク出力や要約に留める習慣を徹底してください。生の値をログへ出さないことが、コードより強い“private”になります。


例題(“private的”設計で安全な注文ドメイン)

更新の入口を一箇所に集約し、不変条件を守る

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

このように、内部状態(_items, _status)には直接触れさせず、操作の窓口(add_item, place, ship)と読み取りの窓口(summary)だけを公開します。これがPythonにおける“private的”設計の完成形です。


まとめ(Pythonの“private”は「隠す記号」より“窓口+不変条件”で守る)

Pythonは強制的なprivateを持ちません。だからこそ、慣習(_name)、名前マングリング(__name)、property(検証付き窓口)を組み合わせて「外からは安全な操作だけできる」設計にするのが本質です。重要なのは、更新の入口を一箇所に集約し、不変条件を常に守ること。必要に応じてslotsやfrozen dataclass、合成で“触れさせない構造”を選ぶ。これを型として徹底すれば、初心者でも「壊れない・漏れない・使いやすい」private的なOOP設計を自然に体得できます。

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