↓ メインコンテンツへスキップ
  1. 第07講 クラス(2/2)/

カプセル化

カプセル化
#

教科書 p.229 (Lesson 8.4)

カプセル化とは、オブジェクト指向プログラミングの特徴の一つで、単純であろうと見せかける仕組みのことです。 内部の細かな仕様を理解しなくとも、外部から利用できるインターフェースだけで利用できるようにすることが目的です。 そのため、不要なデータや手続きは公開せず、公開すべきものを限定することが重要です。

他言語には、public/private のようにクラスの外からのアクセスを言語が禁止するアクセス修飾子がありますが、Python にはありません。 その代わりに Python では、以下の命名規則で「外部から使うことを想定していない」メンバ変数やメソッドを表します。

  • _(アンダースコア1つ)で始まる名前は、クラスの内部で使うものであり、外部から使うべきでないことを示す 慣習 です。
    • 言語としては何も制限されないので、外部からアクセスしようと思えばできます。「使わない」のは書く側・使う側の約束です。
  • __(アンダースコア2つ)で始まる名前(末尾が __ でないもの)は、名前修飾(name mangling) の対象になります。
    • クラス Triangle の中の __secret は、_Triangle__secret という名前に書き換えられます。 そのため obj.__secret ではアクセスできませんが、obj._Triangle__secret と書けばアクセスできます。 これはサブクラスとの名前の衝突を避けるための仕組みであり、アクセスを禁止するものではありません。

つまり、Python のカプセル化は言語による強制ではなく、「_ で始まるものは外から触らない」という約束と、公開するメソッドをきちんと設計することで成り立っています。

参考資料
#

カプセル化の例
#

例えば、3つの辺の長さを持つ三角形を表すクラスを考えます。 公開しているメソッドは次の通りです。

  • is_right: 直角三角形かどうかを判定するメソッド。
  • is_valid: 有効な三角形かどうかを判定するメソッド。
  • area: 三角形の面積を計算するメソッド。

それぞれのメソッドでは、外部からアクセスすべきでないメンバ変数 _line1, _line2, _line3 を使っています。 また、内部のアルゴリズムは外部に公開しておらず、アルゴリズムを知らなくても利用できるようになっています。 加えて、3つの辺の長さから面積を求めるメソッド _calculate_area_helons_formula も _ で始めて、外部から呼ぶことを想定していない内部用のメソッドであることを示しています(メソッド名からヘロンの公式を利用していることはわかります)。 このように、カプセル化を行うことで、外部からの利用を簡単にし、内部の実装を隠せるようになります。 面積の求め方を別の公式に変えても、area を呼ぶ側のプログラムは変更する必要がありません。

ただし前述の通り、Python には、言語としてアクセスを禁止する機能はありません。 Triangle のオブジェクトを生成した後で、_line1, _line2, _line3 や _calculate_area_helons_formula に直接アクセスすることは可能です。 しかし、_ で始まっているメンバ(フィールド、メソッド)には外からアクセスしない、という約束になっているので従うようにしましょう。

