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

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

Pythonのプライベート変数とカプセル化を解説。_xの慣習と__xの名前修飾(_Cls__xへの変換)、get_/set_メソッドでのバリデーション、@property / @setterと計算プロパティの書き方を確認できます。

前回までで 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になりますが、実際にインスタンス内に保存されている属性名はどれですか?