Pydanticでバリデーションをスキップして任意のタイミングでバリデーションする

FastAPI で API を開発していて「リクエストを受けたタイミングではバリデーションはせずに別のタイミングでバリデーションをかけたい」といった要件があり、苦戦したので纏めておく。

前提

  • Python 3.11.3
  • Pydantic 2.6.4

SkipValidation

SkipValidation でバリデーションをスキップすることができるが、任意のタイミングでバリデーションを実行しても Warning は出力されるもののバリデーションエラーとはならずスキップされる。シンプルにバリデーションをしたくないケースにはマッチする。フィールド単位で指定する必要があるため、フィールド数が多かったりネストされたモデルの場合はコード量が増え見通しが悪くなりそう。

from pydantic import BaseModel, SkipValidation


class Item(BaseModel):
    name: SkipValidation[str]
    price: SkipValidation[float]

m1 = Item(name=1, price="foo")
print(m1)  # -> name=1 price='foo'

m1.model_validate(m1.model_dump())  # ->
# /path/to/pydantic/functional_validators.py:702: UserWarning: Pydantic serializer warnings:
#   Expected `str` but got `int` - serialized value may not be as expected
#   function=lambda v, h: h(v), schema=original_schema
# /path/to/pydantic/functional_validators.py:702: UserWarning: Pydantic serializer warnings:
#   Expected `float` but got `str` - serialized value may not be as expected
#   function=lambda v, h: h(v), schema=original_schema

docs.pydantic.dev

docs.pydantic.dev

model_construct

model_construct メソッドを利用することでやりたいことが実現できた。

from pydantic import BaseModel


class Item(BaseModel):
    name: str
    price: float


m1 = Item.model_construct(name=1, price="foo")
print(m1)  # -> name=1 price='foo'

m1.model_validate(m1.model_dump())  # -> ValidationError

デメリットとしては以下が挙げられる。ドキュメントに記載されている通り、バリデーション済みのデータ、信頼できるデータにのみ利用した方が良い。

  • フィールドを指定する際にエディタ補完が効かない
  • フィールドで定義していないものを指定できる
    • 例: Item.model_construct(name=1, price="foo", bar="bar")
  • バリデーションされていないデータのため後続で DB に INSERT するといった場合に型の不整合が発生する可能性がある
  • meta: MetaModel といったネストされたモデルがある場合はコード量が増え見通しが悪くなる
    • 例: Item.model_construct(name=1, price="foo", meta=Meta.model_construct(author=1))

docs.pydantic.dev

docs.pydantic.dev

おわりに

Pydanticでバリデーションをスキップして任意のタイミングでバリデーションする方法を紹介した。 この方法はあまり推奨されることではないため、プロダクトの要件に左右されると思うが本当にバリデーションをスキップする必要があるのかを考えたほうが健全。

2024年3月

育児

3 歳 4 ヶ月になった。

  • あかちゃんのことを「あかちゃま」という
  • 久々のなんでもイヤイヤ
    • バナナジュースで止んだ
  • 久々の胃腸炎でめちゃ吐いた
    • 泣くほど辛いとかではなさそうだけど医療センターで診てもらった
    • 下痢はなくて嘔吐だけ(むしろ便秘気味)
    • 朝ごはんのトーストが食べられなくて涙ポロリ
      • 一気に食べるとよくないので生食パン少しあげたら静かに泣いた
  • 友達家族が遊びに来た
    • 北米仕様っぽいランドクルーザープラドのおもちゃもらった
    • トーマスのおもちゃ貸して IKEA のリラブーぶつけられてギャン泣き
    • 皆でカレー食べた
  • 麻婆豆腐をご飯と一緒に食べると美味しいことに気づく
  • 会社の先輩に会いにいった
    • 持っていったさつまいもとサンドイッチとラスクを外で食べた
    • 先輩にもあげた
  • お別れ会に参加した
    • ママどうしの懇談会があってママたちは泣いていたらしい
    • オレもお別れ会で歌を歌っている皆を見て泣いてしまった
    • 息子氏含めみんな人前で立派に話してすごいと思った
      • 好きな食べ物はりんごらしい(初知り)
    • 大好きなお友達と写真とれた
    • 写真のスライドショーも見れた
  • 最後の登園
    • お迎えはパパママふたりでいった
    • 先生方のメッセージと写真が綴られたブックをもらった
      • 大事にしたい
  • ママがもらったお花をクンクン嗅ぐ

ついに卒園してしまった。。2 年間本当にお世話になりました。この園で息子氏はとても成長したと思うし、パパママもたくさん勉強させてもらった。 転園先も良い園だから楽しんでくれるといいな。元の園にはまた遊びにいきたい。

