LoginSignup
68

More than 5 years have passed since last update.

Pythonのプロパティ記述はよく見るとおかしい(さらによく考えればおかしくない)

Last updated at Posted at 2017-07-26

はじめに

Pythonに関する説明を書く機会があって、プロパティに関する説明を書こうとした際にふと、

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

「あれ?」これどう説明したらいいんだ?と思いました。

おかしな点

さて、どこがおかしいのでしょうか。ところでPythonってオーバーロードありましたっけ?

そう、以下の個所は見た感じ、name(self)とname(self, name)の「同じ名前で引数の数が異なる」メソッドが定義できているように見えます。

@property
def name(self):
    return self._name

@name.setter
def name(self, name):
    self._name = name

が、Pythonではそんなことはできません。同じ名前のメソッドを定義したら後で定義した方で上書きされます(より正確にはメソッド名の属性が後で定義した方の関数オブジェクトを指すようになる)

もちろん、上記の2つのメソッドは両方使われます。printを仕込んでおけば各メソッドが実行されることがわかります。

後ついでに言うと、@name.setterという書き方もどう説明したものか悩みました。何故なら、プロパティによりnameの部分は変わるから。

からくり

デコレータ

では謎解きです。まず、デコレータのおさらいから。

@property
def name(self):
    return self._name

という記述は以下とほぼ同じです。

def name(self):
    return self._name
name = property(name)

つまり、3行目の右辺のnameは関数オブジェクトですが、左辺で代入が終わった後のnameはpropertyオブジェクトです。

書き方が気になりますが以下の記述も同じ発想で変換してみましょう。

@name.setter
def name(self, name):
    self._name = name

こんな感じになります。

def name_setter(self, name):
    self._name = name
name = name.setter(name_setter)

ダウト(笑)。しれっとメソッド名をname_setterにしていますがこれは3行目がちゃんと実行できるようにするためです。実際、公式ドキュメントのデコレータの説明部分を見ると、

ただし、前者のコードでは元々の関数を func という名前へ一時的に束縛することはない、というところを除きます。

となっています。つまり、デコレータを使った場合、nameというメソッドが定義されるわけではない(@propertyで定義されたnameという名前は上書きされない)ということになります。

まとめると、

  1. name(propertyオブジェクト)のsetterメソッドを呼んで
  2. その戻り値をまた改めてnameに設定

ということになります。

property.setter

さて後はpropertyクラスのsetterメソッドってなんだということがわかれば解決です。その答えを知るためにはPython実装に踏み込む必要があります。propertyクラスの実装はObjects/descrobject.cにあります。実装の詳細は省略して要点だけ貼ると、

static PyObject *
property_setter(PyObject *self, PyObject *setter)
{
    return property_copy(self, NULL, setter, NULL);
}

これがpropertyクラスのsetterメソッドに対応するC実装です。
で、property_copy。

static PyObject *
property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del)
{
    propertyobject *pold = (propertyobject *)old;
    PyObject *new, *type, *doc;

    type = PyObject_Type(old);
    if (type == NULL)
        return NULL;

    if (get == NULL || get == Py_None) {
        Py_XDECREF(get);
        get = pold->prop_get ? pold->prop_get : Py_None;
    }
    if (set == NULL || set == Py_None) {
        Py_XDECREF(set);
        set = pold->prop_set ? pold->prop_set : Py_None;
    }
    if (del == NULL || del == Py_None) {
        Py_XDECREF(del);
        del = pold->prop_del ? pold->prop_del : Py_None;
    }
    if (pold->getter_doc && get != Py_None) {
        /* make _init use __doc__ from getter */
        doc = Py_None;
    }
    else {
        doc = pold->prop_doc ? pold->prop_doc : Py_None;
    }

    new =  PyObject_CallFunctionObjArgs(type, get, set, del, doc, NULL);
    Py_DECREF(type);
    if (new == NULL)
        return NULL;
    return new;
}

少し長いですがやっていることを要約すると、

  1. 引数でget, set, delが指定されてなかったら元の定義を取り出し
  2. 取り出したものと今引数で指定されたものを使って改めてpropertyオブジェクトを作って返す

ということをしています。これにより、返されたpropertyメソッドはゲッターとセッターが定義されているということになります。

なお、「というわけでpropertyオブジェクトが設定されることはわかったが、じゃあこれがどう動いて参照・代入が行えるようになってるの?」という疑問については非常に優秀な記事があるのでそちらをご参照ください。

まとめ

話を要約します。

  • 疑問
    • プロパティの記述ってオーバーロードっぽく見えるけどPythonそんなことできたっけ?
    • セッターの@プロパティ名.setterってどういうことだ
  • 回答
    • Pythonにオーバーロードはない。デコレータが指定されている場合、実際にはそういう名前の関数が作られるわけではない(関数オブジェクトを指す属性名が定義されるわけではない。つまり、上書きも起こらない)
    • @プロパティ名.setterはpropertyクラスのメソッドで、すでに定義されているゲッターとデコレータで指定しているセッターを含んだ新しいpropertyオブジェクトが返される

余談

ところで、元々の書こうとしたプロパティの説明ですが、

これを説明するには君らはまだPythonを知らなすぎる

みたいな感じにお茶を濁しました(笑)

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
68