カプセル化とは?サザエさん一家で学ぶPythonの_・__・@propertyの使い分け【実行結果つき】

カプセル化とは、データとそれを扱う処理をひとまとめにし、外からは決められた入口だけで操作させる考え方です。Pythonでは、内部用の名前に_(アンダースコア)を付け、値の検証が必要な所だけ@propertyで入口を作るのが基本です。

この記事では、サザエさん一家を例に、カプセル化が「なぜ必要か」から、Pythonでの具体的な書き方(_ と __ の違い、名前マングリング、@property)までを、実際に動かした結果つきで解説します。コードはすべてPython 3.12.15で実行しました。オブジェクト指向の全体像はオブジェクト指向とは?サザエさん一家とPythonコードで理解する基本、クラスとインスタンスの基本はサザエさん一家で理解するクラスとインスタンスで解説しています。

カプセル化とは:お小遣いの財布で考える

サザエさんのお小遣いを管理するプログラムを考えます。残高を誰でも自由に書き換えられるとどうなるでしょうか。

class Sazae:
    def __init__(self):
        self.allowance = 10000   # 誰でも書き換えられる


sazae = Sazae()
sazae.allowance = -500           # 外から勝手に書き換えられてしまう
print(sazae.allowance)

実行結果:

-500

残高がマイナス500円になってしまいました。プログラムのどこか1か所で計算を間違えたり、うっかり代入したりするだけで、ありえない状態が生まれます。しかも、どこで書き換えられたのかを後から探すのは大変です。

そこで、残高を変える方法を「使う」というメソッドだけに限り、その中でルールを確かめるようにします。これがカプセル化です。

class Sazae:
    def __init__(self, allowance):
        self._allowance = allowance   # 「中身は直接さわらないで」の目印

    def spend(self, amount):
        if amount <= 0:
            raise ValueError("金額は1円以上にしてください")
        if amount > self._allowance:
            raise ValueError("お小遣いが足りません")
        self._allowance -= amount
        return self._allowance

    def balance(self):
        return self._allowance


sazae = Sazae(10000)
print(sazae.spend(3000))
try:
    sazae.spend(20000)
except ValueError as e:
    print("エラー:", e)
print(sazae.balance())

実行結果:

7000
エラー: お小遣いが足りません
7000

残高を減らす処理がspend()の1か所に集まったので、「0円以下は使えない」「残高より多くは使えない」というルールを必ず通るようになりました。おかしな値になったときも、調べる場所はspend()だけです。

カプセル化で得られる3つのこと

得られること お小遣いの例でいうと
おかしな状態を防げる 残高がマイナスにならない
変更の影響を閉じ込められる お小遣いの計算方法を変えても、spend()を呼ぶ側は書き換えなくてよい
読む人が迷わない 「残高を変えたければspend()を使う」と入口がはっきりしている

2つ目の「変更の影響を閉じ込める」は、フネさんの料理で考えるとわかりやすいでしょう。家族は「肉じゃがを作って」と頼むだけで、下ごしらえの順番は知りません。フネさんが手順を変えても、頼む側の言い方は変わりません。

class Fune:
    """料理の手順は隠し、外には cook() だけを見せる"""

    def cook(self, menu):
        self._prepare(menu)
        return f"{menu}ができました"

    def _prepare(self, menu):
        # 手順を変えても cook() を呼ぶ側は書き換えなくてよい
        self._steps = ["下ごしらえ", "味付け", "盛り付け"]


fune = Fune()
print(fune.cook("肉じゃが"))

実行結果:

肉じゃがができました

Pythonのアンダースコアの意味:_ と __ の違い

JavaやC#にはprivateというキーワードがあり、クラスの外からのアクセスをコンパイラが禁止します。Pythonには同じ仕組みはありません。Python公式チュートリアル「プライベート変数」には、オブジェクトの中からしかアクセスできない「プライベート」インスタンス変数はPythonにはない、と明記されています(2026年10月17日確認)。代わりに使うのが、名前の付け方による約束と、名前マングリングという仕組みです。

