順番に読み進めながら学べます

プライベート変数とカプセル化 — getter / setter で安全にアクセスする

Python のプライベート変数とカプセル化を基礎から解説します。_x の慣習と __x の名前修飾、get_xxx / set_xxx での安全なアクセス、@property / @xxx.setter の Pythonic な書き方まで丁寧に学べます。

前回までで OOP の三大要素のうち 継承ポリモーフィズム を押さえました。最後にカプセル化について解説します。

プライベート変数 — Python に「真のプライベート」は存在しない

Java や C++ には private キーワードがあり、宣言した瞬間に外部からアクセスできなくなります。一方 Python には言語が強制する真のプライベートは存在しません。代わりに アンダースコアの個数 で「これは内部用ですよ」「直接触らないでください」と プログラマー間の約束 として伝える慣習があります。

アンダースコア 0 / 1 / 2 個で意図を伝える
name(なし)公開属性外から自由に使える_name(1 個)慣習的プライベート触らない__name(2 個)
_ の個数 だけで強制力が変わるわけではなく、意図を表すためのラベル

シングルアンダースコア _x — 慣習的プライベート

属性名の先頭に _ を 1 つ 付けると、Python のコミュニティでは「この属性はクラスの内部で使うもの。外から直接アクセスしないでください」というメッセージになります。__init__ の引数の方は普通の名前で受け取り、self に格納するときだけ _ を付ける のが定番です。

class UserAccount:
    def __init__(self, owner_name, balance):
        self._owner_name = owner_name      # 内部用 → _ を付ける
        self._balance    = balance

    def get_info(self):                     # 外向きの取り出し口
        return {"owner": self._owner_name, "balance": self._balance}


user = UserAccount("田中", 50000)
print(user._balance)        # 50000  ← 一応動くが推奨されない
print(user.get_info())      # {'owner': '田中', 'balance': 50000}  ← 推奨

ブログサイトの記事クラス BlogPost を題材に、_ の意味を体感します(上のサンプルとは別のシナリオで同じパターンを書いてみる練習です)。

class BlogPost: を定義し、__init__(self, title, views)self._titleself._views に代入してください。

summary(self) メソッドを定義し、{"title": self._title, "views": self._views}return してください。

post = BlogPost("Python の始め方", 100) を作り、まず 直接アクセスで post._viewsprint してください(動くが本来は禁止のパターン)。

④ 次に 推奨パターン として post.summary() の戻り値を print してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

ダブルアンダースコア __x — 名前修飾(name mangling)

属性名の先頭に _ を 2 つ 付けると、Python は内部で 属性名そのものを書き換えます。たとえば Account クラスの中で self.__pin = 1234 と書くと、実際にインスタンスに保存される名前は _Account__pin に変換されます。これを 名前修飾(name mangling) と呼び、外から obj.__pin と書いても見つからないため、事実上アクセスを難しくできます

__pin は内部で _Account__pin に書き換わる
self.__pin= 1234Python が名前を変換_Account__pin= 1234obj.__pin→ Errorobj._Account__pin→ 1234保存
ダブルアンダースコア の属性は、「アンダースコア + クラス名 + 元の属性名」 に変換されて保存される。obj.__pin で直接読みに行っても見つからないので AttributeError になる。
class Account:
    def __init__(self, owner, pin):
        self._owner = owner       # 慣習的プライベート
        self.__pin  = pin         # 名前修飾あり(_Account__pin に変換)

acc = Account("田中", 1234)

print(acc._owner)              # 田中             ← 普通に動く
# print(acc.__pin)             # AttributeError ← 直接は見えない
print(acc._Account__pin)       # 1234            ← 修飾された名前なら届く

ログインフォームのクラス LoginForm を題材に、__password本当に名前変換されている ことを目で確かめます(上の Account サンプルとは別シナリオで同じ挙動を再現する練習です)。

class LoginForm: を定義し、__init__(self, username, password)self._username = usernameself.__password = password を代入してください。

form = LoginForm("taro", "p@ssw0rd") を作り、print(form._username)シングル側はそのまま読める ことを確認してください。

print(form._LoginForm__password)名前修飾された属性 経由でパスワード文字列が取れることを確認してください。

print([n for n in dir(form) if not n.startswith('__')])インスタンス内に保存されている属性名一覧 を出し、_LoginForm__password が並んでいることを目で確認してください。

Python エディタ

コードを実行してください

「__」も完全な防壁ではない

ダブルアンダースコアは 直接 obj.__pin ではアクセスできない という意味で 1 段強い保護になりますが、obj._Account__pin という変換後の名前を知っていれば結局触れます。完全プライベートではない ことは押さえておきましょう。実プロジェクトでは特殊な事情がない限り、シングルアンダースコア _x を使う方が一般的 です。

カプセル化 — メソッド経由で「触り方」を制限する

カプセル化 は「データ属性とその操作(メソッド)を 1 つのクラスにまとめ、外部からは公開された入口だけを通してアクセスさせる」設計思想です。ではどのように外部に公開する入り口を作るのか考えましょう。

外部からの読み書きを「メソッド 1 か所」に集める
整合性崩壊直接代入_price = -100❌ 外部product._price= -100整合性維持set_price()バリデーション✅ 外部product.set_price(100)そのままOK 時のみ
直接書き換え は検証なしで _price に値が入る。メソッド経由 にすればセッターで「型・範囲」を 1 か所でチェックできる。

