[Python入門]クラスのスコープとプライベートな属性Python入門(2/2 ページ)

» 2019年08月23日 05時00分 公開
[かわさきしんじDeep Insider編集部]
前のページへ 1|2       

プライベートな属性

 次に「プライベートな属性」についても話をしておこう。「プライベート」とは「内部で使うだけなので外部には見せたくない」という意味だ。これには幾つかの種類がある。モジュールやパッケージの内部で使うだけなので「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」とアンダースコア「_」で始まっているので、「_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()

インスタンスfooの属性を表示

 こうした「紳士協定」破りを避けるために、もう少し強力な仕組みも用意されている。それは、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」を「__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()

インスタンスfooの属性を調べる

 実行結果を以下に示す。

実行結果 実行結果

 「__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関数

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を持つクラスの定義

 このクラスでは、プロパティの名前を「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  # 範囲外なのでエラーとなる

プロパティmynumを使用するコード例

 これを実行すると次のようになる。

実行結果 実行結果

 通常の属性と同じように代入や読み取りを行いながらも、実際のデータはプライベートな属性に保持して、想定外の値が代入されたらエラーとするような処理がこれで実現できるのが分かるはずだ。

まとめ

 今回は、クラスの定義とスコープや名前空間の関係と、プライベートな属性の取り扱い、プロパティを使ってプライベートな属性は外部に見せずに、だが簡便な記法でその値を読み書きする方法を見た。次回はクラスの最後の話題として「多重継承」を取り上げよう。

今回のまとめ:

  • 通常の関数と同様に、クラスのメソッドからは「ローカルスコープ」「グローバルスコープ」「ビルトインスコープ」に存在する名前が見える
  • クラス内で定義したメソッドなどの名前は、そのクラス用の名前空間に記録される
  • クラスのインスタンスを生成すると、そのクラスの名前空間にある名前(と基底クラスの名前)がその属性として利用できるようにセットアップされる
  • クラスの継承時には、派生クラスで定義した名前により、基底クラスで定義した名前が上書きされることがある(オーバーライド)
  • 基底クラスのメソッドからメソッドを呼び出すと、派生クラスでオーバーライドしたメソッドが呼び出されることに注意
  • プライベートな属性は1つのアンダースコア「_」か2つのアンダースコア「__」で始めることで、それがプライベートであることを利用者に伝える
  • 2つのアンダースコア「__」で始まる名前の属性は、外部からは「_クラス名__属性名」という名前で見えるようにマングリングされる
  • クラスの利用者はプライベートな属性には直接アクセスせずに、何らかのインタフェースを利用するという「紳士協定」を順守する
  • プライベートな属性を持つ一方で、簡便な記述でそのデータを読み書きするための「プロパティ」という仕組みがある

前のページへ 1|2       

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。