Help us understand the problem. What is going on with this article?

Djangoモデルのフィールドがクラスから消える話

More than 1 year has passed since last update.

概要

  • 同僚が発表してた内容を見て自分用に調べてメモった。
  • Djangoのモデルでクラス変数であるフィールドがクラスから消えるのなんで?という話があった
  • 何故そうなるのかという話を順を追って簡潔(雑)に残す

クラス変数

class Person:
    name = 'takanory'

Person.name # => takanory

p1 = Person()
p1.name # => takanory

assert Person.name is p1.name  # same object

Djangoモデルからフィールドが消える

  • 次にDjangoのモデル定義を見る
  • models.Model を継承してはいるが、普通のPythonのクラス定義である
  • だがフィールドのクラス変数が存在しない
class Person(models.Model):
    name = models.CharField(max_length=255, default='takanory')
    age = 37

# 普通のクラス変数は存在する
Person.age # => 37

# Fieldオブジェクトのクラス変数 「name」 がない
Person.name 
# => AttributeError: type object 'Person' has no attribute 'name'

# 実は「name」は「_meta.fields」に存在している
Person._meta.fields
# => (<django.db.models.fields.AutoField: id>, <django.db.models.fields.CharField: name>)
  • 普段、Djangoを使ってる人にとっては何も不思議ではない
  • けどよく考えたらPythonの挙動としてこれおかしくないですか? という疑問

クラスとメタクラス

  • この疑問に答えるためには、メタクラスの話が必要になる
  • type でクラスオブジェクトを作ることができる
  • クラス定義とは type でクラスオブジェクトを生成することと同義である
  • クラスは type のインスタンスである
# 普通のクラス定義
class Person:
    name = 'takanory'

# 上記クラス定義と同じクラスオブジェクト「Person」を生成する
Person = type('Person', tuple(), {'name': 'takanory'})

# クラスオブジェクトは type の インスタンス
assert isinstance(Person, type)
  • クラスを生成するための、元となるクラスのことをメタクラスと呼ぶ
  • デフォルトでは typeメタクラスとしてクラスを生成する。
  • metaclass を指定することで別のメタクラスを利用できる。
  • つまりクラス定義の仕方そのものをカスタマイズ可能
class Taka22(type):
    """ 
    name が 'takanory' の場合のみ
    nickname = 'taka22' というクラス変数を追加するメタクラス
    """
    def __new__(cls, name, bases, attrs):
        if attrs.get('name') == 'takanory':
            attrs['nickname'] = 'taka22'
        return super().__new__(cls, name, bases, attrs)  

class Person(metaclass=Taka22):
    name = 'takanory'

Person.nickname # => 'taka22'

Djangoのソースを追う

  • ここまで来るとクラスから「フィールドが消えてる」のは、メタクラスがやってるんだろうという想像がつく
  • 実際に該当箇所を確認してみる。

Modelクラスのメタクラスを確認

  • Model クラスは ModelBase というメタクラスを利用してる
class Model(six.with_metaclass(ModelBase)):
    _deferred = False

    def __init__(self, *args, **kwargs):

# refs https://github.com/django/django/blob/master/django/db/models/base.py#L355
  • six は Python2 と Python3の互換性を保つためのライブラリ
  • Python2とPython3では metaclass の指定の仕方に差異があるのでそれ埋めてくれる
# python3
class Hoge(metaclass=NewMeta):

# python2
class Hoge(object):
    __metaclass__ = NewMeta

# python3 and python2
class Hoge(six.with_metaclass(NewMeta):

フィールドを消している場所

  • メタクラス ModelBase の中では、attrs(クラス変数が入っている) を 親クラス( type )の new(super_new) に渡していない
  • module だけを attrs として渡してる。この時点でクラス定義からは、フィールドオブジェクトなどのクラス変数が全て一旦消えている
class ModelBase(type):
    """
    Metaclass for all models.
    """
    def __new__(cls, name, bases, attrs):
        # 親クラス(type)の __new__ メソッド
        super_new = super(ModelBase, cls).__new__

        # ~ 省略 ~

        # Create the class.
        module = attrs.pop('__module__')
        # module だけを attr として渡して new_class を生成している
        # この時点でクラス変数は消えている
        new_class = super_new(cls, name, bases, {'__module__': module})

# refs https://github.com/django/django/blob/master/django/db/models/base.py#L67             

フィールド を _meta.fields に移動

  • この後、フィールドは自身の contribute_to_class で モデルの _meta.fields にセットされる
  • 全てのフィールドオブジェクトは contribute_to_class を持っていることを期待する
  • つまり _meta.fields にセットするか否かは Model ではなく Field に責任が移譲されている
  • contribute_to_class をもっていないクラス変数はそのままセットされる
def add_to_class(cls, name, value):
    # We should call the contribute_to_class method only if it's bound
    if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
        value.contribute_to_class(cls, name) # value は フィールドオブジェクト
    else:
        # contribute_to_classを持ってない
        # attrはそのままクラス定義にとしてセットされる(戻される)
        setattr(cls, name, value)

# refs https://github.com/django/django/blob/c339a5a6f72690cd90d5a653dc108fbb60274a20/django/db/models/base.py#L303
  • Fieldcontribute_to_class のなかで Model._meta.fields にセットされる
def contribute_to_class(self, cls, name, private_only=False, virtual_only=NOT_PROVIDED):
    # ~ 省略

    # だいたいこの辺で、受け取ったcls(=model)の _meta に fields として自らを追加してる.
    self.model = cls
    if private_only:
        cls._meta.add_field(self, private=True)
    else:
        cls._meta.add_field(self)
    if self.choices:
        setattr(cls, 'get_%s_display' % self.name,
                    curry(cls._get_FIELD_display, field=self))

# refs https://github.com/django/django/blob/master/django/db/models/fields/__init__.py#L678

まとめ

  • Djangoのモデルからフィールドなどのクラス変数がどうやって消えているのか?
  • それは Django の Model が独自のメタクラス(ModelBase) の中で意図的に、クラス変数から消すようにしているからである
  • ここからは完全な推測だが、こういう実装にした意図は、モデルをインスタンス化したときにフィールドオブジェクトを保護したかったんだろうな思った。
class Person(models.Model):
    name = models.CharField(max_length=255, default='takanory')

p1 = Person()
p1.name # => 'takanory'

# インスタンスは直接値の文字列「takanory」参照できる
# Personモデルがインスタンス化するタイミングで、self.nameにフィールドの「値」が代入されている
# と同時に、クラス変数「Person.name」は「self.name」では参照できなくなる
# そのため「Person._meta.fields」に退避してるんじゃなかろうか?

参考

tell-k
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした