【Python実践自作問題集】No.6 型に厳密なデータクラス3

# 1. はじめに

この問題集は、Pythonの基礎を習得した後、次の段階へ進みたい人のサポートをすることが目的です。
また、競技プログラミングとは異なり、複雑なアルゴリズムの問題ではなく「**可読性の最大化**」に焦点を当てた問題が中心となります。

[目次](https://qiita.com/__Lily__/items/6f70cd6ae0e80593f09d)
前の問題：[型に厳密なデータクラス2](https://qiita.com/__Lily__/items/87ed19c0087a712692a8)
次の問題：[No.7 不正な値を許すな](https://qiita.com/__Lily__/items/ab9dfb8bd0cb0383b641)



# 2. 問題

```python:Question
"""
No.6 型に厳密なデータクラス3

次のSampleデータクラスにはvalueという属性があり、float型を指定しています。
また、__post_init__を活用することでvalue属性の値はfloat型に制限されています。
しかし、Sampleクラスの利用者から次のような要望が出てきました。
全ての要望及び制約を満たすようにSampleクラスの改修案を検討してください。

なお、データクラスの使用は必須ではありません。

1. 再代入を禁止したい
    sample = Sample(value=0.0)
    sample.value = 1.0
    というように、再代入したときはエラーを発生させてほしいです。

制約
    * Sampleクラスはvalueという属性を持ち、初期値を与えることができる。
    * value属性の初期値にfloat型へ変換できる値が指定された場合、変換後の値をvalue属性に格納する。
    * value属性の初期値にfloat型へ変換できない値が指定された場合、エラーを発生させる。
"""

from dataclasses import dataclass


@dataclass
class Sample:
    value: float

    def __post_init__(self) -> None:
        self.value = float(self.value)
```


# 3. 解答例

```python:
from pydantic import BaseModel, ConfigDict


class Sample(BaseModel):
    model_config = ConfigDict(frozen=True)

    value: float
```


# 4. 採点基準

* 初期値が`float`型へ変換できる場合、`value`属性に変換した値が格納されること
* 初期値が`float`型へ変換できない場合、エラーが発生すること
* 再代入する場合、エラーが発生すること


# 5. 解説

前問、前々問の類題です。
今回の問題では、再代入を禁止するという条件が追加されています。

## 5.1 pydanticを使用する場合

まずは`pydantic`を使用した場合ですが、解答の通り`model_config = ...`という1行を追加するだけでOKです。

[Pydantic 忘備録 - Qiita](https://qiita.com/daikumatan/items/2a9b5af366720a3b284b#configdict%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E4%BE%8B)

この`model_config`変数を`ConfigDict`で上書きすることで、型チェックの挙動を変更することができます。
型の指定を厳密にする`strict`など、他にも色々なオプションがあるので是非調べてみてください。

[Configuration - docs.pydantic.dev](https://docs.pydantic.dev/latest/api/config/)

 なお、挙動の変更についても`pydantic v1`と`pydantic v2`で文法が変更されています。
 次のような`Config`クラスを定義する方法は現在非推奨となっているので注意してください。

 ```python
from pydantic import BaseModel


class Sample(BaseModel):
    value: float

    class Config:
        frozen = True
 ```

 [FastAPIがPydantic v2対応したので、V2移行のポイントを紹介する(意外と簡単) - Zenn](https://zenn.dev/tk_resilie/articles/fastapi0100_pydanticv2#(%E6%8E%A8%E5%A5%A8)class-config-%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F-model_config-%3D-configdict()%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B)

 ## 5.2 データクラスを使用する場合

さて、`pydantic`を使った解答は前節の通りですが、データクラスを使った場合はどうなるのでしょうか？
再代入を禁止するには、`@dataclass`を`@dataclass(frozen=True)`に変えればいいです。

```python
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class Sample:
    value: float

    def __post_init__(self) -> None:
        self.value = float(self.value)
```

しかし、これを実際に使ってみると次のようなエラーが発生します。

```python
sample = Sample(value=0.0)
```

```text:実行結果
dataclasses.FrozenInstanceError: cannot assign to field 'value'
```

よく考えてみると、オブジェクトを作成するときに`value`属性に値が設定され、`__post_init__`の中で`value`属性に再代入しています。
つまり、値を`float`型へ変換する際に再代入が生じてしまうのです。

更に、前問で紹介した`__setattr__`をオーバーライドする方法についてですが、`frozen=True`と`__setattr__`のオーバーライドは併用できません。
次のようなコードはエラーになります。

```python
@dataclass(frozen=True)
class Sample:
    value: float

    def __setattr__(self, key: str, value: Any) -> None:
        if key == 'value':
            value = float(value)
        super().__setattr__(key, value)
```

```text:実行結果
TypeError: Cannot overwrite attribute __setattr__ in class Sample
```

ということで、型の強制と再代入の禁止の両方をデータクラスで実現するのは厳しいです。
（もしかしたら方法があるかもしれませんが……）
以上の議論から、今回の問題では`pydantic`を使用した方法を模範解答とします。