書き方 意味 外から読めるか
name 公開してよい属性 読める
_name 内部用。「直接さわらないで」という慣習(非公開APIとして扱う) 読める(約束で守る)
__name 名前マングリングの対象。_クラス名__nameに名前が変わる そのままの名前では読めないが、変換後の名前なら読める

波平さんの「へそくり」で確かめてみます。

class Namihei:
    def __init__(self):
        self.name = "磯野波平"        # 公開してよい
        self._memo = "盆栽の手入れ表"  # 内部用(慣習)
        self.__hesokuri = 5000        # 名前マングリングの対象


namihei = Namihei()
print(namihei.name)
print(namihei._memo)
try:
    print(namihei.__hesokuri)
except AttributeError as e:
    print("AttributeError:", e)
print(namihei._Namihei__hesokuri)
print(list(vars(namihei)))

実行結果:

磯野波平
盆栽の手入れ表
AttributeError: 'Namihei' object has no attribute '__hesokuri'
5000
['name', '_memo', '_Namihei__hesokuri']

namihei.__hesokuriはエラーになりますが、属性の一覧を見ると_Namihei__hesokuriという名前で保存されていて、その名前を使えば普通に読めてしまいます。つまり__は「鍵」ではなく「名前の付け替え」です。パスワードなどの秘密を守る手段にはなりません。

__ が本当に役立つ場面:サブクラスとの名前の衝突を防ぐ

公式チュートリアルでは、名前マングリングはサブクラスで定義された名前との衝突を避けるための限定的な仕組みだと説明されています。親クラスとサブクラスで同じ名前を使った場合を比べてみます。

class Member:
    def __init__(self):
        self.__secret = "親の秘密"
        self._note = "親のメモ"


class Katsuo(Member):
    def __init__(self):
        super().__init__()
        self.__secret = "カツオの秘密"   # 親と同じ名前でも衝突しない
        self._note = "カツオのメモ"      # こちらは親の値を上書きしてしまう


k = Katsuo()
print(list(vars(k)))
print(k._note)

実行結果:

['_Member__secret', '_note', '_Katsuo__secret']
カツオのメモ

__secretは親とカツオで別々の名前(_Member__secretと_Katsuo__secret)として保存され、ぶつかりません。一方、_noteは1つしかなく、カツオの値で親の値が上書きされています。親クラスの内部の値をサブクラスに壊されたくない、という場面以外では、_を1つ付ける書き方で十分です。

@propertyで「検証つきの入口」を作る

値を読む・書くときにルールを確かめたい場合は、組み込みのpropertyを使います。タラちゃんの年齢を例に、0〜6の整数以外は受け付けないようにしてみます(ここでのルールは説明用の設定です)。

class Tarao:
    def __init__(self, age):
        self.age = age             # ここでも setter が動く

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int) or not (0 <= value <= 6):
            raise ValueError("タラちゃんの年齢は0〜6の整数で指定してください")
        self._age = value

    @property
    def is_preschooler(self):      # 読み取り専用(setter なし)
        return self.age >= 3


tara = Tarao(3)
print(tara.age, tara.is_preschooler)
tara.age = 4
print(tara.age)
for bad in (-1, "3"):
    try:
        tara.age = bad
    except ValueError as e:
        print("ValueError:", e)
try:
    tara.is_preschooler = False
except AttributeError as e:
    print("AttributeError:", e)

実行結果:

3 True
4
ValueError: タラちゃんの年齢は0〜6の整数で指定してください
ValueError: タラちゃんの年齢は0〜6の整数で指定してください
AttributeError: property 'is_preschooler' of 'Tarao' object has no setter

ポイントは3つです。

  • 呼び出す側はtara.age = 4と、普通の属性と同じ書き方のまま。裏でsetterが動いて値を確かめている
  • __init__の中のself.age = ageでもsetterが動くので、作るときにおかしな値を渡しても防げる
  • setterを書かなかったis_preschoolerは読み取り専用になり、代入するとAttributeErrorになる

最後のエラーメッセージはPythonのバージョンによって文言が違います。手元のPython 3.9.6ではAttributeError: can't set attributeと表示されました。