もっとも素朴な書き方は、get_xxx / set_xxx という名前のメソッドを自分で並べる方式です。セッターの中で isinstance による型チェック値の範囲チェック を行い、おかしな値が来たら raise ValueError(...) で止めれば、_price に変な値が入る心配がなくなります。

class Product:
    def __init__(self, name, price, stock):
        self._name  = name
        self._price = price
        self._stock = stock

    def get_price(self):
        return self._price

    def set_price(self, price):
        if isinstance(price, int) and price >= 0:
            self._price = price
        else:
            raise ValueError("price は 0 以上の整数で指定してください")


product = Product("Tシャツ", 1500, 30)
print(product.get_price())     # 1500
product.set_price(2000)
print(product.get_price())     # 2000
# product.set_price(-100)      # ValueError

ユーザー登録フォームの会員クラス UserProfile に、age(年齢)専用のゲッターとセッター を実装します。年齢にはマイナスや 200 歳といった不正値が入らないように、セッター側で範囲チェックをかけるのがポイントです。

class UserProfile: を定義し、__init__(self, name, age)_name / _age に代入してください。

get_age(self) を定義して return self._age してください。

set_age(self, age) を定義し、age が int 型かつ 0 以上 150 以下 のときだけ self._age = age、それ以外は raise ValueError("age は 0〜150 の整数で指定してください") してください。

user = UserProfile("田中", 30) を作り、user.get_age()print し、続けて user.set_age(31) してから再度 get_age() の結果を print してください。

⑤ 不正値の挙動を確認するため、try / except ValueError as e:user.set_age(-5) を囲み、print("NG:", e) を出力してください。

Python エディタ

コードを実行してください

@property と @xxx.setter

get_price() / set_price(...) 方式は分かりやすい一方で、呼び出し側のコードが メソッド呼び出しっぽくなりスマートではないです。Python ではより洗練された書き方として、@property@xxx.setter という 2 つのデコレータを使う方法が主流です。

これを使うと、外から見たときの記法は product.price / product.price = 2000 という 属性そのものへのアクセス に見えるのに、実際には裏で ゲッター/セッターメソッドが呼ばれる という二重構造になります。

product.price の見た目はそのまま、内部でメソッドが走る
product.price@propertydef priceself._priceを returnproduct.price= 2000@price.setterdef price検証してself._price 更新読む書く
属性アクセスの構文 を維持したまま、@property が読み取り、@price.setter が書き込みを メソッドへリダイレクト。バリデーションは setter の中に入れる。
class Product:
    def __init__(self, name, price):
        self._name  = name
        self._price = price

    @property
    def price(self):                 # ゲッター
        return self._price

    @price.setter
    def price(self, value):          # セッター。メソッド名はゲッターと一致させる
        if not isinstance(value, int) or value < 0:
            raise ValueError("price は 0 以上の整数で指定してください")
        self._price = value

    @property
    def label(self):                 # 計算プロパティ — 派生値も @property で表現できる
        return f"{self._name} ({self._price}円)"


product = Product("Tシャツ", 1500)
print(product.price)         # 1500           ← @property が呼ばれる
product.price = 2000         # ← @price.setter が呼ばれる
print(product.price)         # 2000
print(product.label)         # Tシャツ (2000円)  ← 計算プロパティ

セッターの名前はゲッターと「同じ」にする

@price.setterprice直前の @property def price のメソッド名と必ず一致させます。Python は「price という同じ名前のオブジェクトに、読み取り版書き込み版 を張り合わせる」ような形でデコレータを解釈するため、ここで名前がズレると別物として扱われます。

実践 3 と同じ UserProfile を、@property / @age.setter に書き直し、さらに 計算プロパティ age_group未成年 / 成人 / シニア)を追加します。

class UserProfile: を定義し、__init__(self, name, age)_name / _age に代入してください。

@property def age(self):return self._age してください。

@age.setter def age(self, value): を定義し、isinstance(value, int) and 0 <= value <= 150 のときだけ self._age = value、それ以外は raise ValueError("age は 0〜150 の整数で指定してください") してください。

@property def age_group(self): を定義し、self._age < 18 なら "未成年"self._age < 65 なら "成人"、それ以上は "シニア" を返してください(セッターは作らない = 読み取り専用)。

user = UserProfile("田中", 30) を作って user.ageuser.age_groupprint し、続けて user.age = 70 で更新後にもう一度 age_groupprint してください。

Python エディタ

コードを実行してください

OOP 三大要素

カプセル化が支えている設計
カプセル化(encapsulation)
  • データ保護_x で内部実装と公開 API を分ける
  • 整合性維持 — セッターでバリデーションを 1 か所に集約
  • 実装の独立性 — 内部表現を変えても外から見た API は変わらない
  • Python の流儀 — 強制ではなく _ の慣習 + @property
継承
  • 親の機能を再利用する
ポリモーフィズム
  • 同じメソッド名で型ごとに違う動き
カプセル化
  • 外から触れる入口を限定する
継承・ポリモーフィズム・カプセル化がオブジェクト指向の三大要素。継承で機能を再利用し、ポリモーフィズムで使う側を統一し、カプセル化で壊れにくさを担保する — 役割分担を意識して設計するとクラス全体が見通しよくなる。
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1Python のプライベート変数に関する説明として 最も正しい ものはどれですか?

Q2@property@xxx.setter を使う 最大のメリット はどれですか?

Q3class Account: の中で self.__pin = 1234 と書きました。外から acc.__pin でアクセスすると AttributeError になりますが、実際にインスタンス内に保存されている属性名 はどれですか?