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

# 概要

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

# クラス変数

- まずクラス変数の話から
- クラス変数は、全てのインスタンスに共有される。
- http://docs.python.jp/3/tutorial/classes.html#class-and-instance-variables

```py3
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のクラス定義である
- だがフィールドのクラス変数が存在しない

```py3
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** のインスタンスである

```py3
# 普通のクラス定義
class Person:
    name = 'takanory'

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

# クラスオブジェクトは type の インスタンス
assert isinstance(Person, type)
```

- クラスを生成するための、元となるクラスのことを**メタクラス**と呼ぶ
- デフォルトでは **type** を**メタクラス**としてクラスを生成する。
- **metaclass** を指定することで別の**メタクラス**を利用できる。
- つまりクラス定義の仕方そのものをカスタマイズ可能

```py3
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** というメタクラスを利用してる

```py3
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](https://pypi.python.org/pypi/six) は Python2 と Python3の互換性を保つためのライブラリ
-  Python2とPython3では **metaclass** の指定の仕方に差異があるのでそれ埋めてくれる

```py3
# 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 として渡してる。この時点でクラス定義からは、フィールドオブジェクトなどのクラス変数が全て一旦消えている

```py3
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** をもっていないクラス変数はそのままセットされる

```py3
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
```

- **Field** の **contribute_to_class** のなかで **Model._meta.fields** にセットされる

```py3
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) の中で意図的に、クラス変数から消すようにしているからである**
- ここからは完全な推測だが、こういう実装にした意図は、モデルをインスタンス化したときにフィールドオブジェクトを保護したかったんだろうな思った。

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

# 参考

- [Pythonのメタクラスについて](http://aodag.bitbucket.org/meta_class.html)
- [Python の メタプログラミング (__metaclass__, メタクラス) を理解する](http://www.yunabe.jp/docs/python_metaclass.html)
- [Django のモデルとフィールドのクラス変数について](http://atas.hatenablog.jp/entry/2016/04/21/203242)
- [Metaprogramming &mdash; Python 3 Patterns, Recipes and Idioms](http://python-3-patterns-idioms-test.readthedocs.org/en/latest/Metaprogramming.html)
- [Pythonによる黒魔術入門](http://www.slideshare.net/ssuser38b704/ll-lang-blackmagic)
- [[python]メタプログラミングの基礎(__init__, __new__, __metaclass__)](http://dackdive.hateblo.jp/entry/2015/08/02/100000)
- [[Python] メタクラスをたおした](http://note.crohaco.net/2016/python-metaclass/)
- [python - メタクラスと継承の違いについて教えてください - スタック・オーバーフロー](http://ja.stackoverflow.com/a/16605)
- [[書籍] エキスパートPython - 3.5.2-metaclass--メソッド](http://www.amazon.co.jp/dp/4048686291?tag=study02-22)
- [[書籍] Python文法詳解 - 6.4.7 メタプログラミング](http://www.amazon.co.jp/dp/4873116880?tag=study02-22)