仕事

  • フロント/バックエンドのバリデーションを実装した
    • Pydantic のバリデーション楽
  • Pydantic のメジャーバージョンアップ(1 系 -> 2 系)した
    • ほとんど Migration script で更新できたのですんなりいった
    • Model の数が多くて差分が 5000 くらいあってレビュー大変だったと思う..
    • 特に大きな問題もなく進んでよかった
    • バージョンアップによってバグが発覚する副次的な効果もあった
  • 江の島でワーケーション
    • フリスビーもってって皆でやった楽しかった
  • タスクの文脈を読み違えてとんちんかんな変更をしてしまって手戻りが発生した
    • 「たぶんこうすればいいだろう」という曖昧な理解のまま実装を進めたのがよくなかった
    • 詳しいメンバーがいるので早いタイミングで相談しておけばよかった

その他

  • 「Effective Python 第2版」を購入した
    • ちまちま読んでいきたい

www.oreilly.co.jp

2024年2月

育児

3 歳 3 ヶ月になった。

  • 「あ、そうだ!おもいついた!」という
  • 雪に大興奮
  • 七五三の撮影でノリノリ
    • パパママは別部屋で待機
    • スタッフさんありがとうございます
    • めちゃくちゃ良い写真だった
  • 選挙で小学校いってついでに遊んだ
    • 同じマンションに住む家族と遊んだ
    • 息子氏うんていぼうに1人でのぼってぶら下がって落ちた
  • 歯医者さんノリノリ
    • お友達が隣でギャン泣きしてても気にしていなかったらしい
  • 寝室で就寝する時に足でパパにかかと落とししまくる
    • 辞めてといっても辞めなかったから何も言わず寝室でてリビングに行った
    • そしたらスンスンしながらママとリビングきた
    • 大丈夫だよって言いながら抱っこして寝室戻ってしばらくスンスンしてたけど寝た
  • ささいな傷で絆創膏はりたがる
    • お風呂で取ったらその痛さでギャン泣き
    • その痛さでまた絆創膏をはり。。の無限ループ
  • 転園先の保育園に面談にいった
    • 先生方よさそうで安心
    • ただ今の園の方が最高
    • 園長先生はママにしか話してない感じだった悲しみ
  • ロマンスカーミュージアムにいった

今の保育園もあと 1 ヶ月..楽しむぞい!

仕事

  • Amplify Authenticate と格闘した
    • Cognito と外部 IdP との連携設定など
  • 外部システムとの接続テストに励んだ
    • やはり curl コマンドは偉大
  • フロント関連のタスクを意識して対応した
    • デザイン修正やボタンの onClick から呼ばれる関数の作成/修正など
    • 詰まったときはメンバーにモブプロを申し込んだ
    • 不明点は踏み込んで質問した
  • バックエンドのリファクタやパフォーマンスチューニングすすめた

その他

  • しずかなインターネットはじめてみた
    • 『Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する』とても良い
    • 早速業務に活かせた
  • Python 関連の薄いエントリ書いた

sizu.me

enokawa.hatenablog.jp

enokawa.hatenablog.jp

【Python】filter関数入門

業務で filter 関数を使うことがあり、まだ理解が足りない点が多いのでアウトプットして知識を深めていく。

docs.python.org

filter 関数とは

Python の組み込み関数の 1 つで、import 文不要で動作する。構文は filter(関数, イテラブル) であり、イテラブルを関数に渡して真だった場合の要素からイテラブルを生成する。for 文を使わなくてもよい。

>>> def f(item):
...     return item == 1

>>> list(filter(f, l))
[1]

filter 関数の返り値の型は filter 型となる。以前書いた zip 関数とは違って遅延評価ではなく、変数に代入して処理することも可能のようだ。配列で返してほしい場合は list でラップするするとよさそう。

>>> value = filter(f, l)
>>> print(value)
<filter object at 0x1052eb1f0>
>>>
>>> type(value)
<class 'filter'>
>>>
>>> list(value)
[1]

enokawa.hatenablog.jp

filter 関数はジェネレータ式というものやリスト内包表記と同等の動きをするらしい。

>>> list((item for item in l if f(item)))
[1]
>>> 
>>> [item for item in l if f(item)]
[1]

なお、filter(function, iterable) は、関数が None でなければジェネレータ式 (item for item in iterable if function(item)) と同等で、関数が None なら (item for item in iterable if item) と同等です。

https://docs.python.org/ja/3/library/functions.html#filter

今関わっているプロジェクトは filter 関数の第 1 引数で指定する関数にラムダ式を用いていた。

>>> list(filter(lambda x: x == 1, l))
[1]

雑感

