概要
- 同僚が発表してた内容を見て自分用に調べてメモった。
- Djangoのモデルでクラス変数であるフィールドがクラスから消えるのなんで?という話があった
- 何故そうなるのかという話を順を追って簡潔(雑)に残す
クラス変数
- まずクラス変数の話から
- クラス変数は、全てのインスタンスに共有される。
- http://docs.python.jp/3/tutorial/classes.html#class-and-instance-variables
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
- Field の contribute_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」に退避してるんじゃなかろうか?
参考
- Pythonのメタクラスについて
- Python の メタプログラミング (metaclass, メタクラス) を理解する
- Django のモデルとフィールドのクラス変数について
- Metaprogramming — Python 3 Patterns, Recipes and Idioms
- Pythonによる黒魔術入門
- [python]メタプログラミングの基礎(init, new, metaclass)
- [Python] メタクラスをたおした
- python - メタクラスと継承の違いについて教えてください - スタック・オーバーフロー
- [書籍] エキスパートPython - 3.5.2-metaclass--メソッド
- [書籍] Python文法詳解 - 6.4.7 メタプログラミング