Python 3.15で追加されるfrozendictクラスとsentinelクラスってどんなもの?HPかわさきの研究ノート

Python 3.15では組み込み型として変更不可能な辞書であるfrozendictクラスと、値が見つからないことや使えないことを意味する番兵値を表すsentinelクラスが追加される予定です。これらをどんなふうに使うのか、ちょっと調べてみました。

» 2026年05月21日 05時00分 公開
[かわさきしんじDeep Insider編集部]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「HPかわさきの研究ノート」のインデックス

連載目次


かわさき

 どうもHPかわさきです。

 Pythonには変更可能な集合を表すsetクラスと変更不可能な集合を表すfrozensetクラスがありました。このfrozensetクラスの辞書版ともいえるのがfrozendictクラスです(ホントかな?)。そして、コンテナオブジェクトから何かを探したり、与えられた引数の値をチェックしたりする際の番兵値を表すための新たな組み込み型としてsentinelクラスが登場します。これらについて、以下ではちょっと試してみることにしましょう。


Python 3.15で追加される2つの組み込み型

 Python 3.15では次の2つの組み込み型が追加される予定です。

  • frozendictクラス(PEP 814)
  • sentinelクラス(PEP 661)

 frozendictクラスは要素の変更や追加ができない辞書(マッピング型)です。通常の辞書(dictクラス)は変更可能なオブジェクトであり、ハッシュ不可能でもあります。つまり、辞書を集合の要素にしたり、別の辞書のキーとして使ったりすることはできません。しかし、frozendictオブジェクトなら、集合の要素にしたり、他の辞書のキーにしたりできます(ただし、その値がリストのように変更可能なオブジェクトであれば、そうはいきません)。

 sentinelクラスはいわゆる番兵値(sentinel value)を表現するために導入されました。番兵値とは、何らかの値が見つからない、存在しない、使えないといった状況を表すために使われます。PythonではNoneやobjectクラスのインスタンスを番兵値として使うことがよくありますが、それではうまくいかないときもあることから、新しくsentinelクラスが作られました。

 以下では、順に見ていきましょう。

frozendictクラス

 上でも述べましたが、frozendictオブジェクトは変更不可能な辞書(マッピング型)です。そのオブジェクトは辞書と同様な作り方をします。ただし、「fd = {}」は空の辞書を作るだけで、空のfrozendictオブジェクトを作るわけではない点には注意してください。

