Djangoのform.Formのclean()メソッドでfieldがNoneになる

# 経緯
Djangoのform.Formを使うとき、まずは`is_valid()`メソッドで中身を確認してから`cleaned_data`を取り出す。
`is_valid()`の中で実行される`self.clean()`メソッドをオーバーライドして複数フィールド間のvalidationを行うが、チェックしようとしたフィールドの値が`None`になることがありforms.Formについて実装を読んだ。
(追記)[ドキュメント](https://docs.djangoproject.com/ja/4.0/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other])に書いてあった...
2022/3/10時点でのdjango==4.0.3のコードとともに説明していく。


# Djangoのforms.Form.is_valid()
`is_valid()`はフォームにデータが与えられていて、かつまだ`is_valid()`をしていない場合に
validationが行われる。はじめは`self.errors`が`None`なので`full_clean()`が行われる。

```python
    def is_valid(self):
        """Return True if the form has no errors, or False otherwise."""
        return self.is_bound and not self.errors

    @property
    def errors(self):
        """Return an ErrorDict for the data provided for the form."""
        if self._errors is None:
            self.full_clean()
        return self._errors
```
`is_valid()`は`not self._errors`を返すのでエラーがあった場合は`False`となりそこで処理を分岐できる。

`full_clean()`は3つのvalidationを行い最終的に`_errors`と`cleaned_data`を生成する。
```python
    def full_clean(self):
        """
        Clean all of self.data and populate self._errors and self.cleaned_data.
        """
        self._errors = ErrorDict()
        if not self.is_bound:  # Stop further processing.
            return
        self.cleaned_data = {}
        # If the form is permitted to be empty, and none of the form data has
        # changed from the initial data, short circuit any validation.
        if self.empty_permitted and not self.has_changed():
            return

        self._clean_fields()
        self._clean_form()
        self._post_clean()
```
`self._clean_fields()`は各フィールドについてのvalidation、
`self._clean_form()`は複数フィールド間でのvalidation、
`self._post_clean()`はModelFormで利用するvalidationらしく今回は扱わない。

`self._clean_fields()`をざっと見ていく。
```python
    def _clean_fields(self):
        for name, bf in self._bound_items():
            field = bf.field
            value = bf.initial if field.disabled else bf.data
            try:
                if isinstance(field, FileField):
                    value = field.clean(value, bf.initial)
                else:
                    value = field.clean(value)
                self.cleaned_data[name] = value
                if hasattr(self, "clean_%s" % name):
                    value = getattr(self, "clean_%s" % name)()
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)
```
各フィールドについて`clean()`を行い問題がなければ`cleaned_data`に追加する。
またFormは`clean_<フィールド名>()`のメソッドがあるとそれを実行してくれる(`if hasattr(self, "clean_%s" % name):`の部分)。

```python
from django import forms


class MyForm(forms.Form):
    hoge = forms.IntegerField(min_value=0, required=True)
    
    def clean_hoge(self):
        hoge = self.cleaned_data['hoge']
        ...    
```
上記の例だと、`hoge`フィールドのvalidationを行うとき`clean_hoge()`も実行してくれる。ここでは多少複雑なvalidationができる。結果を再び`self.cleaned_data[name] = value`で保存するので`return`で値を返す必要がある。

これらのvalidationで失敗すると`self.add_error(name, e)`が実行される。コメント等長かったので一部変更した。
```python
    def add_error(self, field, error):
        if not isinstance(error, ValidationError):
            error = ValidationError(error)

        if hasattr(error, "error_dict"):
            if field is not None:
                raise TypeError()
            else:
                error = error.error_dict
        else:
            error = {field or NON_FIELD_ERRORS: error.error_list}

        for field, error_list in error.items():
            if field not in self.errors:
                if field != NON_FIELD_ERRORS and field not in self.fields:
                    raise ValueError(
                        "'%s' has no field named '%s'."
                        % (self.__class__.__name__, field)
                    )
                if field == NON_FIELD_ERRORS:
                    self._errors[field] = self.error_class(
                        error_class="nonfield", renderer=self.renderer
                    )
                else:
                    self._errors[field] = self.error_class(renderer=self.renderer)
            self._errors[field].extend(error_list)
            if field in self.cleaned_data:
                del self.cleaned_data[field]
```
端的に説明するとエラーがあったフィールドを`self._errors`に追加する。
問題は一番最後の`del self.cleaned_data[field]`だ。
エラーのあったフィールドは`self.cleaned_data`から消されてしまう。これが今回の原因だった。
フィールドの値の不整合があった場合でも処理は止まらない。

`self._clean_fields()`の次は`self._clean_form()`が実行される。

```python
    def _clean_form(self):
        try:
            cleaned_data = self.clean()
        except ValidationError as e:
            self.add_error(None, e)
        else:
            if cleaned_data is not None:
                self.cleaned_data = cleaned_data
```
ここでは`self.clean()`を実行する。`self.clean()`実行するときにはすべてのフィールドについてのvalidationが終了していて`self.cleaned_data`から各フィールドにアクセスできる。
なので`self.clean()`をオーバーライドしてフィールド間のvalidationが行える。

```python
class Order(forms.Form):
    x = forms.IntegerField(min_value=0, required=True)
    y = forms.IntegerField(min_value=0, required=True)
    
    def clean(self):
        cleaned_data = super().clean()
        x, y = cleaned_data.get("x"), cleaned_data.get("y")
        if x < y:
            raise ValidationError()
```
上記の例のようにフィールド`x`と`y`についてのチェックができる。
自分はここでフィールドが`None`返すことでエラーが起き、その原因がわからないでいた。

フィールドに問題があった場合、そもそも`self.clean()`が実行されないと思っていたからだ。
実装を読むとどこかのフィールドでvalidationに失敗しても`self.clean()`は必ず実行され、validationに失敗したフィールドは`self.cleaned_data`から`del`されているので`None`になっていた。
以上

# 所感
原因がわかってスッキリしたのとforms.Formのvalidationの仕組みや手順がわかったので良かった。`required=True`に設定すると空かどうかがフィールドのvalidation時に確認されるが、ここで空だったとしても、やはり`self.clean()`は実行される。
なんというか、フィールドでのvalidationに失敗したなら実行しなくてもいいんじゃないかという気持ちになる。フロントに伝えるエラーメッセージの都合なんだろうか。