45
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Python】propertyとついでにclassを完全に理解する

Last updated at Posted at 2021-12-30

はじめに

propertyを使った書き方を丸暗記だけしてよくわからないままclassを使っている人が多いですね。元をたどればclassをなんとなく使っているからです。ということで、中で何が起きているのかを徹底的に解説します!

2021/12/31 編集

  • インスタンスからクラス変数にアクセスするとき、コピーではなく参照を受け取っています。その点間違っていたので修正しました。

目次

  1. そもそもclassとは
  2. __get__メソッドについて
  3. __set__メソッドについて
  4. propertyとは
  5. よく見るpropertyの使い方

そもそもclassとは

propertyを理解するには、まずclassが何なのかを知る必要があります。
ほとんどの解説記事では、classは次のような構成で紹介されます。

class A:
    def __init__(self, value=0):
        """インスタンスを作成"""
        self.value = value

    def show(self):
        """値を表示"""
        print(self.value)

これによりshow関数は謎の「メソッド」というものとして定義され、classの定義はメソッドの集合体として認識されがちです。しかしこれはいったん忘れてください。

名前空間としてのclass

classの定義もあくまで1行(または1ブロック)ずつ実行しているにすぎません。次の例を見てください。

class A:
    a = 1
    def print_value(x):
        print("value =", x)
    print_value(a)

classの中であること以外、地の文と変わりません。これを実行すると、

value = 1

とちゃんとprintされます。違いは定義した変数aや関数print_valueにアクセスするときにAを介する必要があることです。

A.a  # [Out]: 1
A.print_value  # [Out]: <function __main__.A.print_value(x)>

このおかげで、classの外でa = 10のように同じ名前の変数を新しく定義しても混同されません。「ただのa」と「クラスAa」で区別できるので「名前空間を分ける」というふうに言われるわけです。

インスタンスの作成

上で定義したAは関数呼び出し可能です。

callable(A)  # [Out]: True

Aを関数呼び出ししたときの返り値は「Aのインスタンス」と呼ばれます。typeで型を確認するとAになっているので、型を定義したと解釈することもできます。

ins = A()
ins  # [Out]: <__main__.A at 0x...>
type(ins)  # [Out]: __main__.A

Aのインスタンスは、ピリオド.を介してAの中で定義した変数・関数すべてにアクセス可能です。しかし、このとき、アクセス先の変数によって挙動が異なります。もう一度Aの定義に戻りましょう。

class A:
    a = 1
    def print_value(x):
        print("value =", x)

インスタンスからアクセスできるものはaprint_valueです。前者は

ins.a  # [Out]: 1

となり、とても直観的ですが、後者は

ins.print_value(0)  # TypeError: print_value() takes 1 positional argument but 2 were given
ins.print_value()  # [Out]: value = <__main__.A object at 0x0000021CC6533DC0>

からわかるように、引数が1つ減っている代わりに、1つめの引数xins自身が渡されていることが分かります。このことはclassの基本として紹介されていますが、考えてみればPythonにおいては整数も文字列も関数もすべてobject型に属します。

isinstance(1, object)  # [Out]: True
isinstance(A.print_value, object)  # [Out]: True

なぜこのような違いが出てくるのでしょうか。

__get__メソッドについて

classA内の変数に、Aとは全く別物である、Aのインスタンスinsからアクセスすることは未定義です。これがPythonでどう定義されているのかがポイントです。

AのインスタンスinsはどのようにAから変数をもらっているのか。結論から言うと

  1. デフォルトでは変数の参照をもらう
  2. 変数に__get__メソッドがある場合は__get__メソッドを呼び出す

の2パターンです。

1. デフォルトでは変数の参照をもらう

int__get__メソッドを持ちません。

hasattr(0, "__get__")  # [Out]: False

したがって、ins.aA.aは同じオブジェクトを表します。

ins.a is A.a  # [Out]: True

ただし、ins.aに新しい値を代入しても、ins側で別のオブジェクトに更新されるだけで、A.aに影響はありません。

ins = A()
ins.a = 10
ins.a  # [Out]: 10
A.a  # [Out]: 1

ただし、参照渡しであるので、たとえばlistの中身は共有されます。

hasattr([], "__get__")  # [Out]: False

class B:
    x = [0]

ins_b = B()
ins_b.x  # [Out]: [0]
ins_b.x[0] = -1
ins_b.x  # [Out]: [-1]
B.x  # [Out]: [-1]  <-- 上書きされる!

2. 変数に__get__メソッドがある場合は__get__メソッドを呼び出す

まずは__get__が何なのかを知る必要があります。最も大事な、関数を例に説明します。

