Python におけるプロパティ定義を短く書く

# 普通に ```@property``` でプロパティ定義するコードは長い

Python では以下のように property デコレータを使うことでプロパティを定義することができます。
例として、名前 (name) とメールアドレス (email) をプロパティに持つ User クラスは以下のように定義することができます。

```python
class User:

    def __init__(self, *, name=None, email=None):
        self.__name = name
        self.__email = email

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

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

    @property
    def email(self):
        return self.__email

    @email.setter
    def email(self, email):
        self.__email = email
```

プロパティの値の取得は ```user.name```、値の設定は```user.name = "たろう"``` のように書くことができます。

```python
user = User(name="taro", email="taro@example.com")
print(user.name) # => "taro"
user.name = "たろう"
print(user.name) # => "たろう"
```

User クラスを使う側に面倒だと感じる部分はありませんが、User クラスを定義する側の視点に立つと、似たようなコードをプロパティの数だけ書く必要があるため面倒です。

# プロパティを定義するヘルパー関数を作成する

そこで以下のヘルパー関数を定義します。

```python
def define_property(self, name, value=None):
    # "_User__name" のような name mangling 後の名前.
    field_name = "_{}__{}".format(self.__class__.__name__, name)

    # 初期値を設定する.
    setattr(self, field_name, value)

    # getter/setter を生成し, プロパティを定義する.
    getter = lambda _: getattr(self, field_name)
    setter = lambda _, value: setattr(self, field_name, value)
    setattr(self.__class__, name, property(getter, setter))
```

これを使うと User クラスの定義は次のように短く書くことができます。

```python
class User:
    def __init__(self, *, name=None, email=None):
        define_property(self, "name", name)
        define_property(self, "email", email)
```

```define_property``` により大幅にコード量を減らすことができましたが、```name``` や ```email``` を複数回書いているところが冗長に感じます。

# コンストラクタに対するデコレータを作成する

そこで、コンストラクタに対するデコレータを作成します。

```python
def define_properties(*names):
    def decorator(constructor):
        def wrapper(self, **kwargs):
            for name in names:
                define_property(self, name, kwargs.get(name))
            constructor(self, **kwargs)
        return wrapper
    return decorator
```

```define_properties``` を使用すると、User クラスは次のように書き直すことができます。

```python
class User:
    @define_properties("name", "email")
    def __init__(self):
        pass
```

冗長さが無くなりました。あともう少しだけ改良すれば完成です。

# 完成形

もう少し便利にするために、以下の変更を行います。

 * 読み取り専用/書き込み専用プロパティを定義できるように変更。
    * ```define_property``` の引数を変更。
        * ```readable``` に ```True``` を指定すると読み取り可能として定義する。
        * ```writable``` に ```True``` を指定すると書き込み可能として定義する。
    * ```define_properties``` の引数を変更。
        * ```accessible``` に指定したプロパティは読み書き可能として定義する。
        * ```readable``` に指定したプロパティは読み取り可能として定義する。
        * ```writable``` に指定したプロパティは書き込み可能として定義する。
        * ```readable``` と ```writable``` の両方に指定されたプロパティは読み書き可能として定義する。
 * ```define_properties``` の引数を何も指定しない場合は、コンストラクタのキーワード引数で指定された名前の読み書き可能なプロパティを定義する。

変更したコードは以下の通りです。

```python:define_property.py
def define_property(self, name, value=None, readable=True, writable=True):
    # "_User__name" のような name mangling 後の名前.
    field_name = "_{}__{}".format(self.__class__.__name__, name)

    # 初期値を設定する.
    setattr(self, field_name, value)

    # getter/setter を生成し, プロパティを定義する.
    getter = (lambda self: getattr(self, field_name)) if readable else None
    setter = (lambda self, value: setattr(self, field_name, value)) if writable else None
    setattr(self.__class__, name, property(getter, setter))

def define_properties(constructor=None, *, accessible=(), readable=(), writable=()):
    if callable(constructor):
        def wrapper(self, *args, **kwargs):
            for name, value in kwargs.items():
                define_property(self, name, value)
            constructor(self, *args, **kwargs)
        return wrapper
    else:
        to_set = lambda x: set(x) if any(isinstance(x, type_) for type_ in (set, list, tuple)) else {x}
        accessibles = to_set(accessible)
        readables = accessibles | to_set(readable)
        writables = accessibles | to_set(writable)

        def decorator(constructor):
            def wrapper(self, *args, **kwargs):
                for name in (readables | writables):
                    readable = name in readables
                    writable = name in writables
                    initial_value = kwargs.get(name, None)
                    define_property(self, name, initial_value, readable, writable)
                constructor_kwargs = dict([(key, kwargs[key]) for key in (constructor.__kwdefaults__ or {}) if key in kwargs])
                constructor(self, *args, **constructor_kwargs)
            return wrapper
        return decorator
```

テストコードは以下の通りです。

