Optionalって何?一言でいうと「Noneかもしれない」を型で表す道具
Optional は、型ヒントの世界で
「この値は、あるときは T 型だけど、ないときは None かもしれない」
という状態を表すための記号です。
もう少し形式的に言うと、
Optional[T] は Union[T, None] の別名
Pythonです。
つまり、Optional[str] は「str または None」という意味になります。
Python 3.10 以降なら、str | None と書けますが、
意味は Optional[str] とまったく同じです。
ここでの超重要ポイントは、
「None になる可能性があることを、型として“明示する”」
というところです。
これが、設計・テスト・バグ防止のすべてに効いてきます。
まずはシンプルな例で「Optionalの感覚」をつかむ
ユーザー名を探す関数の例
ユーザーIDから名前を探す関数を考えます。
def find_user_name(user_id: int) -> str:
...
Pythonこう書いてあると、「必ず str が返ってくる」と読めます。
でも、実際には「見つからないとき」がありますよね。
そのときに None を返す設計にするなら、
型ヒントもそれに合わせてこう書くべきです。
from typing import Optional
def find_user_name(user_id: int) -> Optional[str]:
if user_id == 1:
return "Taro"
return None
Pythonこれで、
「この関数は、str が返るかもしれないし、None かもしれない」
という仕様が、コードとしてはっきり残ります。
Python 3.10 以降なら、こうも書けます。
def find_user_name(user_id: int) -> str | None:
...
Pythonどちらでも意味は同じです。
Optionalがテストと品質に効いてくるポイント
呼び出し側に「Noneのときどうする?」を考えさせる
Optional[str] と書いてある関数を呼ぶ側は、
必ず「None の可能性」を意識することになります。
name = find_user_name(1)
print(name.upper())
Python型ヒントがないと、こう書いてしまいがちです。
でも、Optional[str] だとエディタや静的解析ツールが
「name は None かもしれないよ」と警告してくれます。
それを受けて、呼び出し側はこう書き直します。
name = find_user_name(1)
if name is not None:
print(name.upper())
else:
print("ユーザーが見つかりませんでした")
Pythonこの「None のときどうする?」という分岐は、
本来は最初から設計に含めるべきものです。
Optional を付けることで、
その設計上の前提が「型」として強制されるイメージです。
テストケースの抜けに気づきやすくなる
Optional[str] と書いてある関数をテストするとき、
自然にこう考えるようになります。
見つかったとき(str が返るパターン)
見つからなかったとき(None が返るパターン)
つまり、「少なくとも 2 パターンはテストしよう」と思えるわけです。
型ヒントがないと、
「たまたま見つかるケースだけテストして終わり」
ということが起きやすくなります。
Optional は、「仕様上の分岐」を型で表現することで、
テストの観点を漏れにくくしてくれます。
Optionalを使うときに絶対に押さえておきたい注意点
「なんでもかんでもOptional」は逆効果
便利だからといって、
何でもかんでも Optional を付けるのは逆効果です。
例えば、こういうのはよくありません。
def add(a: Optional[int], b: Optional[int]) -> Optional[int]:
if a is None or b is None:
return None
return a + b
Python本当に None を受け取りたい関数ならいいのですが、
「呼び出し側がちゃんと int を渡すべき」関数なら、
素直にこう書くべきです。
def add(a: int, b: int) -> int:
return a + b
PythonOptional を付けるということは、
「None を渡してもいいし、None を返してもいい」という契約を結ぶことです。
それはつまり、「呼び出し側の責任も増える」ということでもあります。
本当に必要なところだけに Optional を付ける。
「面倒だからとりあえず Optional」はやめる。
ここが品質的にはかなり重要です。
「Noneを返す設計」自体を見直すべき場面もある
例えば、ユーザーが見つからないときに None を返す代わりに、
例外を投げる設計にすることもあります。
class UserNotFoundError(Exception):
...
def get_user_name(user_id: int) -> str:
if user_id == 1:
return "Taro"
raise UserNotFoundError
Pythonこの場合、戻り値は str だけでよく、
Optional は不要です。
どちらが正しいかは「仕様次第」ですが、
Optional を付ける前に、
「本当に None を返す設計が適切か?」
を一度立ち止まって考えるのが大事です。
OptionalとUnionの関係をもう少しだけきちんと理解する
Optional[T] は Union[T, None] のシンタックスシュガー
型レベルでは、
from typing import Optional, Union
Optional[str] は Union[str, None] と同じ
Pythonです。
Python 3.10 以降なら、
str | None # これも同じ意味
Pythonと書けます。
つまり、Optional は「None を含む Union」を
読みやすく書くためのエイリアスです。
「None以外も混ざる」なら Optional ではなく Union を使う
例えば、「int か str か None」という型を表したいなら、
Optional ではなく Union を使います。
from typing import Union, Optional
value: Union[int, str, None] # これが正しい
value: Optional[int | str] # こう書くと意味が変わるので注意
PythonOptional[int | str] は
「int | str か None」=「int か str か None」なので、
結果的には同じ意味になりますが、
読みやすさ的には int | str | None の方が素直です。
初心者がOptionalとどう付き合うといいか
まずは「戻り値」から使い始める
いきなり引数にも Optional を付け始めると、
設計がごちゃごちゃになりがちです。
最初のステップとしては、
「見つからないときに None を返す関数」の戻り値にだけ Optional を付ける
くらいから始めるのがちょうどいいです。
例:検索系、find 系、get_or_none 系の関数など。
そこから少しずつ、
この引数は None を許すべきか?
この戻り値は本当に None を返すべきか?
といった設計の問いを増やしていくと、
Optional の使いどころの感覚が育っていきます。
「Noneかもしれない」を放置しない癖をつける
Optional を付けたら、
呼び出し側では必ず「None のときどうする?」を考える。
if で分岐する
デフォルト値を用意する
例外に変換する
など、何かしらの「扱い方」を決める。
「Optional を付けたのに、呼び出し側で何もしていない」
という状態は、設計的には危険信号です。
まとめ(Optionalは「Noneの可能性を型で約束する」ための道具)
Optional を初心者目線で整理すると、こうなります。
Optional[T] は「T かもしれないし None かもしれない」という型で、Union[T, None] の別名。Python 3.10 以降なら T | None と書ける。
戻り値に Optional を付けることで、「この関数は値がないときに None を返す」という仕様を型として明示でき、呼び出し側に「None のときどうする?」を考えさせられる。
Optional を付ける=「None を許す契約」を結ぶことなので、本当に必要なところだけに使い、「面倒だからとりあえず Optional」は避けるのが品質的に重要。
Optional をきっかけに、「None を返す設計が本当に正しいか?」「例外にすべきか?」といった設計の問いが生まれ、テストケースの抜けも見つけやすくなる。