なお、この Triangle は(第06講 例題2にis_rightメソッドを追加し、必要なカプセル化を行なっています。

class Triangle:
    def __init__(self, line1, line2, line3): # 底の長さ、高さを受け取るコンストラクタ。
        self._line1 = line1 # 底辺の長さを保持するメンバ変数。
        self._line2 = line2 # 高さを保持するメンバ変数。
        self._line3 = line3 # 斜辺の長さを保持するメンバ変数。

    def is_right(self): # 直角三角形かどうかを判定するメソッド。
        l1 = max(self._line1, self._line2, self._line3)        # 最長の辺(斜辺)を求める。
        l2 = min(self._line1, self._line2, self._line3)        # 最短の辺を求める
        l3 = self._line1 + self._line2 + self._line3 - l1 - l2 # もう一つの辺の長さを求める。
        return l1 * l1 == l2 * l2 + l3 * l3   # ピタゴラスの定理が成り立てば直角三角形である。

    def is_valid(self): # 有効な三角形かどうかを判定するメソッド。
        return self._line1 + self._line2 > self._line3 and \
            self._line2 + self._line3 > self._line1 and \
            self._line3 + self._line1 > self._line2

    def area(self): # 三角形の面積を計算するメソッド。
        if not self.is_valid(): # 三角形が成立しない場合は 0 を返す。
            return 0
        return self._calculate_area_helons_formula(self._line1, self._line2, self._line3)
    
    def _calculate_area_helons_formula(self, a, b, c): # ヘロンの公式により三角形の面積を計算するメソッド。
        s = (a + b + c) / 2
        return (s * (s - a) * (s - b) * (s - c)) ** 0.5

setter/getter
#

メンバ変数を取得するためのメソッドを getter、設定するためのメソッドを setter と呼びます。 get_xxxx、set_xxxx のような名前のメソッドとして書かれることが多く、他の言語ではクラスを書くときに機械的に用意されることもあります。 しかし、近年では setter/getter は極力使わないようにしようという考えが広まっています。 メンバ変数を外から読み書きするのではなく、Triangle.area() や Stack.push() のように、オブジェクトに「してほしいこと」を依頼するメソッドを用意します。

setter/getter は極力使わない

Triangle に get_line1()、get_line2()、get_line3() を用意すると、面積の計算をクラスの外側で書くことになります。 すると、ヘロンの公式を知らないと三角形を使えなくなり、内部の実装を隠すというカプセル化の目的が達成できません。 getter で内部の値を取り出すのは、カプセル化を壊す入り口です。

同様に、setter で外から値を書き換えられるようにすると、オブジェクトの状態がどこで変わるのかを追いにくくなります。 値を変える必要があるなら、push のように、何をするのかがわかる操作をメソッドにしましょう。

これは、オブジェクト指向の原則として言われている「求めるな、命じよ(Tell, Don’t Ask)」に通じます。 オブジェクトには複雑な処理を任せて結果だけを受け取り(tell)、内部の値を根掘り葉掘り聞き出さない(don’t ask)ということです。

参考: property
#

なお、Python には property という仕組みがあり、メソッドをメンバ変数のように見せることができます。 以下は、半径の setter で不正な値を弾く例です。

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):              # getter: c.radius と書くと呼び出される。
        return self._radius

    @radius.setter
    def radius(self, value):       # setter: c.radius = 3 と書くと呼び出される。
        if value <= 0:
            raise ValueError("半径は正の数でなければなりません")
        self._radius = value

    def area(self):
        return 3.14159 * self._radius ** 2

c = Circle(2)
print(c.radius, c.area())  # 2 12.56636
c.radius = 3               # setter が呼ばれ、値のチェックが行われる。
print(c.radius, c.area())  # 3 28.27431
c.radius = -1              # ValueError: 半径は正の数でなければなりません

これは言語機能の紹介という位置付けです。 property を使えば見た目はきれいになりますが、やっていることは setter/getter と変わりません。 そのため、上で述べた「極力使わない」という考えに逆らうものです。 どうしても外から値を取り出す・設定する必要がある場合に限って使ってください。

setter/getterはどこから出てきたのか

昔、Java言語でプログラムの自動作成の考え方のもと、JavaBeansという考え方が提案されました。 JavaBeansでは、クラスのフィールドは private にして、getter/setter を用意することが推奨されました。 プログラムの一部ではありますが、自動的にプログラムを作成するため、オブジェクト内のフィールドを操作したかったわけです。

しかし、変数に直接アクセスすることは、アクセス修飾子を持つ言語としては避けたいので、getter/setter が使われるようになりました。 setter/getter があると、オブジェクト内の変数を直接操作するわけではないので、カプセル化になっているという誤解が生まれ、広まったのです。

もちろん、setterにより不正な値の設定を防げるという利点もあります。 しかし、getter/setter を用意しただけではカプセル化にはならない、ということは理解しておく必要があります。