getterとsetterは「必要になってから」でよい

JavaやC#から来た人は、最初からすべての属性にget_age()・set_age()を用意しがちです。Pythonでは、最初は普通の属性として公開し、検証が必要になった時点で@propertyに置き換えても、呼び出す側のtara.ageという書き方は変わりません。この性質があるので、「とりあえず全部getter・setter」にする必要がありません。propertyの書き方そのものはPythonのproperty()とは?で詳しく解説しています。

何を隠して、何を公開するか

カプセル化は「全部隠すこと」ではありません。隠しすぎると、使う側が必要な情報にたどり着けず、意味のないgetter・setterだけが増えます。判断の目安をまとめます。

分類 例 書き方の目安
勝手に変えられると困る値 残高、在庫数、状態(注文済み・発送済みなど) _を付け、変更はメソッドかproperty経由に限る
読ませたいが書かせたくない値 計算で求める値(未就学児かどうか、合計金額) setterなしの@property
手順が変わりうる処理 料理の手順、外部APIの呼び出し方 _付きのメソッドにして、公開メソッドから呼ぶ
変えても困らない値 名前、表示用のラベル 普通の属性のままでよい

迷ったら「この値を誰かが勝手に書き換えたら、どこかが壊れるか」を考えると判断しやすくなります。壊れるなら入口を1つに絞る、壊れないなら公開したままにする、という順番です。

カプセル化とオブジェクト指向のほかの考え方

カプセル化は、継承・ポリモーフィズムと並ぶオブジェクト指向の柱の1つです。継承とポリモーフィズムはポリモーフィズムとは?、クラス全体をどう設計するかはPythonのクラス設計の考え方、カプセル化を含む設計の原則はSOLID原則をサザエさん一家で解説した記事で扱っています。

クラスの書き方は、読んで理解したつもりでも、実際に自分で書いて動かすとつまずく所が見つかります。テックジムは東京本校(永田町)などに通って自分のペースで学ぶ対面型のプログラミングスクールで、詰まったところはその場でトレーナーに質問できます。まずは無料Python講座で、Pythonの基本を手を動かして確かめてみてください。

よくある質問

Q. カプセル化を一言でいうと何ですか?

データと、そのデータを扱う処理をひとまとめにし、外からは決められた入口(メソッドやプロパティ)だけを使わせる設計のことです。お小遣いの残高を直接書き換えさせず、使うときは必ず「使う」メソッドを通すようにすると、マイナスの残高のようなおかしな状態を防げます。

Q. Pythonにprivate変数はありますか?

Python公式チュートリアルには、オブジェクトの中からしかアクセスできない「プライベート」インスタンス変数はPythonにはない、と書かれています。代わりに、先頭にアンダースコア1つ(_name)を付けて「内部用」と示す慣習と、2つ(__name)で名前マングリングを起こす仕組みがあります。

Q. _(アンダースコア1つ)と__(2つ)はどう使い分けますか?

ふだんの「外から触らないでほしい」には_を1つ付けるのが基本です。__は名前が_クラス名__名前に変わる名前マングリングが起きるため、主にサブクラスで同じ名前を使っても親クラスの属性を上書きしないようにしたいときに使います。__を付けても、変換後の名前を使えば外から読めるので、セキュリティ対策にはなりません。

Q. getterとsetterはPythonでも書くべきですか?

最初から書く必要はありません。Pythonでは、まず普通の属性として公開しておき、値の検証などが必要になった時点で@propertyに置き換えれば、呼び出す側のコード(obj.age = 4 のような書き方)を変えずに済みます。get_age()のようなメソッドを機械的に並べるより、Pythonらしい書き方です。

Q. カプセル化をやりすぎると何が困りますか?

検証も変換もしないのにgetterとsetterだけを大量に用意すると、コードが長くなるだけで守れるものは増えません。何でも隠すのではなく、「勝手に変えられると困る値」と「中身を変える予定がある処理」に絞って隠すのがコツです。

プログラミングの基本概念はプログラミングの基本概念を完全解説で整理しています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら