普通に @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
により大幅にコード量を減らすことができましたが、name
や email
を複数回書いているところが冗長に感じます。
コンストラクタに対するデコレータを作成する
そこで、コンストラクタに対するデコレータを作成します。
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
の引数を変更。-
readable
にTrue
を指定すると読み取り可能として定義する。 -
writable
にTrue
を指定すると書き込み可能として定義する。
-
-
define_properties
の引数を変更。-
accessible
に指定したプロパティは読み書き可能として定義する。 -
readable
に指定したプロパティは読み取り可能として定義する。 -
writable
に指定したプロパティは書き込み可能として定義する。 -
readable
とwritable
の両方に指定されたプロパティは読み書き可能として定義する。
-
-
-
define_properties
の引数を何も指定しない場合は、コンストラクタのキーワード引数で指定された名前の読み書き可能なプロパティを定義する。
変更したコードは以下の通りです。
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
テストコードは以下の通りです。
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()