form.clean_xxxの実行順

重要な追記

コメ欄でぼこすか殴られたので追記。
こういう複数のフィールドにまたがるバリデーションはcleanを使うべきで、↓みたいなコードを書くべきではありません。
この記事の趣旨は今まで不定だと思いこんでたclean_xxxの実行順が実は決まってたというのと、それを悪用するとこんなカウボーイコードもかけちゃうみたいな話でした。

http://docs.djangoproject.com/en/dev//ref/forms/validation/

djangoのフォームはclean_[FIELD NAME]というメソッドを生やすと独自のバリデーションを定義できます。

これを(無理やり)使ってパスワード確認入力の一致チェックをやろうとすると

class PasswordForm(forms.Form):
    password = forms.CharField(label=u"パスワード")
    password2 = forms.CharField(label=u"確認入力")

    def clean_password2(self):
        if self.cleaned_data['password'] != self.cleaned_data['password2']:
            raise forms.ValidationError(u"一致しません")

みたいになりますが、clean_password2の実行時点でcleaned_data['password']が存在するかどうかはフォームフィールドの定義順に依存しているので注意が必要です。*1

追ってみる

各cleanの実行順とcleaned_dataの状態をprintデバッグでおっかけると

class DebugField(forms.CharField):
    def clean(self, val):
        print "clean DebugField %s" % val
        return super(DebugField, self).clean(val)

class DebugForm(forms.Form):
    password = DebugField()
    password2 = DebugField()
    
    def clean_password(self):
        print "clean_password"
        print self.cleaned_data
        return self.cleaned_data['password']

    def clean_password2(self):
        print "clean_password2"
        print self.cleaned_data
        return self.cleaned_data['password2']

実行

>>> DebugForm({'password': 'password', 'password2': 'password2'}).is_valid()
clean DebugField password
clean_password
{'password': u'password'}
clean DebugField password2
clean_password2
{'password2': u'password2', 'password': u'password'}
True

とまあ、Field.clean->form.clean_field という1セットをフィールド分繰り返しているのが分かります。

clean_passwordの時点ではcleaned_dataはpasswordのデータしかないので、ここでpassword2を参照しようとしてもだめです。

定義順ってほんと?

これがほんとに定義順になってるのか気になったのでもう少し追ってみた。

django/forms/form.py

    def _clean_fields(self):
        for name, field in self.fields.items():
            ...

items()じゃん。なってねぇくせー

と思ったら

>>> type(DebugForm().fields)
<class 'django.utils.datastructures.SortedDict'>

SortedDictなので順番は持ってるっぽい

def get_declared_fields(bases, attrs, with_base_fields=True):
    ...
    fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
    fields.sort(key=lambda x: x[1].creation_counter)

Fieldインスタンスのcreation_counterなる値でソートしているらしい

django/forms/fields.py

class Field(object):
    ... 

    # Tracks each time a Field instance is created. Used to retain order.
    creation_counter = 0

    def __init__(self, required=True, widget=None, label=None, initial=None,
                 help_text=None, error_messages=None, show_hidden_initial=False,
                 validators=[], localize=False):
        ...
        # Increase the creation counter, and save our local copy.
        self.creation_counter = Field.creation_counter
        Field.creation_counter += 1

Field.creation_counterという属性値が__init__が呼び出されるたびにカウントアップされて各インスタンスのcreation_counter属性にセットされてるようだ。

これならたしかに定義順=__init__が呼び出される順にソート可能だ。

まとめ

  • clean_xxxメソッド内部で別フィールドのcleaned_dataを参照することはできる
  • ただし自分より前に定義されているフィールドの値しか参照できない
  • そういう処理はcleanメソッドでやれ

*1:ドキュメントでも実行順については少し言及されている