そろそろ Python 3 年生だが、list 関数や zip 関数、filter 関数やリスト内包表記はまだ読みづらいと感じる。頭がごちゃってしまう。ただこれは Pythonic 思考に反する気がしているので慣れが必要だとも思っている。どんどん書いていこう。ただ参考にした記事だと filter 関数よりもリスト内包表記やジェネレータ式を使った方が好ましいとの記載があったので、また調べてアウトプットしようと思う。

参考

note.nkmk.me

【Python】zip関数入門

業務で zip 関数を使うことがあり、まだ理解が足りない点が多いのでアウトプットして知識を深めていく。

docs.python.org

zip 関数とは

Python の組み込み関数の 1 つで、import 文不要で動作する。複数のイテラブルから tuple を生成する。

>>> for item in zip([1, 2, 3], ['sugar', 'spice', 'everything nice']):
...     print(item)
...
(1, 'sugar')
(2, 'spice')
(3, 'everything nice')

zip 関数の返り値の型は zip 型となる。また zip 関数は遅延評価のため、変数に代入して処理されることはないようだ。基本 for ループ内で処理したり list でラップする必要があると考えた方がよさそう。

>>> value = zip([1, 2, 3], ['sugar', 'spice', 'everything nice'])
>>> print(value)
<zip object at 0x104d2d7c0>

>>> type(value)
<class 'zip'>

>>> dict(value)
{}

zip() は遅延評価です: イテラブルが for ループに渡されたり、 list でラップされたりするなどして反復処理されるまで、要素が実際に処理されることはありません。

https://docs.python.org/ja/3/library/functions.html#zip

Key-Value な形に加工することもできそう。

>>> for k, v in zip([1, 2, 3], ['sugar', 'spice', 'everything nice']):
...     print(k, v)
...
1 sugar
2 spice
3 everything nice

>>> dict(zip([1, 2, 3], ['sugar', 'spice', 'everything nice']))
{1: 'sugar', 2: 'spice', 3: 'everything nice'}

イテラブルってなんだっけ

以下の記事が参考になった。list や tuple を使うケースが多そう。

qiita.com

zip 関数で渡されたイテラブルの要素数が異なる場合の挙動

第 1 引数の要素数は 4, 第 2 引数の要素数が 3 の場合、第 1 引数の 4 番目の要素が無視される。

>>> for item in zip([1, 2, 3, 4], ['sugar', 'spice', 'everything nice']):
...     print(item)
...
(1, 'sugar')
(2, 'spice')
(3, 'everything nice')

逆に第 1 引数の要素数を 3, 第 2 引数の要素数を 4 とした場合、第 2 引数の 4 番目の要素が無視される。

>>> for item in zip([1, 2, 3], ['sugar', 'spice', 'everything nice', 'bad']):
...     print(item)
...
(1, 'sugar')
(2, 'spice')
(3, 'everything nice')

これはドキュメントに記載の通り。

デフォルトでは、 zip() は最も短いイテラブルが消費しきった時点で停止します。より繰り返し数の長いイテラブルの残りの要素は無視して、結果を最も短いイテラブルの長さに切り詰めます:

https://docs.python.org/ja/3/library/functions.html#zip

素数が異なる場合はエラーとさせたい場合は strict=True オプションを付与することで ValueError を 出すことができる。

>>> for item in zip([1, 2, 3], ['sugar', 'spice', 'everything nice', 'bad'], strict=True):
...     print(item)
...
(1, 'sugar')
(2, 'spice')
(3, 'everything nice')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: zip() argument 2 is longer than argument 1

参考

note.nkmk.me

2024年1月

育児

3 歳 2 ヶ月になった。

  • 「違うか」と言う
  • 親戚の結婚式で沖縄に帰省した
  • 口臭いと言うと悲しむ
    • ごめんよ。。
  • パズルが上手
  • 従姉妹の結婚式で帰省した
    • 飛行機は全然余裕
    • ゆいレールに乗って楽しそうだった
    • 何も遊ぶものがなくメインプレイスでトミカ調達
    • 結婚式は服を着てくれず肌着で乗り込んだ
      • りんごジュースを飲ませて着せた
  • パンと一緒に親指を噛んでギャン泣き
  • 「はい」の返事が上手

仕事

  • Amplify と格闘した
    • 既存 Ampilfy をインポートするのに苦戦した
    • 複数 AWS アカウントにまたがって Amplify をコードで管理するのに苦戦した

その他

  • Findy さん主催の t-wada さんのイベントに参加したとてもよかった
    • 生々しい話が聞けたしレガシーコードをどう改善していくのかのプロセスが参考になった
    • t-wada さんの引き出しの広さがすごい(語彙力)
  • HTTP を完全理解したいので『Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する』を購入した