```python:define_property_test.rb
import unittest
from define_property import *

class DefinePropertyTest(unittest.TestCase):

    def test_initial_value(self):
        class User:
            def __init__(self, *, name=None, email=None):
                define_property(self, "name", name)
                define_property(self, "email", email)

        user = User()
        taro = User(name="taro", email="taro@example.com")
        self.assertEqual(None, user.name)
        self.assertEqual(None, user.email)
        self.assertEqual("taro", taro.name)
        self.assertEqual("taro@example.com", taro.email)

    def test_accessor(self):
        class User:
            def __init__(self, *, name=None):
                define_property(self, "name", name)

        taro = User(name="taro")
        self.assertEqual("taro", taro.name)
        taro.name = "たろう"
        self.assertEqual("たろう", taro.name)

    def test_readable(self):
        class User:
            def __init__(self, *, name=None, email=None):
                define_property(self, "name", name, readable=True)
                define_property(self, "email", email, readable=False)

        taro = User(name="taro", email="taro@example.com")
        taro.name
        with self.assertRaises(AttributeError):
            taro.email

    def test_writable(self):
        class User:
            def __init__(self, *, name=None, email=None):
                define_property(self, "name", name, writable=True)
                define_property(self, "email", email, writable=False)

        taro = User(name="taro", email="taro@example.com")
        taro.name = "たろう"
        with self.assertRaises(AttributeError):
            taro.email = "taro+test@example.com"

    def test_not_accessible(self):
        class User:
            def __init__(self, *, name=None, email=None):
                define_property(self, "name", name, readable=False)
                define_property(self, "email", email, readable=False)

        taro = User(name="taro", email="taro@example.com")
        with self.assertRaises(AttributeError):
            taro.name
        with self.assertRaises(AttributeError):
            taro.email

    def test_access_by_other_method(self):
        class User:
            def __init__(self, *, name=None, email=None):
                define_property(self, "name", name)
            def get_name(self):
                return self.__name

        taro = User(name="taro")
        self.assertEqual("taro", taro.get_name())

class DefinePropertiesTest(unittest.TestCase):

    def test_no_arguments(self):
        class User:
            @define_properties
            def __init__(self, *, name=None, email=None):
                pass

        with self.assertRaises(AttributeError):
            User().name
        User(name="taro").name

        with self.assertRaises(AttributeError):
            User(name="taro").email
        User(name="taro", email="taro@example.com").email

    def test_initial_value(self):
        class User:
            @define_properties(accessible=("name", "email"))
            def __init__(self, *, name=None, email=None):
                if name != None:
                    self.__name = self.name.upper()
                if email != None:
                    self.__email = self.email.upper()

        user = User()
        self.assertEqual(None, user.name)
        self.assertEqual(None, user.email)

        taro = User(name="taro", email="taro@example.com")
        self.assertEqual("TARO", taro.name)
        self.assertEqual("TARO@EXAMPLE.COM", taro.email)

    def test_accessible_with_no_arguments(self):
        class User:
            @define_properties(accessible=())
            def __init__(self):
                pass

        user = User()
        with self.assertRaises(AttributeError):
            user.name

    def test_accessible_with_string_argument(self):
        class User:
            @define_properties(accessible="name")
            def __init__(self):
                pass

        user = User()
        self.assertEqual(None, user.name)
        user.name = "jiro"
        self.assertEqual("jiro", user.name)

        user = User(name="taro")
        self.assertEqual("taro", user.name)
        user.name = "jiro"
        self.assertEqual("jiro", user.name)

    def test_accessible_with_tuple_argument(self):
        class User:
            @define_properties(accessible=("name", "email"))
            def __init__(self):
                pass

        user = User()
        self.assertEqual(None, user.name)
        self.assertEqual(None, user.email)
        user.name = "jiro"
        user.email = "jiro@example.com"
        self.assertEqual("jiro", user.name)
        self.assertEqual("jiro@example.com", user.email)

        user = User(name="taro", email="taro@example.com")
        self.assertEqual("taro", user.name)
        self.assertEqual("taro@example.com", user.email)
        user.name = "jiro"
        user.email = "jiro@example.com"
        self.assertEqual("jiro", user.name)
        self.assertEqual("jiro@example.com", user.email)

    def test_readable_with_no_arguments(self):
        class User:
            @define_properties(readable=())
            def __init__(self):
                pass

        user = User()
        with self.assertRaises(AttributeError):
            user.name

    def test_readable_with_string_argument(self):
        class User:
            @define_properties(readable="name")
            def __init__(self):
                pass

        user = User()
        self.assertEqual(None, user.name)
        with self.assertRaises(AttributeError):
            user.name = "jiro"

        user = User(name="taro")
        self.assertEqual("taro", user.name)
        with self.assertRaises(AttributeError):
            user.name = "jiro"

    def test_readable_with_tuple_argument(self):
        class User:
            @define_properties(readable=("name", "email"))
            def __init__(self):
                pass

        user = User()
        self.assertEqual(None, user.name)
        self.assertEqual(None, user.email)
        with self.assertRaises(AttributeError):
            user.name = "jiro"
        with self.assertRaises(AttributeError):
            user.email = "jiro@example.com"

        user = User(name="taro", email="taro@example.com")
        self.assertEqual("taro", user.name)
        self.assertEqual("taro@example.com", user.email)
        with self.assertRaises(AttributeError):
            user.name = "jiro"
        with self.assertRaises(AttributeError):
            user.email = "jiro@example.com"

    def test_writable_with_no_arguments(self):
        class User:
            @define_properties(writable=())
            def __init__(self):
                pass

        user = User()
        with self.assertRaises(AttributeError):
            user.name
        user.name = "taro"

    def test_writable_with_string_argument(self):
        class User:
            @define_properties(writable="name")
            def __init__(self):
                pass

        user = User()
        with self.assertRaises(AttributeError):
            user.name
        user.name = "jiro"

        user = User(name="taro")
        with self.assertRaises(AttributeError):
            user.name
        user.name = "jiro"

    def test_writable_with_tuple_argument(self):
        class User:
            @define_properties(writable=("name", "email"))
            def __init__(self):
                pass

        user = User()
        with self.assertRaises(AttributeError):
            user.name
        with self.assertRaises(AttributeError):
            user.email
        user.name = "jiro"
        user.email = "jiro@example.com"

        user = User(name="taro", email="taro@example.com")
        with self.assertRaises(AttributeError):
            user.name
        with self.assertRaises(AttributeError):
            user.email
        user.name = "jiro"
        user.email = "jiro@example.com"

if __name__ == "__main__":
    unittest.main()
```