カプセル化

林です。

あるプログラミング言語がオブジェクト指向(object-oriented)であるためには下記の三つの要件を満たしている必要がある。

1.カプセル化(encapsulation)
2.継承(inheritance)
3.多態性(polymorphism)

多態性は継承と不可分の概念なので上記の2と3は1個としてカウントしても良いのかもしれない。

昨年末からChainerを独習しているので、好む好まざるに関わらずPythonでコードを書く機会が増えた。

Pythonも含めたインタプリタ方式のオブジェクト指向プログラミング言語を用いて上記のカプセル化を実装する時に何とも言い難い苦痛をしばしば感じることがある。

カプセル化を乱暴に説明すると、「オブジェクトの持つ状態を隠蔽し、その状態への手続きは公開されている振る舞いを呼び出して実現する」ということだ。

故に、オブジェクト指向プログラミング言語では何よりもまず状態を隠蔽する機能が必須となる。

ところが、Pythonも含めたインタプリタ方式のオブジェクト指向プログラミング言語では状態を完全に隠蔽できないものが多い。

以下に、JavaとPythonを用いてカプセル化のコードを記述してみる。

オブジェクトの「状態」と「振る舞い」の呼び方は言語によって異なるので、ここでは「状態」を「インスタンス変数」、「振る舞い」を「インスタンスメソッド」と呼ぶことにする。

先ずは、Javaでカプセル化を検証する。

下記は、Javaでカプセル化を実装したLifeクラス(Life.java)、そしてそれを実行するためのテストドライバ(Foo1.java)、および、その実行結果だ。

Lifeクラスには年齢というオブジェクトの状態を保持するインスタンス変数ageが定義されているが、privateで修飾されているのでオブジェクトの外部から直接的に値を取得したり変更したりすることは出来ない。

Lifeクラスには、インスタンス変数ageから年齢を取得するためのインスタンスメソッドgetAgeと、インスタンス変数ageに年齢を設定するためのインスタンスメソッドsetAgeが定義されているので、オブジェクトの外部からはこの二つのインスタンスメソッドを呼び出すことにより間接的にインスタンス変数ageへのアクセスが可能となる。

インスタンスメソッドsetAgeは引数ageに渡された年齢が0未満の場合はインスタンス変数ageへの年齢の設定は行わず、0以上の場合はインスタンス変数ageへの年齢の設定を行う。

テストドライバの21行目でインスタンスメソッドsetAgeを呼び出してインスタンス変数ageに-1という負の数の年齢を設定しているが、実行結果を見ると実際には負の数の年齢がインスタンス変数ageに設定されていないことが判る。

下記は、インスタンス変数ageにオブジェクトの外部から直接年齢を設定しているテストドライバ(Foo2.java)、および、そのコンパイル結果だ。


インスタンス変数ageはprivateで修飾されているので、オブジェクトの外部から直接年齢を設定している20行目に対してJavaコンパイラがコンパイルエラーを出している。

以上のことから、Javaではインスタンス変数(オブジェクトの状態)を完全にカプセル化できることが確認できた。

次に、Pythonでカプセル化を検証する。

似たようなことの繰り返しになるが我慢してお付き合い頂きたい。

下記は、Pythonでカプセル化を実装したLifeクラス(life.py)、そしてそれを実行するためのテストドライバ(foo1.py)、および、その実行結果だ。



Lifeクラスには年齢というオブジェクトの状態を保持するインスタンス変数__ageが定義されているが、変数名の先頭2文字が_(アンダースコア)なので、これがPythonでは隠蔽された(privateな)インスタンス変数ということになるらしい。

Lifeクラスには、インスタンス変数__ageから年齢を取得するためのインスタンスメソッドget_ageと、インスタンス変数__ageに年齢を設定するためのインスタンスメソッドset_ageが定義されているので、オブジェクトの外部からはこの二つのインスタンスメソッドを呼び出すことにより間接的にインスタンス変数__ageへのアクセスが可能となる。

インスタンスメソッドset_ageは引数ageに渡された年齢が0未満の場合はインスタンス変数__ageへの年齢の設定は行わず、0以上の場合はインスタンス変数__ageへの年齢の設定を行う。

テストドライバの19行目でインスタンスメソッドset_ageを呼び出してインスタンス変数__ageに-1という負の数の年齢を設定しているが、実行結果を見ると実際には負の数の年齢がインスタンス変数__ageに設定されていないことが判る。

下記は、インスタンス変数__ageにオブジェクトの外部から直接年齢を設定しているテストドライバ(foo2.py)、および、その実行結果だ。


インスタンス変数__ageは変数名の先頭2文字が_(アンダースコア)なので隠蔽された(privateな)インスタンス変数ということなるはずなのだが、オブジェクトの外部から直接年齢を設定しようとしている18行目に対してPythonインタプリタは実行エラーを出すことなくプログラムを正常終了させている。

しかし、20行目でインスタンスメソッドget_ageを呼び出して取得した年齢を21行目で表示させた実行結果を見るとインスタンス変数__ageには-1が設定されておらず0のままなので、一見するとインスタンス変数__ageがカプセル化されているようなのだが、色々調べてみるとこの挙動には一寸した「からくり」があることが判った。

Pythonの言語仕様では、先頭2文字が_(アンダースコア)で始まるインスタンス変数の本名は「_クラス名__任意」という具合に「アンダースコア1つ+クラス名+アンダースコア2つ+任意」となる。

クラスの中で「__任意」と書いた場合、Pythonインタプリタが暗黙的に「_クラス名__任意」に読み換えてくれる。

従って、上記テストドライバの18行目で-1を設定しているインスタンス変数__ageはLifeクラスの中で定義されている__ageとは同一ではない。

Pythonインタプリタによって、Lifeクラスに定義されている__ageは_Life__ageに暗黙的に読み換えられているのである。

故に、上記テストドライバの20行目でインスタンスメソッドget_ageを呼び出して取得した年齢を21行目で表示させた実行結果は-1ではなく0のままなのである。

それでは、上記テストドライバの18行目で-1を設定しているインスタンス変数を__ageではなく_Life__ageと書くとどのような実行結果になるのだろうか?

下記は、インスタンス変数_Life__ageにオブジェクトの外部から直接年齢を設定しているテストドライバ(foo3.py)、および、その実行結果だ。


20行目でインスタンスメソッドget_ageを呼び出して取得した年齢を21行目で表示させた実行結果を見るとインスタンス変数__age(厳密には_Life__age)には-1が設定されてしまっている。

以上のことから、Pythonではインスタンス変数(オブジェクトの状態)を完全にはカプセル化できないことが確認できた。

これは完全なカプセル化を実現できるオブジェクト指向プログラミング言語に慣れたプログラマにとってはかなりの苦痛を強いられることになる。