(1) 関数の__get__メソッド

Pythonでの関数はfunction型です。関数は、どこで定義されようとfunctionのインスタンスであり、__get__メソッドを持ちます。

def f(x): ...
type(f)  # [Out]: function
hasattr(f, "__get__")  # [Out]: True

type(A.print_value)  # [Out]: function
hasattr(A.print_value, "__get__")  # [Out]: True

Python内部での関数の__get__メソッドはPython実装ではないですが、真似すれば次のようになっています。

class function:
    def __get__(self, obj, objtype=None):
        def method(*args, **kwargs):
            return self(obj, *args, **kwargs) # selfが関数自身であることに注意
        return method

:warning: これはいろいろな意味で完全ではありません。あくまで説明のためです)

少しわかりづらいですが、自身の1つめの引数のみobjにした新しい関数methodを作成して返しています。

functionクラスを無理やりtypeで取得してこれを実行してみましょう。足し算する関数とfunctionクラスを用意します。

def add(x, y):
    return x + y

function = type(add)

整数1addメソッド1.addのようなものを用意します。

add_method = function.__get__(add, 1)

add_methodaddの1つめの引数に1が入った状態ですので、もう片方を指定すれば足し算が計算されます。

add_method(2)  # [Out]: 3
(2) class内での__get__の挙動

以上を念頭に考えます。さきほどから使っているclassAとそのインスタンスを使います。

class A:
    a = 1
    def print_value(x):
        print("value =", x)

ins = A()

関数print_valueがclassAで定義され、それがAのインスタンスinsからins.print_valueとアクセスされたとき、次のような作業が行われます。

  1. method = function.__get__(A.print_value, ins)によりメソッド関数methodが作成される。
  2. ins.print_valueの返り値がそのままmethodになる。

つまり、print_valueをそのままコピーする代わりに、print_valueには__get__が定義されているので、__get__の返り値(すなわち1つめの引数が指定された関数)をコピーの代わりにしているということです。

(3) __get__を使ってみる

よりシンプルに、"Hello!"を常にコピーとして渡してくる無能な型を作ってみましょう。

class Munou:
    def __get__(self, obj, objtype=None):
        return "Hello!"

この無能オブジェクトをclass内で定義すれば、アクセスしたときに常に"Hello!"が得られます。

class B:
    m = Munou()

ins = B()
ins.m  # [Out]: 'Hello!'

値へのアクセスが完全に制御できました。

__set__メソッドについて

__get__は値を受け取るときに呼び出されました。値を渡す(代入する)ときはどうでしょうか。これも同じような構成になっています。

  1. デフォルトではそのまま値を上書きする。
  2. 上書きしようとしている変数に__set__メソッドがある場合は__set__メソッドを呼び出す

intには__set__メソッドがないので、普通に上書きされます。

class B:
    i = 0

ins = B()
ins.i  # [Out]: 0
ins.i = 1
ins.i  # [Out]: 1 <-- 上書きされている

先ほどのMunou__set__を定義していないので、新しい値を与えると上書きされます。

class B:
    m = Munou()

ins = B()
ins.m = 0
ins.m  # [Out]: 0 <-- 上書きされている

そこで、上書きしないように、エラーを返すように書き換えます。

class Munou:
    def __get__(self, obj, objtype=None):
        return "Hello!"
    def __set__(self, obj, value):
        raise AttributeError

この無能オブジェクトを使うと、上書きins.m = 0の際にMunou.__set__(B.m, ins, 0)が呼び出されます。この例では、無能なうえに上書きが阻止されます。

class B:
    m = Munou()

ins = B()
ins.m = 0  # [Out]: AttributeError
ins.m  # [Out]: 'Hello!'

propertyとは

以上のように__get____set__は値の取得・上書きを制御するのに使えることが分かりました。これをうまく使えば、たとえば

  • 格納された値を使いやすい形で返してくれるように__get__を定義する。
  • 値の更新の際、型や値をチェックしてから更新するように__set__を定義する。
  • 値の更新を禁止するために__set__でエラーを吐くようにする。

のようなことができます。

しかし問題は、毎回新しいクラスを定義するのは面倒だということです。メインのクラスであるBとは別に、__get____set__を有したMunouクラスを定義しましたが、そもそもBにしか使わないのにわざわざMunouを別で用意するといった大掛かりな作業は避けたいです。

ここで出てくるのが、標準実装のpropertyクラスです。

propertyクラスの実装

propertyクラスはPythonで真似れば次のような実装になっています。

