はじめに
「アタイも挑戦したい、管理人みたいに」1
『ジョゼと虎と魚たち』めっちゃいいですよね? nikkieです。
タイトルの値オブジェクトとは、値を扱うための専用クラス2です。
これを「Pythonの@dataclasses.dataclass
を使って書いたらどうなるのか」手を動かしました。
この記事は、分かったことの備忘録です。
おことわり:「今の自分が書いたらこんな感じかな」というのをまとめました。
技術的な誤りや伸びしろにお気づきの際は、コメントやTwitter @ftnext までお知らせいただけると大変ありがたいです。
目次
- はじめに
- 目次
- 経緯
- 値オブジェクトとは
- 値オブジェクトの実装要件
- 値オブジェクトの例:競走馬名
- 馬の名は。値オブジェクト RaceHorseName 🐴
- 実装ポイント
- Future works
- 終わりに
「アタイ」ヒロインならぬ、「アタイ」オブジェクトについてです
アタイは大阪弁じゃないんですね。
— nikkie 📣PyCon JP 2021 スタッフ募集中! (@ftnext) 2021年2月13日
田辺聖子さんの原作には確かに一人称がアタイになった理由がありました(しんみり)
プログラミングを生業としていると、アタイという音が値に脳内変換されることがあって、職業病を感じます。
逆も然りでアタイオブジェクトとか https://t.co/9eUMpBWa1b
経緯
先日TECH StreetさんのPythonエンジニア勉強会でPythonのobjectについて10分話しました。
本日 #テックストリート さんの「Pythonエンジニア勉強会」でのLTスライドですhttps://t.co/V96bTbcLvo
— nikkie 📣PyCon JP 2021 スタッフ募集中! (@ftnext) 2021年4月27日
「object活用ことはじめ 〜dataclassと特殊メソッド〜」
dataclassと特殊メソッドで最近Pythonがとても楽しく書けている😆のを共有することにしました。
よろしくお願いしますー
紹介した例は、値オブジェクトやファーストクラスコレクションを意識して実装しています。
準備の中で『現場で役立つシステム設計の原則』を参照したところ、「意識しているけど、値オブジェクトやファーストクラスコレクションにはできていないなー」と、目の前に広がる圧倒的な伸びしろを感じました。
そこで、LTとは別に実装を突き詰める機会を設けました。
今回は、値オブジェクト編のアウトプットです。
値オブジェクトとは
※『現場で役立つシステム設計の原則』の1章を参考にしています
値オブジェクトの実装は「基本データ型のインスタンス変数を1つか2つ持つだけの小さなクラス」3です。
基本データ型をインスタンス変数としてラップして、独自の型(クラス)を作ると捉えています。
値オブジェクトを使う目的は、業務のニーズに合っていない基本データ型の代わりに、業務のニーズに合わせた値の範囲を使うためと理解しました。
値オブジェクトの利点はいくつも紹介されています。
イチオシはこちら、関数の引数がめちゃくちゃ分かりやすくなりますね!
def amount(unit_price: Money, quantity: Quantity) -> Money: if quantity.is_discountable(): return discount(unit_price, quantity) return unit_price.multiply(quantity.value)
1章の「『型』を使ってコードを分かりやすく安全にする」のJavaのコードをPythonで書き直してみました。
第1引数がMoney型、第2引数がQuantity型という型ヒントが分かりやすいと思います。
値オブジェクトの実装要件
- 基本データ型を属性として1つか2つ持つ
- 👉 簡単な例で練習したいので、1つにします
- 不変
- 完全コンストラクタ
値オブジェクトの例:競走馬名
TECH StreetさんでのLTの題材にした競走馬名を表すクラスを例に選びました。
※競走馬の名前を値オブジェクトにするというのは、業務アプリケーションからかけ離れていると思います。
今回の取り組みは「値オブジェクトの実装方法」を見つける練習なので、これでいきます🐴
日本における馬名登録のルール6
- カタカナ9文字以内7
- 「ヰ」・「ヱ」は使用できない
- 2文字以上9文字以内
馬の名は。値オブジェクト RaceHorseName
🐴
完成したコード
from __future__ import annotations import re from collections.abc import Iterable from dataclasses import dataclass from typing import ClassVar @dataclass(frozen=True) class RaceHorseName: """カタカナの馬名を表すクラス""" value: str MIN_LENGTH: ClassVar[int] = 2 MAX_LENGTH: ClassVar[int] = 9 INVALID_CHARACTERS: ClassVar[Iterable[str]] = ("ヰ", "ヱ") def __init__(self, value: str): # 完全コンストラクタの実装例 length = len(value) if length < self.MIN_LENGTH: raise ValueError(f"不正: {self.MIN_LENGTH} 文字未満") if length > self.MAX_LENGTH: raise ValueError(f"不正: {self.MAX_LENGTH} 文字超") if not self.is_katakana_only(value): raise ValueError("不正: カタカナではない文字") if self.does_include_invalid_characters(value): raise ValueError(f"不正: 使用禁止文字 {self.INVALID_CHARACTERS} を含む") object.__setattr__(self, "value", value) @staticmethod def is_katakana_only(string: str) -> bool: return bool(re.fullmatch(r"[\u30A0-\u30FF]+", string)) @classmethod def does_include_invalid_characters(cls, string: str) -> bool: return any(c in string for c in cls.INVALID_CHARACTERS)
不変なことの確認
>>> rice = RaceHorseName("ライスシャワー") >>> rice.value = "ミホノブルボン" Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 4, in __setattr__ dataclasses.FrozenInstanceError: cannot assign to field 'value'
frozen=True
により、ラップした値を変更できません。
frozen=True を dataclass() デコレータに渡すことで、不変性の模倣はできます。 このケースでは、データクラスは
__setattr__()
メソッドと__delattr__()
メソッドをクラスに追加します。 これらのメソッドは起動すると FrozenInstanceError を送出します。8
完全コンストラクタであることの確認
- 2文字未満:ラ
- 9文字超:トッテモライスシャワー
- カタカナではない文字を含む:ライスshower
- 使えないカタカナ:ラヰスシャワー
いずれもValueError
が送出されます
>>> RaceHorseName("らイスシャワー") Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: 不正: カタカナではない文字
実装ポイント
frozen=True
と完全コンストラクタ
イニシャライザ(__init__
)でvalue属性に値を代入する必要があります。
これは以下の書き方をする必要がありました。
object.__setattr__(self, "value", value)
frozen=True を使うとき、実行する上でのわずかな代償があります:
__init__()
でフィールドを初期化するのに単純に割り当てることはできず、object.__setattr__()
を使わなくてはなりません。9
イニシャライザでself.value = value
と書くと、以下が出てしまいます(frozenにしたのでごもっともなのですが、けっこう焦りました)。
dataclasses.FrozenInstanceError: cannot assign to field 'value'
これは object.__setattr__()
10で回避できました。
dataclassのクラス変数
typing.ClassVar
を初めて知りました。
クラス変数であることを示す特別な型構築子です。11
フィールドが ClassVar の場合、フィールドとは見なされなくなり、データクラスの機構からは無視されます。12
dataclassのクラス変数の理解があやふやだと分かって、いくつか手を動かしました。
型ヒントをつけなければクラス変数 なんですね。
@dataclass class C: a: int b: int = 0 d = "Class var"
>>> c = C(108) >>> c C(a=108, b=0) >>> c.d 'Class var' >>> C.d 'Class var'
クラス変数d
に型ヒントを付けるときにClassVar
を使うという理解です。
これで型チェッカーの支援を受けられるとのこと13。
クラス変数の扱いをさらに深めたければ、@dataclasses.dataclass
の実装を見ることになりそうです。
脱線:すべてカタカナのチェック
最後に値オブジェクトとは別の話題を。
文字列がすべてカタカナかの判定は https://note.nkmk.me/python-re-regex-character-type/#_7 を参考にしています。
re.fullmatch
で「文字列全体が正規表現にマッチするか」検証- カタカナの並びを表す正規表現は、カタカナを表すブロックの文字の1回以上の繰り返し
- Wikipediaに「ブロックの一覧」があり、Katakanaも載っていました
- https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF_(Unicode)
Future works
今回書いたコードは暫定解(よりよい方法で棄却されうる)と思っているので、今後試したい事項を挙げておきます:
__slots__
はどうなんだろう?- 知ったきっかけ:dataclassと __slots__ について調べた - Keep on moving
frozen=True
より利点がある?:https://stackoverflow.com/a/28059785
- サードパーティライブラリを使う
- PyDanticは完全コンストラクタをサポートしてくれる?(FastAPI Tutorialで触った程度の印象)
終わりに
値オブジェクト、dataclass
のfrozen=True
を知っていた14ので、ぱぱっと実装できるかと思いきや、意外と時間がかかりました。
完全コンストラクタの実装で、値の検証に使う変数やメソッドをクラスに持たせていき、「競走馬名をクラスで表現できた!」と満足しています😃
次は、ファーストクラスコレクション編です!(執筆時期は未定です)
-
アタイつながりで持ってきました😝 https://youtu.be/P5LLhoKfI1E?t=36↩
-
増田 亨『現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法』 (Japanese Edition) 1章 (Kindle の位置No.687). Kindle 版. ↩
-
『現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法』 (Kindle の位置No.689-690)↩
-
https://docs.python.org/ja/3/library/dataclasses.html#dataclasses.dataclass↩
-
『現場で役立つシステム設計の原則 〜変更を楽で安全にするオブジェクト指向の実践技法』 (Kindle の位置No.735-736)↩
-
馬名登録のルール、けっこう面白かったです。「オレハマッテルゼ」なんてあるんですね🤣 類似しているとNGだそうですが、「チョウカイテイオー」は却下で、「ナイキシャトル」は採用🤔↩
-
「アルファベットの馬名と合わせて登録する」そうですが、今回は考えません。だって、ウマ娘、カタカナの印象が強いじゃないですか!↩
-
https://docs.python.org/ja/3/library/dataclasses.html#frozen-instances↩
-
https://docs.python.org/ja/3/library/dataclasses.html#frozen-instances↩
-
https://docs.python.org/ja/3/reference/datamodel.html#object.__setattr__
(URL末尾の__setattr__
が太字になってしまいます😢)↩ -
https://docs.python.org/ja/3/library/typing.html#typing.ClassVar↩
-
https://docs.python.org/ja/3/library/dataclasses.html#class-variables↩
-
Here ClassVar is a special class defined by the typing module that indicates to the static type checker that this variable should not be set on instances. ref: https://www.python.org/dev/peps/pep-0526/#class-and-instance-variable-annotations↩
-
hoboroさんのトークで知りました。詳しくは イベントレポート | オンラインで開催された #pycon_shizu 、私の知らないPythonがいくつもありました! - nikkie-ftnextの日記↩