次に「プライベートな属性」についても話をしておこう。「プライベート」とは「内部で使うだけなので外部には見せたくない」という意味だ。これには幾つかの種類がある。モジュールやパッケージの内部で使うだけなので「from モジュール名 import *」で自動的にインポートされないようにしたい場合にどうすればよいかは、第23回「モジュールの作り方」の「公開したくないもの」や第24回「パッケージ」の「__init__.pyファイルと__all__変数とパッケージ内インポート」で取り上げた。
クラスを定義する際にも、メソッドが長くなったのでその内容を幾つかのメソッドに分割したり、複数のメソッドで共通に使う処理として取り出したりして、クラス内でのみ使用することが前提となるメソッドが存在するかもしれない。あるいは、クラス内でのみ保持しておき、外部からはそのクラスやインスタンスの属性として自由に触ってほしくはないデータもあるかもしれない。
そうしたメソッドやデータ(インスタンス変数やクラス変数)はクラスを利用する側から不用意に扱ってもらっては困る。Python以外のプログラミング言語では、そうした属性があるときには、実際にアクセスができない(メソッドを呼び出そうとしたり、インスタンス変数にアクセスしようとしたりするとエラーとなる)ようにする仕組みがある。だが、基本的にPythonにはそうした仕組みは存在しない。そこで、メソッドや変数の名前をアンダースコア「_」で始めることで、「これはプライベートな属性なので使わないようにしてね」ということを、クラスの利用者に伝えるようになっている。
以下に例を示す。
class Foo:
def __init__(self, name):
self._name = name # _nameはプライベート
def get_name(self):
return self._name
def set_name(self, new_name):
self._name = new_name
def show_attr(self):
print(dir(self))
このコードでは、インスタンス変数が「_name」とアンダースコア「_」で始まっているので、「_nameには直接触らないでね」ということを利用者に伝えている。そして、get_nameメソッドとset_nameメソッドを用意することで、直接アクセスせずにそれらのメソッドを使ってその値を取得したり、変更したりできるようにしている。
これは単純なコードなので、こうすることのメリットが分からないかもしれない。だが、このように属性に直接アクセスさせるのではなくメソッドを経由させることで、インスタンス変数「_name」の値を変更しようとしたときに、その値が適切かを調べたり、その値を取得しようとしたときに画面に表示しやすいように書こうしたりといった処理を挟み込めるようになる。
このようなクラスを利用するときには、「プライベートな属性には直接アクセスしない」という「紳士協定」を順守して、次のようなコードを書く。
foo = Foo('deep insider')
print(foo.get_name()) # Fooクラスが用意したメソッドを使ってデータを取得
foo.set_name('atmarkit') # Fooクラスが用意したメソッドを使ってデータを書き換え
print(foo._name) # だが、このようにして直接アクセスもできてしまう
このコードの実行結果を以下に示す。
「print(foo._name)」行がエラーとなっていないことからも、これがあくまでも「紳士協定」でしかないことが分かるはずだ。実際、以下のコードを実行すると、インスタンスfooの属性として「_name」も含めて表示される(実行結果は省略)。
foo.show_attr()
こうした「紳士協定」破りを避けるために、もう少し強力な仕組みも用意されている。それは、2つのアンダースコア「__」で名前を始めることだ。これにより、クラスの外部からは「__名前」のようにしてアクセスできないようになる。例を見てみよう。以下は先ほどのコードで「_name」だったところを全て「__name」としたものだ。
class Foo:
def __init__(self, name):
self.__name = name # __nameはプライベート
def get_name(self):
return self.__name
def set_name(self, new_name):
self.__name = new_name
def show_attr(self):
print(dir(self))
これを利用するコードも先ほどと同様だ(「_name」を「__name」に変更している)。
foo = Foo('deep insider')
print(foo.get_name()) # Fooクラスが用意したメソッドを使ってデータを取得
foo.set_name('atmarkit') # Fooクラスが用意したメソッドを使ってデータを書き換え
print(foo.__name) # エラーとなる
これを実行すると次のように最後の「print(foo.__name)」行でエラーが発生する。
これは本当に「__name」がクラスの外部に対して隠されたのではなく、外部からは「_クラス名__インスタンス変数名」(この場合は「_Foo__name」)という名前で見えるようになっていることから起きたものだ(このように属性の名前を変更することを「名前マングリング」または単純に「マングリング」と呼ぶ)。以下のコードを試してみよう。
foo.show_attr()
実行結果を以下に示す。
「__name」という属性がなく、「_Foo__name」という属性があるのが分かるはずだ(赤枠内)。「__name」を使わずに、この属性名を使えば、無理やりに属性に直接アクセスすることは可能だ(興味のある方は試してみよう)。だが、これはクラスを定義した側が外部に公開する属性名を変えるほどに強力に「プライベートだ!」といっているものを、無理やりにアクセスすることはお勧めできない(もちろん、1つのアンダースコアで始まる属性に直接アクセスすることもお勧めはしない)。
特に、上で述べたように属性にアクセスする際に何らかのロジックを挟み込んでいるような状況では、その処理をスキップしてしまうことになり、プログラムの整合性が取れなくなる可能性もある。プライベートだと示されている属性は、それにアクセスするための公式な手段(インタフェース)を用いて利用するようにしよう(そのために便利に使える「プロパティ」という仕組みもある。これについてはこの後で取り上げる)。
なお、「__init__」などの特殊メソッドについては、こうしたマングリングの対象ではないことにも注意しよう。Pythonのドキュメント「プライベート変数」には「__spam (先頭に二個以上の下線文字、末尾に一個以下の下線文字) という形式の識別子は、 _classname__spam へとテキスト置換される」とある。プライベートな属性には「__name」や「__name_」などの名前を付けるようにしよう。
プライベートな属性にはアンダースコアを付けて、それにアクセスするにはそのためのメソッド(インタフェース、API)を用意することは分かった。だが、それは以下のような単純で分かりやすく、コードも短くなる書き方を許さないということでもある。
foo = Foo()
foo._some_value = ……
print(foo._some_value)
そこで、このような書き方をしながらも、上で述べたメソッド経由でプライベートな属性にアクセスするための仕組みが用意されている。それが「プロパティ」と呼ばれるものだ。クラスにプロパティを持たせるには、組み込みの「property関数」で読み取り、変更、削除するためのメソッドと、プロパティの説明(docstring)を指定する。
property(fget=None, fset=None, fdel=None, doc=None)
プロパティの値をfgetに渡されたメソッドで取得、fsetに渡されたメソッドで変更、fdelに渡されたメソッドで削除可能な「プロパティ属性」を返す。docに渡された文字列はそのプロパティを説明するdocstringとなる。クラス定義内でproperty関数の戻り値をクラス変数に代入すると、それがプロパティにアクセスするためのインタフェースとして利用できるようになる。
パラメーター | 説明 |
---|---|
fget | プロパティの値を取得するのに使用するメソッド |
fset | プロパティの値を設定するのに使用するメソッド |
fdel | プロパティを値を削除するのに使用するメソッド |
doc | プロパティのdocstring |
property関数のパラメーター |
例えば、0〜100の範囲の整数しか設定できないプロパティを作成してみよう。ここではプロパティの削除とdocstringは省略する
class Foo:
def __init__(self):
self.__mynum = 0
def get_num(self):
return self.__mynum
def set_num(self, new_value):
if 0 < new_value < 101:
self.__mynum = new_value
else:
raise ValueError('value out of range(0-100)')
mynum = property(get_num, set_num)
このクラスでは、プロパティの名前を「mynum」として、その値を取得するメソッド(これを「getter」「ゲッター」と呼ぶことがある)として「get_num」を、その値を設定するメソッド(これを「setter」「セッター」と呼ぶことがある)として「set_num」を定義して、それらのメソッドの中ではプライベートなインスタンス変数「__mynum」を操作するようにしている。
セッターであるset_numメソッドには、プロパティの新しい値を受け取るためのパラメーター(ここでは「new_value」)もあることに注意しよう。そして、セッターの中ではパラメーターnew_valueの値を調べて、それが0〜100の範囲であれば、それをプロパティの新しい値としてプライベートなインスタンス変数「__mynum」に代入している。そうでなければ、「raise ValueError('value out of range(0-100)')」としてエラー(ValueError例外)を発生させている(なお、例外については後続の回で取り上げる)。
これらのメソッドをproperty関数に渡して(このとき、selfはいらないことに注意。あくまでもクラス内で定義した関数≒メソッド自体を関数の引数としている)、その戻り値をプロパティを使用するインタフェースとなるクラス変数mynumに代入している。
利用する側では、インスタンスに続けてインタフェースとなる「mynum」に代入をしたり、その値を読み取ったりするだけでよい。以下に利用例を示す。
foo = Foo()
foo.mynum = 50 # mynumをインタフェースとして__mynumの値を変更
print(foo.mynum) # mynumをインタフェースとして__mynumの値を取得
foo.mynum = 101 # 範囲外なのでエラーとなる
これを実行すると次のようになる。
通常の属性と同じように代入や読み取りを行いながらも、実際のデータはプライベートな属性に保持して、想定外の値が代入されたらエラーとするような処理がこれで実現できるのが分かるはずだ。
今回は、クラスの定義とスコープや名前空間の関係と、プライベートな属性の取り扱い、プロパティを使ってプライベートな属性は外部に見せずに、だが簡便な記法でその値を読み書きする方法を見た。次回はクラスの最後の話題として「多重継承」を取り上げよう。
Copyright© Digital Advantage Corp. All Rights Reserved.