はじめに
django REST frameworkにあるSerializersでは、以下のようにクラス変数で定義されたフィールドをオーバーライドして、書き換えたり独自のバリデーションを付与したりすることができます。
from rest_framework import serializers
class BlogPostSerializer(serializers.Serializer):
title = serializers.CharField(max_length=100)
days_since_joined = serializers.SerializerMethodField()
def validate_title(self, value):
if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
return value
def get_days_since_joined(self, obj):
return (now() - obj.date_joined).days
validate_{field名}
やget_{field名}
の命名規則で定義します。
でもこれってどうやっているんだろう?
気になったので実装してみました。
実装
とりあえず get_**
だけで実装してみました。
import inspect
from typing import Final
RESERVED_PROPERTY_WORD: Final = ['Meta', 'props']
"""あらかじめ定義しているクラス変数"""
class MyClass:
a = 'a'
b = 'b'
def __init__(self):
meta = getattr(self, 'Meta')
is_get = getattr(meta, 'is_get', False)
attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
props = [
a for a in attr
if not a[0].startswith('__') and a[0] not in RESERVED_PROPERTY_WORD
]
for name, _ in props:
if is_get:
f_name = 'get_{}'.format(name)
get_f = getattr(self, f_name, None)
if inspect.ismethod(get_f):
setattr(self, name, get_f())
def get_a(self):
return 'aaa'
@property
def props(self):
return {
'a': self.a,
'b': self.b
}
class Meta:
is_get = True
def main():
my_class = MyClass()
print(my_class.props) # {'a': 'aaa', 'b': 'b'}
if __name__ == '__main__':
main()
解説
ざっと説明していきます。
RESERVED_PROPERTY_WORD
あらかじめ定義しているクラス変数は取得しないようにするために RESERVED_PROPERTY_WORD
を入れています。
こういうのにありがちな
class Meta:
pass
と、
変わったかを確認するために入れた
@property
def props(self):
pass
に対応するために入れています。
フィルタ処理
props = [
a for a in attr
if not a[0].startswith('__') and a[0] not in RESERVED_PROPERTY_WORD
]
で、あらかじめ定義している変数名と特殊メソッドを弾いています。
これを行わないと、 get_**
の対象に、 get_Meta
や get_props
も含まれてしまうためです。
上記のフィルタ処理をしない attr
を出力すると以下の通りになっています。
[
('Meta', <class __main__.MyClass.Meta>),
('__class__', <class __main__.MyClass>),
('__dict__', {}),
('__doc__', None),
('__module__', '__main__'),
('__weakref__', None),
('a', 'a'),
('b', 'b'),
('props', {'a': 'a', 'b': 'b'})
]
クラス変数 a
, b
のみ欲しいので、フィルタ処理が必要になる理由がお分かりいただけるかと思います。
inspect
クラス変数を取得するために用いています。
標準で生えています。
参考: https://docs.python.org/ja/3/library/inspect.html
getmembersで全てのメンバを取得できるのですが、多すぎていらないので、
isroutineである程度絞ります。
それでもまだ __**__
が少し残るので、 startswith('__')
でフィルタしてあげます。
今回はprivateメンバを定義していないのでstartswithのみですが、privateメンバを定義するならendswithも行ってあげる必要があります。
attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
props = [
a for a in attr
if not a[0].startswith('__') and a[0] not in RESERVED_PROPERTY_WORD
]
class Meta
pythonにはmetaclassが存在しますが、それとは別のものになります。
他のフレームワークは知りませんが、djangoにはよく出てきます。
よく見かけるし、DRFにもModelSerializerなどで出てくるため入れました。
今回はあってもなくてもいいけれど、せっかくなので get_**
の有効無効設定を入れました。
is_get
にFalseを入れるかそもそも入れないと、 get_**
が機能しなくなります。
Falseを入れた場合の出力結果は以下です。
{'a': 'a', 'b': 'b'}
'a': 'aaa'
になっていないので、機能していないことがわかります。
class Meta
は以下の通りに取得できます。
meta = getattr(self, 'Meta')
get_** の取得
ラストです。
この時点で props
は list[tuple[str, Any]]
となっており、 タプルの中身は (変数名, 値)
となっています。
ですのでループで回してあげて、 get_{変数名}
をクラスから取得し、関数であれば実行して詰め直してあげるようにします。
for name, _ in props:
f_name = 'get_{}'.format(name)
get_f = getattr(self, f_name, None)
if inspect.ismethod(get_f):
setattr(self, name, get_f())
おわりに
いかがだったでしょうか。
簡単にではありますが、やりたいことは叶えられたのではないでしょうか。
クラス変数のキーとバリューが取れるので、バリューをisinstanceでフィルタしてfieldクラスで定義されていたら、与えられたオブジェクトと比較してあげるといった処理をしてあげることでDRFのSerializer的な振る舞いが実現できそうですね。
終わります。