class property:
    def __init__(self, fget=None, fset=None):
        self.fget = fget # ゲッター関数(なくてもよい)
        self.fset = fset # セッター関数(なくてもよい)

    def getter(self, fget):
        """ゲッター関数を更新した新たなpropertyを返す"""
        return property(fget=fget, fset=self.fset)

    def __get__(self, obj, objtype=None):
        if self.fget is None:
            raise AttributeError("unreadable attribute") # ゲッター関数がなければエラー
        return self.fget(obj) # ゲッター関数を呼び出す

    def setter(self, fset):
        """セッター関数を更新した新たなpropertyを返す"""
        return property(fget=self.fget, fset=fset)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute") # セッター関数がなければエラー
        return self.fset(obj, value) # セッター関数を呼び出す

:warning: これもいろいろな意味で完全ではありません。あくまで説明のためです)

つまり、propertyのインスタンスpropを何かのクラスに定義し、適宜propにゲッター関数やセッター関数を与えているとすると、インスタンスinsからpropにアクセスするとき、

  • ins.propでは、ゲッター関数があればそれを呼び出し、なければ書き込み専用ということでエラーを返す。
  • ins.prop = 0では、セッター関数があればそれを呼び出し、なければ読み取り専用ということでエラーを返す。

というルールに従うということです。したがって「__get____set__を持ったクラスを定義する」代わりに「ゲッター関数とセッター関数を定義する」ことでほぼ同じ仕組みが実現できることになります。

propertyの利用

それでは、自己紹介と、セットしようとした値の説明をするだけのpropertyを作ります。ゲッター関数とセッター関数を定義します。

def getter_func(self):
    print(f"I am {self}")

def setter_func(self, value):
    print(f"This is {value}")

これらをもちいてpropertyインスタンスを作成します。

prop = property(getter_func, setter_func)
prop  # [Out]: <property at 0x...>

ゲッター関数とセッター関数を別々でセットするのであれば次のコードで同じpropが得られます。

prop0 = property(getter_func)
prop = prop0.setter(setter_func)
prop  # [Out]: <property at 0x...>

このpropertyのインスタンスpropをclassに渡します。

class C:
    prop = prop

すると値のアクセス/代入をするたびに対応する関数が呼び出されます。

c = C()
c.prop  # [Out]: I am <__main__.C object at 0x...>
c.prop = 0  # [Out]: This is 0

よく見るpropertyの使い方

さて、ようやくいつもの使い方が出てきます。ポイントは

  1. ゲッター関数・セッター関数はどうせクラス外部から使わないので、クラス内で定義する。
  2. propertyのメソッド、__init__setterは単一の関数を引数にとれるので、できるだけデコレータを使う。

の2点です。

デコレータのとは

関数またはクラスを引数にとり、引数1つで動く関数は、デコレータ表記ができます。たとえばlistに関数を追加するとき、

def func():
    return 0

function_list.append(func)

@function_list.append
def func():
    return 0

はほぼ等価です。後者ではfuncにはfunction_list.append(func)の返り値が格納されるので、funcNoneになっているという違いだけです。

デコレータを使ったpropertyの定義

以上から、property__init__, getter, setter関数はすべてデコレータ表記できます。したがって

def getter_func(self):
    print(f"I am {self}")

def setter_func(self, value):
    print(f"This is {value}")

prop0 = property(getter_func)
prop = prop0.setter(setter_func)

@property
def getter_func(self):
    print(f"I am {self}")

@getter_func.setter
def setter_func(self, value):
    print(f"This is {value}")

はほぼ等価です。そして、getter_funcはゲッター関数だけ定義された中間産物で不要なので、すべて同じ変数名propで問題ないです。

@property
def prop(self):
    print(f"I am {self}")

@prop.setter
def prop(self, value):
    print(f"This is {value}")

このように、はじめの方法で出てきた不要な変数名getter_func, setter_func, prop0を書かずにpropを定義できたので、デコレータを使った方法が良いとされています。これをクラス内でやればOKです。

class C:
    @property
    def prop(self):
        print(f"I am {self}")

    @prop.setter
    def prop(self, value):
        print(f"This is {value}")

まとめ

以上のようにpropertyというのは、Pythonでのクラスとインスタンスのアクセス関係を利用して、読み取り専用・書き込み専用のような変数を可読性高く簡単に定義するためのものでした。この仕組みが分かっていれば無用なトラブルシューティングが避けられますね。

また、__get__/__set__はPythonで魔法のような(直観的で使いやすいが一見Pythonの範疇を超えているかのように動作する)クラスを作るときに非常に有用です。ぜひ使えるようにしたいメソッドです。

45
51
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?