fd0 = frozendict()  # 空のfrozendictオブジェクト
fd1 = frozendict({'k0': 'v0', 'k1': 'v1'})  # 辞書からfrozendictを作成
fd2 = frozendict(k0='v0', k1='v1'# キーワード引数を使う
fd3 = frozendict([('k1', 'v1'), ('k0', 'v0')])  # キー/値を要素とするリストを使う
fd4 = frozendict(fd3)  # frozendictからfrozendictを作成

frozendictオブジェクトを定義する

 辞書と大きく違うのは、frozendictは変更不可能な点にあります。キー/値の組を追加したり、特定のキーの値を変更したりはできません。

fd1['k2'] = 'v2'  # TypeError

frozendictは変更不可能

 ただし、値が変更可能なオブジェクトであれば、その内容は変更可能です。これはタプルの要素が変更可能なオブジェクトであれば、その内容を変更できることと同様です。

fd5 = frozendict({'key': ['foo', 'bar']})
print(fd5)  # frozendict({'key': ['foo', 'bar']})
fd5['key'][0] = 'FOO'
print(fd5)  # frozendict({'key': ['FOO', 'bar']})

値が変更可能オブジェクトであれば、変更できる

 frozendictオブジェクトは変更不可能なので、そのハッシュ値を計算できます(値が変更可能なオブジェクトでない場合)。以下では上で作成したfrozendictオブジェクトを使い回しています。

print(fd1)  # frozendict({'k0': 'v0', 'k1': 'v1'})
h1 = hash(fd1)
print(h1)  # 6894830583868851914など(実行時に変化する)

frozendictはハッシュ可能

 また、次のような特性がある点にも注意してください。

print(fd2)  # frozendict({'k0': 'v0', 'k1': 'v1'})
print(fd3)  # frozendict({'k1': 'v1', 'k0': 'v0'})
print(hash(fd2))  # 6894830583868851914など
print(hash(fd3))  # 6894830583868851914など
print(hash(fd2) == hash(fd3))  # True
print(fd2 == fd3)  # True

frozendictは要素の挿入順序を覚えているが、ハッシュの計算や同値性の判定には関係がない

 2つのfrozendictオブジェクト、fd2とfd3はキー/値の組の並びが異なります(frozendictは要素の挿入順序を保持します。これはPython 3.7以降の通常の辞書と同様です)。ですが、hash関数の値は同じになり、これら2つのfrozendictを==演算子で比較すると同じ値であると判断されてTrueが返されます。


かわさき

 言い忘れていましたが、frozendictと通常の辞書の比較も可能です。以下に例を示します。

print(frozendict({'foo': 'FOO'}) == {'foo': 'FOO'})  # True
print(frozendict({'foo': 'FOO'}) == {'foo': 'FOO', 'bar': 'BAR'})  # False

frozendictと辞書の比較も可能

 このようにfrozendictと辞書は比較が可能ですが、これはfrozendictが辞書(dict)のサブクラスだからというわけではありません。

print(isinstance(fd0, dict))  # False

frozendictはdictのサブクラスではない

 frozendictと辞書はともにcollections.abc.Mappingを実装しているのであって、frozendictと辞書の間に継承関係はありません。


 一方、変更可能なオブジェクトを値とする要素を含んでいるfrozendictはハッシュ関数に渡すと例外が発生します。

print(fd5)  # frozendict({'key': ['FOO', 'bar']})
hash(fd5)  # TypeError

変更可能なオブジェクトを値とする要素を含むfrozendictのハッシュ値は計算できない

 次にfrozendictオブジェクトを辞書のキーや集合の要素として使ってみましょう。

print(fd0)  # frozendict()
print(fd1)  # frozendict({'k0': 'v0', 'k1': 'v1'})
print(fd2)  # frozendict({'k0': 'v0', 'k1': 'v1'})
print(fd3)  # frozendict({'k1': 'v1', 'k0': 'v0'})
print(fd5)  # frozendict({'key': ['FOO', 'bar']})

fd0は空のfrozendict、fd1とfd2はキーも値も変更不可能なfrozendictで、fd5は値が変更可能なfrozendict

 fd0は空のfrozendictです。fd1とfd2、fd3はキーも値も文字列(変更不可能)なfrozendictです(fd1とfd2はキーと値の並びが同じで、fd3は並びだけが違っています)。fd5は'key'の値が変更可能なオブジェクトになっているfrozendictです。これらを使って、まずは辞書のキーとしてみます。

# frozendictオブジェクトを辞書のキーに使う
d1 = {fd2: 'some value'}
print(d1)  # {frozendict({'k0': 'v0', 'k1': 'v1'}): 'some value'}

d2 = {fd5: 'some value'# TypeError

キーと値が両方変更不可能なfd2は辞書のキーに使えるが、変更可能な値を持つfd5は辞書のキーにするとTypeError例外が発生する

 fd2はキーも値も変更不可能な文字列でした。そのため、これを辞書のキーにすることは問題ありませんでした。しかし、値がリストになっている要素を含むfd5を辞書のキーにするとTypeError例外が発生しました。

 これは辞書ではなく、frozendictのキーにこれらを使う場合も同様です。

# frozendictオブジェクトをfrozendictオブジェクトのキーとする
fd6 = frozendict(d1)
print(fd6)  # frozendict({frozendict({'k0': 'v0', 'k1': 'v1'}): 'some value'})

fd7 = frozendict({fd5: 'some value'})  # TypeError

frozendictのキーにする場合も同様

 最後に集合の要素とする場合です。ここでは空のfrozendictやキーと値の並びだけが異なるfd3も含めて、集合を作成しています。

# frozendictオブジェクトを集合の要素とする
s1 = set([fd0, fd1, fd2, fd3])
print(s1)  # {frozendict(), frozendict({'k0': 'v0', 'k1': 'v1'})}

s2 = set([fd1, fd2, fd3, fd5])  # TypeError

frozendictを辞書のキー、frozendictのキー、集合の要素として使ってみた

 「s1 = set([fd0, fd1, fd2, fd3])」として作成した集合には空のfrozendict(fd0)の他に要素は1つだけです。fd1とfd2は同じキーと値だけを含み、その並びも同一でしたが、fd3はキーと値は同じで並びは異なっていました。それでもハッシュ値が同じであることは上で確認しましたが、ここでも同一の要素として扱われていることに注意してください。

 また、fd5を含めるとこれまでと同様にTypeError例外が発生します。

 というのが、frozendictの基本的な性質といえるでしょう。本当は演算などについても触れた方がよいのですが、今回はここまでとしておきます。


かわさき

 どんなところでfrozendictは使えるかがなかなか分からないんですよね。シンプルに「変更できない」という点に注目して、さまざまな設定のデフォルト値を格納するといった使い方はありそうです。


sentinelクラス

 sentinelクラスは番兵値を表すために使います。例えば、以下のコードについて考えてみます。これは値が見つからなかったことを意味する番兵値としてNoneを使おうというコードです。

d = {'foo': 'FOO', 'bar': 'BAR', 'baz': None}

target = 'baz'
result = d.get(target, None)

if result is None:
    print(f'key: {target} does not exist')
else:
    print(f'key: {target} exist, value: {result}')

辞書の値としてNoneを許容していると……

 ここでは辞書dのキー'baz'の値はNoneになっています(いかにもな例ですね)。このときに「d.get(target, None)」としてキー'baz'の値を探そうとすると、次のようになってしまいます。

キー'baz'は存在し、その値がNoneなのに、キー'baz'は存在しないとなってしまう キー'baz'は存在し、その値がNoneなのに、キー'baz'は存在しないとなってしまう

 これを避けるには、次のようなコードを書けます。

d = {'foo': 'FOO', 'bar': 'BAR', 'baz': None}
MISSING = object()

target = 'baz'
result = d.get(target, MISSING)

if result is MISSING:
    print(f'key: {target} does not exist')
else:
    print(f'key: {target} exist, value: {result}')

辞書の値としてNoneを許容していると……

 これは番兵値としてobject型のMISSINGというインスタンスを新規に作成して、getメソッドではキーが存在しなかったときに、それを返すように指定するものです。これにより、上のコードではキー'baz'が存在し、その値がNoneであることを判定できるようになります。

object型の値を番兵値として使うとうまくいくこともある object型の値を番兵値として使うとうまくいくこともある

 しかし、object型の値を番兵値として使うのがうまくないこともあります。

MISSING = object()

def greet(greeting='hello', to=MISSING):
    if to is MISSING:
        to = 'world'
    print(f'{greeting} {to}')

greet()  # hello world
greet(to='Python'# hello Python
greet(greeting='goodbye', to=None# goodbye None

パラメーターtoが指定されなかったときにはMISSINGがその値になる

 これはパラメーターtoに引数が与えられなかった場合には、その値がMISSINGとなる関数の定義です。そのため、Noneを渡しても、引数が与えられなかったとは見なされません。挙動も思った通りのものです。しかし、これに型ヒントを付けようとすると、少し困ったことになります。

from typing import Any

MISSING: Any = object()

def greet(greeting: str = 'hello', to: str | Any = MISSING) -> None:
    if to is MISSING:
        to = 'world'
    print(f'{greeting} {to}')

greet()  # hello world
greet(to='Python'# hello Python
greet(greeting='goodbye', to=None# goodbye None

先ほどと同じことをする関数を定義した

 typing.Anyは「任意の型がAnyと互換性がある」ことを意味します。つまり、パラメーターtoは「文字列と任意の型を受け取る」ことを意味します。それって型チェックを実質的にはあきらめることになっちゃうということです。ちなみにAnyをobjectとしても結果は似たようなものです。Pythonのオブジェクトはobjectを継承しているので、実質的には全てのオブジェクトが許容されてしまいます。

 このように従来のやり方では型ヒントを書く上でも問題が発生します。番兵を表すクラスを定義するのはありですが。

class Sentinel:
    pass  # 省略

MISSING = Sentinel()

def greet(greeting: str = 'hello', to: str | Sentinel = MISSING) -> None:
    pass  # 省略

必要ならSentinelクラスを定義すればよい

 しかし、こうしたクラスを毎回自分で定義するのも面倒です。そこで登場したのが組み込みのsentinelクラスというわけです。何より素晴らしいのは、sentinelクラスのオブジェクトは特別扱いされていて、型表現に含むことが可能な点です(Noneと同様ですね)。以下に例を示します。

MISSING = sentinel('MISSING')

def greet(greeting: str = 'hello', to: str | MISSING | None = MISSING) -> None:
    if to is MISSING:
        to = 'world'
    print(f'{greeting} {to}')

greet()  # hello world
greet(to='Python'# hello Python
greet(greeting='goodbye', to=None# goodbye None

sentinelクラスの使用例

 sentinelクラスのオブジェクトを作成するときには、引数としてそのオブジェクトが表す文字列を渡します。また、上でも述べたように「to: str | MISSING | None」のように型表現にsentinelオブジェクトをそのまま記述できます。こうすることで、Anyやobjectよりも精密にパラメーターの型を指定可能になります。

 Python 3.15からは、番兵値を扱うときには、自分でSentinelクラスを定義するのではなく、sentinelクラスを使用するのがよいでしょう。

「HPかわさきの研究ノート」のインデックス

HPかわさきの研究ノート

Copyright© Digital Advantage Corp. All Rights Reserved.

アイティメディアからのお知らせ

スポンサーからのお知らせPR

注目のテーマ

その「AIコーディング」は本当に必要か?
Microsoft & Windows最前線2026
4AI by @IT - AIを作り、動かし、守り、生かす
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。