LoginSignup
85
97

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-09-18

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

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

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 = "たろう" のように書くことができます。

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

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

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

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

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 クラスの定義は次のように短く書くことができます。

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

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

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

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

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 クラスは次のように書き直すことができます。

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

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

完成形

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

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

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

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

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

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()
85
97
0

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
85
97