はじめに
- DDDでコード書く練習していたときに、ValueObjectの型チェック方法を試行錯誤したのでその時のメモ
- Qiitaの記事練習として(初投稿)。
結論
-
__post_init__()
で初期化時の処理がかけるので、ここで型チェックを実行 - isinstanceで型チェック
-
self.__annotations__
から期待値の型を取得する -
dataclasses.asdict(self)
でインスタンスをdictに変換。これをisinstanceにかける。
-
dataclassとは?
-
init()を自動生成してくれる。
-
__init__()
に引数を入れて、self.hoge=arg_hoge
とかする必要ない。
-
- ValueObjectを生成するのに適している。
普通の書き方
class Users:
def __init__(self, user_name: str, user_id: int):
self.user_name = user_name
self.user_id = user_id
dataclassでの書き方
class User:
user_name: str
user_id: int
dataclassで書いたほうがきれいに書けますね!
dataclassでは型チェックしてくれない?
今回の本題です。
user_name: str
やuser_id: int
で型指定していて、型チェックしているように見えますが、実際は普通のアノテーションです。
str型で指定しているのに、int型で入れられてしまいます。
コード
import dataclasses
@dataclasses.dataclass(frozen=True)
class User:
user_name: str
user_id: int
c = User(user_name='ヒノヤコマ', user_id=1)
print(f'{c.user_id}:{c.user_name}')
c_fail = User(user_name=2, user_id='ファイアロー')
print(f'{c_fail.user_id}:{c_fail.user_name}')
実行結果
> python .\dataclassテスト.py
1:ヒノヤコマ
ファイアロー:2
上記のようになんでも入れられてしまいます。
__post_init__()
で初期化時に型チェックを実行
下記のようにdataclass内で、__post_init__()
関数を定義することで、初期化時の処理を書くことができます。
ここにisinstance
を使用して型チェックしましょう
@dataclasses.dataclass(frozen=True)
class User:
user_name: str
user_id: int
def __post_init__(self):
if not isinstance(self.user_name, str):
raise Exception
if not isinstance(self.user_id, int):
raise Exception
実行結果
> python .\dataclassテスト.py
1:ヒノヤコマ
Traceback (most recent call last):
File ".\dataclassテスト.py", line 17, in <module>
c_fail = User(user_name=2, user_id='ファイアロー')
File "<string>", line 4, in __init__
File ".\dataclassテスト.py", line 10, in __post_init__
raise Exception
Exception
期待通り、例外を出すことができました!
変数が多いと型チェックが大変
いままでの例では、user_name
とuser_id
の2つでしたが、たくさんあると大変です。
そのため、__post_init__
で書いた型チェックを変数の数だけますようにしたいですね。
というわけで書いたのが下記になります。
@dataclasses.dataclass(frozen=True)
class User:
user_name: str
user_id: int
def __post_init__(self):
# 1. asdictでUserインスタンスをdict型に変換
user_dict = dataclasses.asdict(self)
# 2. self.__annotations__から期待値の型を取得
# self.__annotations__には引数の名前とその指定する型がdictで入ってます。
# これから期待値の型を取得して、isinstanceで型チェックします。
for user_arg_name, user_arg_expected_type in self.__annotations__.items():
# 3. isinstance実行
# dict型に変換したUserから、アノテーションのKeyで指定して対象の変数を取り出して実行します。
if not isinstance(user_dict[user_arg_name], user_arg_expected_type):
print(f'{user_arg_name} is not ok')
raise Exception
else:
print(f'{user_arg_name} is ok')
細かいことはコメントで入れました。
asdict
でインスタンスが持つすべての引数(変数)を取得し、self.__annotations__
で期待値の型を取得して、isinstance
をかけます。
コメントアウトやprint
を除くと、4、5行程度で書けます
実行結果
> python .\dataclassテスト.py
user_name is ok
user_id is ok
1:ヒノヤコマ
user_name is not ok
Traceback (most recent call last):
File ".\dataclassテスト.py", line 21, in <module>
c_fail = User(user_name=2, user_id='ファイアロー')
File "<string>", line 4, in __init__
File ".\dataclassテスト.py", line 13, in __post_init__
raise Exception
Exception
このコードの弱点
typingなどで型を指定すると、この方法だとうまくいきません。
下記はコメントを抜いて、List[int]
で引数の型を指定したもの。
import dataclasses
from typing import List
@dataclasses.dataclass(frozen=True)
class User:
user_name: str
user_id: int
status_list: List[int]
def __post_init__(self):
user_dict = dataclasses.asdict(self)
for user_arg_name, user_arg_expected_type in self.__annotations__.items():
if not isinstance(user_dict[user_arg_name], user_arg_expected_type):
print(f'{user_arg_name} is not ok')
raise Exception
else:
print(f'{user_arg_name} is ok')
status_list=[50,51]
c = User(user_name='ヒノヤコマ', user_id=1, status_list=status_list)
print(f'{c.user_id}:{c.user_name}')
c_fail = User(user_name=2, user_id='ファイアロー', status_list=status_list)
print(f'{c_fail.user_id}:{c_fail.user_name}')
以下のようにlistのところでエラーが出ています。
> python .\dataclassテスト.py
user_name is ok
user_id is ok
Traceback (most recent call last):
File ".\dataclassテスト.py", line 27, in <module>
c = User(user_name='ヒノヤコマ', user_id=1, status_list=status_list)
File "<string>", line 5, in __init__
File ".\dataclassテスト.py", line 19, in __post_init__
if not isinstance(user_dict[user_arg_name], user_arg_expected_type):
File "C:\Users\proje\AppData\Local\Programs\Python\Python37\lib\typing.py", line 708, in __instancecheck__
return self.__subclasscheck__(type(obj))
File "C:\Users\proje\AppData\Local\Programs\Python\Python37\lib\typing.py", line 716, in __subclasscheck__
raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks
なぜだめなのか
以下を実行するとわかりますが、List[int]は型を指定するための型なので、実際のlist型ではありません。
>>> from typing import List
>>> print(type(List[dir]))
<class 'typing._GenericAlias'>
型チェック時に変換する必要がありそうですね。
おわりに
__post_init__
を使用して型チェックするのは問題なさそうです。
ですが、型を指定する際にtyping
を使用していると、for文で回してチェックするのはもう一工夫必要そうです。
mypy
などでコード内にいれるのではなく、外から確認するのもありかもしれません。
(push時に自動確認するなど、環境がしっかりしていればこちらもありですね)
参考
- Python Documentation contents dataclasses --- データクラス