Q1Python のプライベート変数に関する説明として 最も正しい ものはどれですか?
プライベート変数とカプセル化 — getter / setter で安全にアクセスする
Python のプライベート変数とカプセル化を基礎から解説します。_x の慣習と __x の名前修飾、get_xxx / set_xxx での安全なアクセス、@property / @xxx.setter の Pythonic な書き方まで丁寧に学べます。
前回までで OOP の三大要素のうち 継承 と ポリモーフィズム を押さえました。最後にカプセル化について解説します。
プライベート変数 — Python に「真のプライベート」は存在しない
Java や C++ には private キーワードがあり、宣言した瞬間に外部からアクセスできなくなります。一方 Python には言語が強制する真のプライベートは存在しません。代わりに アンダースコアの個数 で「これは内部用ですよ」「直接触らないでください」と プログラマー間の約束 として伝える慣習があります。
シングルアンダースコア _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} ← 推奨
ダブルアンダースコア __x — 名前修飾(name mangling)
属性名の先頭に _ を 2 つ 付けると、Python は内部で 属性名そのものを書き換えます。たとえば Account クラスの中で self.__pin = 1234 と書くと、実際にインスタンスに保存される名前は _Account__pin に変換されます。これを 名前修飾(name mangling) と呼び、外から obj.__pin と書いても見つからないため、事実上アクセスを難しくできます。
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 ← 修飾された名前なら届く
「__」も完全な防壁ではない
ダブルアンダースコアは 直接 obj.__pin ではアクセスできない という意味で 1 段強い保護になりますが、obj._Account__pin という変換後の名前を知っていれば結局触れます。完全プライベートではない ことは押さえておきましょう。実プロジェクトでは特殊な事情がない限り、シングルアンダースコア _x を使う方が一般的 です。
カプセル化 — メソッド経由で「触り方」を制限する
カプセル化 は「データ属性とその操作(メソッド)を 1 つのクラスにまとめ、外部からは公開された入口だけを通してアクセスさせる」設計思想です。ではどのように外部に公開する入り口を作るのか考えましょう。
_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
@property と @xxx.setter
get_price() / set_price(...) 方式は分かりやすい一方で、呼び出し側のコードが メソッド呼び出しっぽくなりスマートではないです。Python ではより洗練された書き方として、@property と @xxx.setter という 2 つのデコレータを使う方法が主流です。
これを使うと、外から見たときの記法は product.price / product.price = 2000 という 属性そのものへのアクセス に見えるのに、実際には裏で ゲッター/セッターメソッドが呼ばれる という二重構造になります。
@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.setter の price は 直前の @property def price のメソッド名と必ず一致させます。Python は「price という同じ名前のオブジェクトに、読み取り版 と 書き込み版 を張り合わせる」ような形でデコレータを解釈するため、ここで名前がズレると別物として扱われます。
OOP 三大要素
- データ保護 —
_xで内部実装と公開 API を分ける - 整合性維持 — セッターでバリデーションを 1 か所に集約
- 実装の独立性 — 内部表現を変えても外から見た API は変わらない
- Python の流儀 — 強制ではなく
_の慣習 +@property
- 親の機能を再利用する
- 同じメソッド名で型ごとに違う動き
- 外から触れる入口を限定する
理解度チェック
まずは1問ずつ答えてみましょう。
Q2@property と @xxx.setter を使う 最大のメリット はどれですか?
Q3class Account: の中で self.__pin = 1234 と書きました。外から acc.__pin でアクセスすると AttributeError になりますが、実際にインスタンス内に保存されている属性名 はどれですか?