0
0

More than 1 year has passed since last update.

Python3でクラス変数をオーバーライドする関数を作成する

Posted at

はじめに

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_** の取得

ラストです。
この時点で propslist[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的な振る舞いが実現できそうですね。
終わります。

0
0
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
0
0