まえがき
今回からPythonの文法事項について解説していく講義形式の記事を書く。どちらかというと、初心者向けテキスト扱われることが少ないテーマを中心に扱うことを目的としているが、一方でこの記事だけでも、その理解に前提となる内容も含めて理解が完結するように工夫も行う。ただ、しっかりとした文章を書くというよりも、本を読みながら肉付けした読書メモみたいな体裁になることについてはご了承いただきたい。
今回はオブジェクト指向プログラミングについて扱う。大抵のPython本ではそのさわりしか触れられておらず、例えば継承は触れられているが、多重継承や多態性や隠蔽についてはもちろん、データクラスについては触れられないといったテキストがほとんどである。
それ自体がこの記事を書く理由にはなりえないが、動機とは少なくともなりえたので、ここでまとめて整理する。
カプセル化(Encapsuation)
前回の記事では、オブジェクト指向の中核であるクラスの基本について書いた*。以降ではオブジェクト指向らしいコードを記述するための技術について扱う。
カプセル化:クラスで用意された機能のうち、利用するうえで知らなくても差し支えないものを隠すこと
テレビを例にとると分かりやすいかも。テレビの中に様々な複雑な回路が含まれているが、それらは隠され電源やチャンネルといったごく限られた機能のみがフロントに出ている。
クラスにおいても同様。クラスにも利用者に使ってほしい機能と機能を実現するための内部的な機能に分かれる。それら難十個にも及ぶメンバーが区別なく公開されていれば、混乱のもとになる。
インスタンス変数の隠蔽
これまではクラスの中で保持するデータをインスタンス変数という形で外部に公開してきた。例えば
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def show(self) -> None:
print(f'私の名前は{self.name}、{self.age}歳です!')
しかし、インスタンス変数をそのまま外からアクセスできる状態にしておくことは好ましくない。
-
読み書きの許可/禁止を制御できない
インスタンス変数は、インスタンスの状態を管理するための変数。よって性質上、値の取得は許しても、変更には何らかの制限を課したい場合がほとんど(複数のインスタンス変数が互いに関連を持っている場合には尚更) -
値の妥当性を検証できない
Pythonはデータ型という観点で寛容(というか緩い)言語。例えば、ageに正の整数以外の値は入れたくないとなっても。負の浮動小数点や、文字列型の代入も可能。
インスタンス変数を参照しているメソッドで型チェックを行うことも可能だが、非推奨。複数のメソッドから同じ変数を参照している場合、同様の検証ロジックや呼び出しが散在しているのは、コードの保守性以前に好ましい状態ではない。 -
内部状態の変更に左右されない
オブジェクトの内部状態(インスタンス変数)に、外部から作用させるべきではない。
→ インスタンス変数はオブジェクトの内部的な状態を表すもの。しかし実装では、内部的な値の持ち方も変化するかもしれなし、ある変数を参照するすべてのコードが影響を受ける可能性がある。
オブジェクトの内部状態について、外部から直接アクセスできないようにする場合は、インスタンス変数の名前を「__
」で始まるように変更すればよい。
class Person:
def __init__(self, name: str, age: int) -> None:
self.__name = name
self.__age = age
def show(self) -> None:
print(f'私の名前は{self.name}、{self.age}歳です!')
if __name__ == '__main__':
p = Person('山田太郎', 15)
print(p.__age) # エラー
隠蔽する際の注意点
- 完全に隠蔽できるわけではない
Pythonは「__
」付きのインスタンス変数を内部的にリネーム(具体的には、「_<クラス名><元の名前>」)しているだけで、アクセスそのものを制限しているわけではない
→ 「__
」をもってしても、インスタンス変数は完全に隠蔽されるわけではない
よって、例えば上の例であれば、
print(p._Person__age)
とすれば、隠蔽したはずのインスタンス変数にアクセスできてしまう。
- 設定は無視される
class Person:
def __init__(self, name: str, age: int) -> None:
self.__name = name
self.__age = age
def show(self) -> None:
print(f'私の名前は{self.name}、{self.age}歳です!')
if __name__ == '__main__':
p = Person('山田太郎', 15)
p.__age = 38
p.show() # 私の名前は山田太郎、15歳です
__age
が隠蔽されていると考えれば、p.__age = 38
でエラーになりそうだが実際はそうもならないし、p.show()
では元の __age
の値が返されてしまう。
→ __age
は内部的に名前が変化しているだけで、p.__age = 38
も新たな__age
を追加しているに過ぎない。しかし内部的には、__age
は _Person__age
なのでその値は反映されず無視されてしまう。
これは直観的でもなく、そうする意味でもなので、このようなコードを書くべきではない。
-
__age__
は隠蔽されない
Pythonでは、名前前後のアンダースコアは意味を持つので、通常の識別子として利用すべきではない。
そもそもどこまで厳密に隠蔽するか
どこまで厳密にインスタンス変数/メソッドを隠蔽するかは難しい。
実務上は、インスタンス変数/メソッドの先頭に「_
」だけをつけて、そのインスタンス変数/メソッドが内部用途であることを示す記述方法もよくみられる。(あくまで示すだけで紳士協定)
しかし、「__
」で完全に隠蔽できないなら、アクセスできないことを示すだけで充分であるし、その方がデバッグを行う際に、インスタンス変数/メソッドに簡単にアクセスできて便利という考え方もあるし、実務上はむしろこちらのが主流にも感じる。
アクセサーメソッド
アクセサーメソッド:真に内部用途の値は別にして、最低でも値を参照し、(必要に応じて)値を設定するのに必要な仕組み
→ 隠蔽されたインスタンス変数にアクセスするためのメソッド
class Person:
def __init__(self, name: str, age: int) -> None:
self.__name = name
self.__age = age
# nameのゲッター
def get_name(self) -> str:
return self.__name
# ageのゲッター
def get_age(self) -> int:
return self.__age
# nameのセッター
def set_name(self, value: str) -> None:
self.__name = value
# ageのセッター
def set_age(self, value: int) -> None:
if value <= 0:
raise ValueError('ageは正数で指定します。')
self.__age = value
def show(self) -> None:
print(f'私の名前は{self.get_name()}、{self.get_age()}歳です!')
if __name__ == '__main__':
p = Person('山田太郎', 15)
p.set_age(35)
print(p.print_age()) # 35
p.set_age(-15) # Error
上記の例であれば、get_name/get_ageが値取得のための、set_name/set_ageが値設定のためのメソッド。それぞれ区別してゲッターメソッド/セッターメソッド(あるいは単にゲッター/セッター)と呼ぶ場合もある。
変数名の先頭から、__
を取り除いて、代わりに「get_
」「set_
」を付与する。よって、インスタンス変数 __name
に対応するアクセサリーメソッドは、get_name
/ set_name
であり、__age
に対応するのは get_age
/ set_age
である。
アクセサーメソッドは「メソッド」なので、インスタンス変数の読み書きに際して任意の処理を加えることが出来る。この場合、設定のときだけでなく、取得に際して値を加工することも可能。内部的なデータの持ち方に変化があった場合も、ゲッターを介することで、呼び出し側に影響することなく、内部の実装だけを差し替え可能。あるいは、ゲッターだけを用意することで、インスタンス変数を読み取り専用にしたり、セッターのみにすることで書き込み専用にすることも出来る。
ただし、インスタンスの状態は変化しない方が扱いは簡単になる。無条件にゲッター/セッターをセットとしてとらえるのではなく、差し支えない場合はセッターを書かないという選択肢を取るべき。
プロパティ
アクセサーメソッドはカプセル化のための一時的手法であるが、十分ではない。値を出し入れするためのメソッド呼び出しは、冗長であり直観的でもない。
p.set_name('鈴木太郎')
は、p.name = '鈴木太郎'
と書けた方が、代入の意図が明確。
プロパティ:クラス内部ではメソッドのように表現できるが、外からは変数のようにアクセス出来るような仕組み
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
# プロパティ(値取得)
@property
def name(self) -> str:
return self.__name
@property
def age(self) -> int:
return self.__age
# プロパティ(値設定)
@name.setter
def name(self, value: str) -> None:
self.__name = value
@age.setter
def age(self, value: int) -> None:
if value <= 0:
raise ValueError('ageは正数で指定します。')
self.__age = value
def show(self) -> None:
print(f'私の名前は{self.get_name()}、{self.get_age()}歳です!')
if __name__ == '__main__':
p = Person('山田太郎', 15)
p.name = '鈴木次郎'
p.age = 35
print(p.name) # 鈴木次郎
print(p.age) # 35
プロパティの設定には、ゲッター/セッターから「get_」「set_」を取り除き、代わりに
・ゲッターには@propertyデコレーター
・セッターには@<名前>.setterデコレーター
を付与するだけ。
実体はメソッドでもかかわらず、呼び出し側では変数の代入/参照としてあらわせる点に注目。
property関数
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def get_name(self) -> setr:
return self.__name
def get_age(self) -> int:
return self.__age
def set_name(self, value: int) ->None:
self.name = value
def set_age(self, value: int) -> None:
if value <= 0:
raise ValueError('ageは正数で')
self.__age = value
def show(self) -> None:
print(f'私の名前は{self.name}、{self.age}歳です!')
name = property(get_name, set_name)
age = property(get_name, set_name)
あらかじめ作成したアクセサリーメソッドをproperty関数に渡すだけ。関数の戻り値を格納する変数(ここではname/age)がプロパティ名になる。
読み取り専用のプロぱていぃを設定したい場合は、セッターに関する引数を略記すればよい。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[編集追記予定]