Q1Python のプライベート変数に関する説明として最も正しいものはどれですか?
プライベート変数とカプセル化 — getter / setter で安全にアクセスする
Pythonのプライベート変数とカプセル化を解説。_xの慣習と__xの名前修飾(_Cls__xへの変換)、get_/set_メソッドでのバリデーション、@property / @setterと計算プロパティの書き方を確認できます。
前回までで 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になりますが、実際にインスタンス内に保存されている属性名はどれですか?