Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pythonで実現する堅牢なシステム設計 〜イミュータブルがもたらす恩恵〜

Avatar for Masaya Masaya
September 25, 2025
150

Pythonで実現する堅牢なシステム設計 〜イミュータブルがもたらす恩恵〜

PyCon JP 2025 登壇資料

Avatar for Masaya

Masaya

September 25, 2025
Tweet

Transcript

  1. ミュータブル / イミュータブル とは? ミュータブル 一度作られたオブジェクトの中身(値や構造)を変更できる 代表的な例 list dict set

    class(特に指定がない場合) イミュータブル 一度作られたオブジェクトの中身(値や構造)を変更できない 代表的な例 int str float tuple None
  2. ミュータブル / イミュータブル とは? ミュータブル 1 2 3 4 5

    6 7 8 9 10 11 12 13 # リストは変更可能 [ , , ] ( ( )) # 例:140220164169920 # 値の追加 . ( ) ( ) # [1, 2, 3, 4] ( ( )) # 140220164169920 (同じID) # 値の変更 [ ] ( ) # [99, 2, 3, 4] ( ( )) # 140220164169920 (同じID) my_list id my_list my_list append my_list id my_list my_list my_list id my_list = 1 2 3 4 0 = 99 print print print print print メモリ上では同じオブジェクトが変更される 99 2 3 4 1 2 3 id: 140220164169920 id: 140220164169920 イミュータブル 1 2 3 4 5 6 7 8 9 10 # 整数は変更不可能 ( ( )) # 例:9784960 # 新しい値を代入 ( ) # 6 ( ( )) # 9785024 (異なるID) # 実際には新しいオブジェクトが作成されている x id x x x x id x = 5 = + 1 print print print メモリ上では新しいオブジェクトが作成される 5 6 id: 9784960 id: 9785024
  3. ミュータブル / イミュータブル とは? 注意点 1 2 3 >>> =

    1 2 3 >>> 1 0 = 4 >>> t t t ( , [ , ]) [ ][ ] ( ) # (1, [4, 3]) print イミュータブルなオブジェクト内の ミュータブルオブジェクトは変更可能 補足 イミュータブルのオブジェクトは オブジェクトの値を操作するための 関数が用意されていない 1 2 3 4 5 6 7 >>> = 1 2 >>> 0 = 3 1 < > 0 = 3 ~^^^ t t Traceback most recent call last File line module t TypeError object does support item assignment ( , ) [ ] ( ): , , [ ] : "<python-input-11>" in 'tuple' not
  4. ミュータブルの危ないところ1 関数のデフォルト引数にミュータブルオブジェクトを指定すると それが共有され続ける 問題点 1 2 3 4 5 6

    7 8 9 10 11 12 13 14 15 def return 'apple' print 'banana' print 'cherry' print item items items append item items result1 add_item item result1 result2 add_item item result2 result3 add_item item result3 add_item( , []): . ( ) # 1回目の呼び出し ( ) ( ) # ['apple'] # 2回目の呼び出し - 空のリストを期待するが... ( ) ( ) # ['apple', 'banana'] 予想外! # 3回目の呼び出し - さらに積み重なる! ( ) ( ) # ['apple', 'banana', 'cherry'] = = = = = = = Pythonのデフォルト引数は関数定義時に一度だけ評価される ミュータブルなデフォルト引数(リスト、辞書など)は全ての関数 呼び出しで共有される 関数呼び出し時ではなく関数定義時に作られた同一オブジェクトが 使われる そのため、前の呼び出しでの変更が次の呼び出しに影響する なぜこうなるのか? 関数定義時 1回目の呼び出し 2回目の呼び出し 3回目の呼び出し 何が起きているか? apple apple banana apple banana cherry [ ]
  5. ミュータブルの危ないところ1 回避策 1の実装例 デフォルトは Noneにして、関数内で新しいリストを作成する イミュータブルなオブジェクトをデフォルト引数に使用する 1 2 3 4

    5 6 7 8 9 def None if is None return print "apple" print "banana" print "cherry" item str items list str list str items items items append item items add_item add_item add_item add_item( : , : [ ] ) [ ]: : [] . ( ) ( ( )) # ['apple'] ( ( )) # ['banana'] ← 期待どおり独立 ( ( )) # ['cherry'] = -> =
  6. ミュータブルの危ないところ2 問題点 リストや辞書を代入すると、コピーではなく参照が渡されるため、片方を変更するともう片方も変更される reference 1 2 3 4 参照共有のメモリ図解 original

    2つの変数が同じメモリ位置を参照している 1 2 3 4 5 6 7 8 9 10 11 12 13 # 配列の参照共有問題 [ , , ] # 参照を共有 . ( ) # 変更を加える [ , , , ] # 意図せず元のリストも変わる [ , , , ] ( ) ( ) # 同じオブジェクト >>> = 1 2 3 >>> = >>> 4 >>> 1 2 3 4 >>> 1 2 3 4 >>> == original reference original reference append reference original id original id reference True listの場合
  7. ミュータブルの危ないところ2 回避策 参照の共有による意図しない変更を防ぐため、オブジェクトをコピーしてから操作する コピー方法 浅いコピー (Shallow Copy) 深いコピー (Deep Copy)

    1 2 3 4 5 6 7 # リストの場合 . () # または ( ) # 辞書の場合 . () new_list original_list copy new_list list original_list new_dict original_dict copy = = = 1 2 3 import copy new_obj copy deepcopy original_obj = . ( ) 入れ子になったミュータブルオブジェクトも完全にコピー 大きなオブジェクトのコピーはメモリ消費が増えるため、適切な場面で使用する
  8. ミュータブル・イミュータブル のメリット / デメリット ミュータブル メリット デメリット 中身をその場で変えられるため効率的 参照を共有していると他の変数からも影響を受ける 状態管理が複雑になりやすい

    メモリの再割り当てが少ない 更新処理が直感的で実装しやすい イミュータブル メリット デメリット 安全性が高く、意図しない変更や追加が起きない 新しい値を作るときはコピーが必須なので メモリ使用量が増える可能性がある 副作用が少なく、テストしやすい
  9. 注意点:実行時に型の強制はされない 1 2 3 4 5 6 7 8 9

    from import True class print dataclasses dataclass frozen Order name str o Order name type o name @dataclass( ) : : ( ) ( ( . )) # <class 'int'> = = =1000 dataclass 基本情報 特徴 Pythonの標準ライブラリ データを格納するクラスを宣言的に定義する __init__, __repr__, __eq__, 比較演算子などを自動生成 ユースケース ビジネスロジック薄めのデータ構造の定義で活用 frozen=Trueにより、属性の追加・変更を防止 1 2 3 4 5 6 7 8 from import True class dataclasses dataclass frozen Order id str customer_id str product_id str ordered_at datetime @dataclass( ) : : : : : = コード例
  10. Pydantic 基本情報 特徴 外部ライブラリ データを格納するクラスを宣言的に定義する データの自動変換と型強制 ユースケース 外部データ(API)の検証と変換で利用 型アノテーションによる強力なバリデーション 実行時に型が異なる場合はエラーとなる

    1 2 3 4 5 6 7 # 期待する型でない値を導入するとエラーが発生する ( , ) # ValidationError: 1 validation error for Order # name # Input should be a valid string [type=string_type, # #input_value=10000, input_type=int] # For further information visit https:// #errors.pydantic.dev/2.11/v/string_type o Order id name = = =10000 '0001' 1 2 3 4 5 6 7 8 from import class True pydantic BaseModel ConfigDict Order BaseModel model_config ConfigDict frozen id str name str , ( ): # イミュータブルにする ( ) : : = = コー ド例
  11. イミュータブルデータモデルとは イミュータブルの原則 得られるメリット 従来モデル vs イミュータブルモデル 更新 = 上書き →

    新バージョン追加 オブジェクトは変更不可 変更する代わりに新しいバージョンを作成 すべての状態変化が記録として残る 過去の状態を完全に復元可能 変更の透明性と監査の容易さ 従来の更新モデル 上書きにより履歴が失われる 履歴が完全に保持される イミュータブルデータモデル v1 v1’ v1 v2 v3
  12. ミュータブルモデルのモデル例 1 2 3 4 5 6 7 8 9

    10 11 12 class "ORDERED" "SHIPPED" class None None Status StrEnum ORDERED SHIPPED Order id int customer_id int product_id int status Status ordered_at datetime shipped_at datetime ( ): @dataclass : : : : : : : = = | = 注文テーブル設計例(ミュータブル) カラム名 データ型 NULL許容 id UUID 不可 customer_id UUID 不可 product_id UUID 不可 status STRING 不可 ordered_at DATETIME 不可 shipped_at DATETIME 可 モデル例 1 2 3 4 # 利用例 (...), # DBから復元したもの . . . . () # 発送済みに更新 order Order order status Status SHIPPED order shipped_at datetime now = = = 利用例
  13. ミュータブルモデルのメリット/デメリット メリット 「今の状態」を取得するクエリが簡単 履歴不要なケースでは十分 シンプルな設計で開発初期は速く進む NULLカラムや複数の日時属性が混在し、状態の 変遷が不透明になります。システムの成長に伴い 保守性が低下していきます。 状態管理の課題 デメリット

    履歴が残らず、過去の真実を再現できない 状態管理が複雑になりやすい NULLカラムが増えやすい 短期的なセッション情報や、ステートレスな操 作、履歴や監査が必要ないシステムに向いている ユースケース
  14. 抽象的なものなので理解が難しい Step1. エンティティの抽出 エンティティとは 名前が付けられて他と区別できる対象 明確かつ独立した存在を持つもの 5W1H によるエンティティ抽出 要件文書から以下の要素を抽出することで、 エンティティとその属性の候補を特定する

    W W W W W H Who(誰が):顧客、管理者など What(何を):商品、注文など When(いつ):注文日時、発送日時など Where(どこで):住所、店舗など Why(なぜ):返品理由など How(どのように):支払い方法など たとえば「顧客」「商品」「ショップ」などを、 データとして扱える単位に抽象化したもの 商品 ショップ 顧客
  15. Step1. エンティティの抽出 注文システムの要件例 顧客 商品 注文 注文日時 記録 発送 発送日時

    記録 が を するとき、 を する。 されたときに、 を する。 要件文書に対して、関連する5W1Hに下線を引 き、エンティティを抽出する 抽出のコツ 抽出されたエンティティ候補 Who 注文日時、発送日時 When How 注文、商品 What ー Where 顧客 ー
  16. Step2. エンティティを分類 モデリングにおけるエンティティ分類 エンティティをリソース、イベントの2つに分類する リソースエンティティ イベントエンティティ 特徴: 日時属性を持たない 定義: 「もの」としての実体を表す

    例:顧客、商品、注文 例:発送 特徴: 日時属性を持つ 定義: 「できごと」としての実体を表す 「請求予定日」のように将来の予定を表すものや、 「有効期限」「適用開始日」のようにデータのライフサイクル を表すものは、ここでいう"日時"属性ではない。 イベントエンティティの注意点 エンティティ分類 注文日時、発送日時 リソースエンティティ イベントエンティティ 顧客、注文、商品
  17. Step5. 非依存のリレーションシップを交差エンティティで表す 非依存である場合に、一方のエンティティのキーをもう一方に外部キーとして持たせてしまうと強い依存関係が生 まれる 注文ID 請求書ID 注文ID 注文 請求書 注文ID

    請求書ID 注文 請求書 請求書エンティティが注文エンティティに 依存する形となる 複数注文を1つの請求書で支払う要件などが 登場すると複雑になる 1:1 / 1:N / N:1 / N:N すべて対応 締め請求、部分請求など複雑な要件にも対応しうる 注文ID 請求書ID 注文請求
  18. モデル例 1 2 3 4 5 6 7 8 9

    10 @dataclass( ) : : : @dataclass( ) : : : frozen OrderedEvent order_id OrderId occurred_at datetime frozen ShippedEvent order_id OrderId occurred_at datetime = = True class True class エンティティ 1 2 3 4 5 @dataclass( ) : : : : frozen Order id int customer_id int product_id int =True class イベント
  19. 実際の利用フロー Order 自体は変わらず、「発送されたという事実(イベン ト)」が追加されるだけ 注文が生成されるとき 発送が完了したとき 1 2 3 4

    5 6 7 8 9 10 11 12 13 from datetime datetime order         id         customer_id         product_id ordered_event         order_id order id           occurred_at datetime import = = 1 = 1 = 1 = = = Order OrderId CustomerId ProductId OrderedEvent now ( ( ), ( ), ( ) ) ( . , . () ) 1 2 3 4 5 order # DBから復元 shipped_event order_id order id occurred_at datetime = ... = = = ShippedEvent now ( . . () )
  20. Recustomerの紹介 返品キャンセルのCSおよび 倉庫業務を自動化 返品キャンセル お届け日時 3月16日(水) 17:00 - 19:00 配達完了

    配送状況 配送業者 ヤマト運輸 受取り日時を変更 注文番号 PAL#002 追跡番号 12345678900 お届け先 東京都千代田区 3月29日 07:27 am 配達完了 日本, 東京都港区 購入後の配送追跡から 新たな顧客接点を創り出す 配送追跡
  21. 元々のモデル例 1 2 3 4 5 6 7 8 9

    10 11 12 13 @dataclass( ) : : : : : : : : : : : frozen ReturnOrder id int order_id int customer_id int product_id int status int requested_at datetime approved_at datetime rejected_at datetime completed_at datetime reject_reason str = | | | | True class None None None None 注文テーブル設計例(ミュータブル) 一部幅の都合で省略 カラム名 データ型 NULL許容 id int 不可 order_id int 不可 status int 不可 requested_at datetime 不可 approved_at datetime 可 rejected_at datetime 可 completed_at datetime 可 reject_reason datetime 可 注文テーブル設計例(ミュータブル)
  22. エンティティの抽出、分類 リソースエンティティ イベントエンティティ Customer Product Order ReturnOrder Requested Approved Rejected

    Complete エンティティの分類 リソースエンティティ候補 イベントエンティティ候補 顧客 商品 注文 返品 返品申請 返品承認 返品拒否 返送 エンティティの抽出
  23. モデル例 1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21 22 23 @dataclass( ) : : : : : : @classmethod ():... ( : ) : ( . , ): # このときに既存のオブジェクトを更新するのではなく、 # 新しくオブジェクトを作成している ( . , ..., ) frozen ReturnOrder id UUID order_id OrderId customer_id CustomerId product_id ProductId activity Requested Approved Rejected Completed activity Approved ReturnOrder isinstance self activity Requested Exception obj ReturnOrder id self id activity activity obj = | | | -> = = = True class def def if not raise return initialize set_approved エンティティモデル 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # イベントエンティティ @dataclass( ) : : @dataclass( ) : : @dataclass( ) : : : @dataclass( ) : : frozen Requested created_at datetime frozen Approved created_at datetime frozen Rejected created_at datetime reason str frozen Completed created_at datetime = = = = True class True class True class True class イベントモデル
  24. 実際の利用方法 1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 class def def Usecase return_order_repository execute return_order ReturnOrder return_order_repository new_obj return_order set_approved activity Approved created_at datetime now return_order_repository obj new_obj : ( : ... ): ... : # DBから返品申請済の返品を取得する : ... # DBから取得した返品注文のステータスを変更する . ( ( . ())) # DBに永続化させる ....( ) __init__ = = = = = 返品申請を承認するユースケース