TIL

Today I Learned. 知ったこと、学んだことを書いていく

クラスでプロパティを使う - Python

入門 Python 3 に記載されていたプロパティについてまとめてみた。忘れてもいいようにメモとして残しておく。


Pythonではすべての属性、メソッドが公開となっている。もし、属性を非公開をしたいときにはプロパティという機能を使う。まずはproperty()メソッドを使った実装方法を見てみる。

propertyメソッド

例として、hidden_nameという属性を持つPersonクラスを定義する。hidden_nameは外部から直接アクセスしないようにしたいとする。

class Person():

    def __init__(self, input_name):
        print('get_name!')
        self.hidden_name = input_name

    def get_name(self):
        print('get_name!')
        return self.hidden_name

    def set_name(self, input_name):
        self.hidden_name = input_name

    name = property(get_name, set_name)

name = property(get_name, set_name)という行で「get_name()をnameというプロパティのgetter」として、「set_name()をnameというプロパティのsetter」として定義されている。propertyメソッドの第一引数にgetter、第二引数にsetterを渡す。


実際に使ってみる。まずはgetterから。

>>> person = Person('taro')
>>> person.name
get_name!
'taro'
>>> person.get_name()
get_name!
'taro'

nameプロパティにアクセスすると、get_name()が呼び出されるようになっている。また、get_name()を直接呼び出すこともできる。

次はsetter

>>> person.name = 'jiro'
set_name!
>>> person.name
get_name!
'jiro'
>>> person.set_name('saburo')
set_name!
>>> person.name
get_name!
'saburo'

getterと同様に、nameプロパティに代入をすると、set_name()が呼び出されている。また、直接set_name()を呼び出すこともできる。次に、デコレータを使った実装方法を見てみる。


デコレータ

name = property(get_name, set_name)と記述していた部分はデコレータでも記述できる。同じメソッド名に@property@プロパティ名.setterというデコレータをつける

@property:getterのメソッドにつける
@プロパティ名.setter:setterのメソッドにつける。例)name.setter

デコレータを使ってプロパティを定義

class Person():

    def __init__(self, input_name):
        self.hidden_name = input_name

    @property
    def name(self):
        print('get_name!')
        return self.hidden_name

    @name.setter
    def name(self, input_name):
        print('set_name!')
        self.hidden_name = input_name

使ってみる

>>> from main import Person
>>> person = Person('tata')
>>> person.name
get_name!
'tata'
>>> person.name = 'kaka'
set_name!
>>> person.name
get_name!
'kaka' 

get_name()set_name()が定義されていない。また、hidden_nameには外部から直接アクセスできる。それについてはまたあとで。

また、プロパティは必ずしも属性の値を返さなくても良い。計算した結果を返しても良い。以下のようにCircle(円)クラスにradius(半径)という属性とdiameter(直径)というプロパティを定義する。

# 円クラス
class Circle():
        
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return 2 * self.radius
>>> c = Circle(5)
>>> c.radius
5
>>> c.diameter
10

radiusの値を元に結果を返している。

diameterはその時のradiusの値を元に結果を返しているため、変更した場合には違う値が返ってくる。

>>> c.radius = 4
>>> c.radius
4
>>> c.diameter
8

プロパティのsetterを定義しなかった場合、それは読み取り専用のプロパティになる。

プロパティを使うことで、プロパティのメソッド(@property@xxx.setterのメソッド)内の処理を変えても呼び出す側のソースは変える必要がなくなる。



非公開な属性

プロパティを使うだけでは外部からは直接アクセスできないようにはなっていない。Pythonには属性を非公開にするための命名規則がある。

非公開にしたい属性の変数名の先頭にアンダーバーを2つつける(__)だけでよい。Personクラスのhidden_name属性を非公開属性に変えてみる。

class Person():

    def __init__(self, input_name):
        self.__name = input_name

    @property
    def name(self):
        print('get_name!')
        return self.__name

    @name.setter
    def name(self, input_name):
        print('set_name!')
        self.__name = input_name
>>> person = Person('taro')
>>> person.name
get_name!
'taro'
>>> person.name = 'jiro'
set_name!
>>> person.name
get_name!
'jiro'

前と同じように属性にアクセスすることはできている。しかし、__nameに直接アクセスはできなくなっている。

>>> person.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute '__name'

完全には非公開にはなっていない。Pythonの機能のマングリング(ぐちゃぐちゃに変形すること)によって偶然直接呼び出してしまわないようになっている。マングリング後には次のようになっている。

>>> person._Person__name
'jiro'

getterとして呼び出していないということがわかる(get_name!と出力されていない)。完全ではないが、簡単には直接アクセスできないようになっている。

以上。

また、プロパティについて何か新しい発見があった場合にはここに追記していく。

2017/10/9 追記

Pythonでの日付について学習中にdatetimeモジュールのtimeクラスのソースを見ていたときに、プロパティを使っていたため、どのように使っていたかをメモしておく。

読み取り専用として使っていた。

Pythonのdatetime.timeのソースでのプロパティの記述

@hour.setterがついたhour()メソッドがないため読み取り専用として扱われる。

あと、Pythonのソースがデコレータを使っているからデコレータを使うのがPython的な書き方なのかも?

また、self._hourの部分でアンダースコアが一つになっているのが少し気になった。アンダースコアが1つしかついていない属性は習慣的に参照しないということらしい。また、アンダースコアが2つ前についている属性は完全に参照できなくなる。ということらしい。「1つは習慣的に参照しない」、「2つは文法的に参照できない」ということ!!



参考文献

【備忘録】Pythonにおけるアンダースコア"_"の役割について - Qiita


入門 Python 3

入門 Python 3