findy.connpass.com

gihyo.jp

2023年振り返り

今年も振り返っていく。

enokawa.hatenablog.jp

子育て

今年はいろんな家族とたくさん触れあえた年だった。いとことその子どもに会いに行ったり、通っている保育園を退園した子どもとその家族に会いにいったり、同じマンションに住む子どもとその家族と遊んだり、保育園に通うみんなと登園時にじゃれあったりパパママさんとお話したり、沖縄で姪っ子たちと遊んだり。もともとは引っ込み思案で親御さんとお話するきっかけが作れずに関係が続かないってのが常だったが、今年はなぜか積極的にコミュニケーションをとるようにした。その結果プライベートで遊んだりお家に招いて一緒にお昼ごはんやおやつを食べるぐらいの関係になり、とても嬉しい。息子氏と遊ぶのも楽しいが、他の子と遊ぶのも楽しい。

たくさん触れあえたのは主に保育園の先生方、園児とその親御さんのお陰だと思う。ここで園の自慢をさせて欲しい。園の運営方針は子どもを第一に考えることで、あくまで主役は園児、先生方はそのサポートをするスタンスをとっている。例えば息子氏が 1 人夢中になって遊んでいて、ご飯の時間になっても「食べない」といった場合は強制せずに「いま夢中なんだね。じゃあご飯食べたくなったらおいでね」といった具合。逆に暇そうな時は「これやってみる?」「A くんと一緒に遊んでみる?」「えほん読む?」など選択肢を常に子どもに与えている。この点はとても素晴らしいと思っていて真似している。コドモンでの連絡帳でのやりとりも毎日たのしいし、先日は退園した子との写真を共有したりもした。この園は小規模保育園で、だからこそこんなに質の高い保育やコミュニケーションができるのだと思う。来年度からは大きめの園に転園する予定のため少し不安がある。ただ息子氏はあまり環境に左右されないタイプなのでたぶんすぐに慣れると思う。あぁ退園したくないな〜(泣)。

息子氏は昨年に引き続き心身ともにぐんぐん成長し、抱っこするのもつらくなってきた(笑)。発語も順調で、簡単なコミュニケーションはできるようになった。最近びっくりしたのは、以前お友達に自分のおもちゃを取られていたのを聞いて嫌だったかと聞くと「嫌だった」と。立派なコミュニケーションが成立している。オレたち親も「まだ子どもだから」と決めつけたり息子氏の行動を先回りしてなんでも手を出すのはやめなきゃなと思った。

仕事

初めてスクラムでの開発に参画した。とても新鮮で自分の性に合っていると強く感じた。以前関わっていたプロジェクトはウォーターフォールで、フロントエンドはフロントエンドチームが、バックエンドはバックエンドチームが担当するといった体制で違和感を感じていた。もともとバックエンドをメインとしていたため、フロントエンドに関する問題が発覚した場合は軽微な修正であってもフロントエンドチームに依頼する必要があった。スクラムは基本ワンチームで 1 つのプロダクトを作り上げようという思想で、エンジニアの作業範囲を明確に決めていない。そのためオレがフロントエンドに手を出してもよくて、それがとても心地よかった。まだ数は少ないが、フロントにも Pull requests を出したりしている。

スプリントプランニングやバックログリファインメントでサービスの仕様について質問・提案をしたりできるのも魅力で、実際にオレが提案したものが PBI 化されたりしたのが嬉しかった。言われた通りのモノを作るのではなく、自分たちで仕様や機能を考えて実装していく、そのステップを一定期間繰り返していくのが改善を積み重ねている実感があってとても面白い。

このプロジェクトで CDK にもチャレンジした。もともとは Terraform 派だったが、CDK はコードの記述量を抑えながらも欲しいリソースを構築してくれるのがとても便利だと思う。当初は「知らないリソースができている。。」「宣言したリソースだけ立ち上がってほしい」というネガティブなイメージしかなかったが、学習コストも少なくてプログラマブルAWS リソースを定義できるし、スクラムとの相性も良い気がしている。フロントエンドは TypeScript で書いているため、フロントエンドに強いメンバーからアドバイスも受けることができる。

勉強

読んだ(読んでいる)本は以下のエントリと変わりない。

enokawa.hatenablog.jp

GitHub の活動はこんな感じ。以前友人と始めた個人開発を再開(1 人)した。

自作キーボード

新しい趣味(?)として自作キーボードを始めた。たのしい。(小並感)

enokawa.hatenablog.jp

enokawa.hatenablog.jp

2024年どうする

変わらず家族最優先でいく。仕事もプライベートも充実させるぞい!

ということで

来年から本気だす。