Edited at

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


概要


  • 同僚が発表してた内容を見て自分用に調べてメモった。

  • 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」に退避してるんじゃなかろうか?


参考