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

# 1. はじめに

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

[目次](https://qiita.com/__Lily__/items/6f70cd6ae0e80593f09d)
前の問題：[型に厳密なデータクラス1](https://qiita.com/__Lily__/items/1f6a2d221fbaae98a523)
次の問題：[型に厳密なデータクラス3](https://qiita.com/__Lily__/items/36f527a357aa63b2520d)


# 2. 問題

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

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

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

1. 再代入したときにも型をチェックしたい
    sample = Sample(value=0.0)
    sample.value = 'abc'
    というように、再代入したときにも型をチェックしたいです。
    __post_init__はオブジェクト作成時にしか実行されないため、型チェックが実行されません。
    なお再代入時の型チェックロジックは、初期値を指定するときと同じものを使用してください。

制約
    * 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(validate_assignment=True)

    value: float
```


# 4. 採点基準

* 初期値が`float`型へ変換できる場合、`value`属性に変換した値が格納されること
* 初期値が`float`型へ変換できない場合、エラーが発生すること
* 再代入する値が`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:
        validate_assignment = 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`を使った解答は前節の通りですが、データクラスを使った場合はどうなるのでしょうか？
一応、次のようにして実現はできますが……

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


@dataclass
class Sample:
    value: float

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

この方法には良くない点があります。
属性`value`の名前を変更する場合を考えてみましょう。
最近のエディタは変数名の一括置換ができるものが多いですが、このとき`if key == 'value'`の`'value'`までは置換されません。
変更漏れに気付かなければエラーになってしまいます。
また、これは個人的な感覚になってしまいますが、`__setattr__`のオーバーライドは黒魔術と化しやすい印象を持っています。
そのため、この方法は非推奨としたいと思います。
（もしかしたら別の良い方法があるかもしれません）


# 6. テストコード

```python
print('-----初期化-----')

value = 0.0
sample = Sample(value=value)
assert sample.value == 0.0
assert isinstance(sample.value, float)

value = 0
sample = Sample(value=value)
assert sample.value == 0.0
assert isinstance(sample.value, float)

value = '0.0'
sample = Sample(value=value)
assert sample.value == 0.0
assert isinstance(sample.value, float)

value = 'abc'
try:
    _ = Sample(value=value)
except (Exception,) as e:
    print(type(e))
    print(e)

print('-----再代入-----')

sample = Sample(value=0.0)
new_value = 1.0
sample.value = new_value
assert sample.value == 1.0
assert isinstance(sample.value, float)

sample = Sample(value=0.0)
new_value = 1
sample.value = new_value
assert sample.value == 1.0
assert isinstance(sample.value, float)

sample = Sample(value=0.0)
new_value = '1.0'
sample.value = new_value
assert sample.value == 1.0
assert isinstance(sample.value, float)

sample = Sample(value=0.0)
new_value = 'abc'
try:
    sample.value = new_value
except (Exception,) as e:
    print(type(e))
    print(e)
```

```text:実行結果
-----初期化-----
<class 'pydantic_core._pydantic_core.ValidationError'>
1 validation error for Sample
value
  Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='abc', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/float_parsing
-----再代入-----
<class 'pydantic_core._pydantic_core.ValidationError'>
1 validation error for Sample
value
  Input should be a valid number, unable to parse string as a number [type=float_parsing, input_value='abc', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/float_parsing
```