PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

Python 3.15の“地味だけど効く”新機能たちを読む

Python 3.15と聞くと、つい大きな話題——たとえば lazy imports や tachyon profiler ——に目が行きがちです。
でも今回紹介する記事は、その“見出しになりにくいけれど実は面白い”変更にフォーカスしています。

正直、こういう記事はかなり好きです。派手な新機能よりも、日々の開発で「うわ、これ地味に助かる…」となる改善のほうが、実務では効いたりするからです。この記事もまさにそのタイプでした。

この記事のキーポイント

まず前提: Python 3.15は「大きい機能」だけじゃない

記事では、Python 3.15 の大きな話題として lazy imports や tachyon profiler がある、と触れたうえで、「でも小さめの改善にも注目したい」と始まります。
この姿勢、すごくわかります。新機能の目玉はニュースになりやすいですが、実際の開発を楽にするのは、こういう“つまらなそうに見えるけど便利”な変更だったりします。


1. asyncio.TaskGroup を素直に止められるようになる

何がうれしいのか

TaskGroup は、複数の非同期タスクをまとめて扱う仕組みです。
ざっくり言うと、「並行に動く処理の部屋を1つ作って、その中で複数の仕事をまとめて管理する」イメージです。こういう仕組みを structured concurrency と呼びます。

これまでは、TaskGroup を途中で止めたいときに少し面倒でした。
記事では、信号を待って途中終了する例として、わざと例外を投げてキャンセルする方法が紹介されています。

class Interrupt(Exception):
    ...

with suppress(Interrupt):
    async with asyncio.TaskGroup() as tg:
        tg.create_task(run())
        tg.create_task(run())
        if await wait_for_signal():
            raise Interrupt()

このやり方は、TaskGroup の中で例外が起きると他のタスクもキャンセルされる、という仕組みを利用しています。
しかも contextlib.suppressExceptionGroup をうまく扱うことで、例外を表に出さずに静かに終われる、というわけです。

Python 3.15で何が変わる?

3.15 では tg.cancel() が使えるようになり、もっと直接的になります。

async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())
    if await wait_for_signal():
        tg.cancel()

これ、かなり気持ちいいです。
例外を「キャンセルの道具」として無理やり使う感じがなくなるので、コードの意図が読みやすくなります。個人的には、こういう改善は本当に価値が高いと思います。地味ですが、バグの入り口が減るからです。


2. context manager が decorator として、もっとちゃんと使えるようになる

そもそも context manager って何?

with 文で使うあれです。たとえばファイルを開いて、終わったら自動で閉じるような仕組みが context manager です。
この記事では、処理時間を測る例が使われています。

@contextmanager
def duration(message: str) -> Iterator[None]:
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{message} elapsed {time.perf_counter() - start:.2f} seconds")

これを decorator として使うこともできます。

@duration('workload')
def workload():
    ...

何が問題だったのか

ただし、これまでは async 関数や generator ではうまく動かないケースがありました。
理由はシンプルで、普通の関数は「呼んだ瞬間に全部終わる」ことが多いのに対して、async 関数や generator は呼んだだけでは処理が完了しないからです。

つまり、decorator が「関数を呼んだ瞬間」だけを包んでしまうと、本来包みたいはずの“全体の実行時間”を測れないわけです。

Python 3.15の改善

3.15 では ContextDecorator がラップ対象の関数の型を見て、適切に全体を包むようになります。
記事の筆者はこれについて、「context manager は decorator を作る最良の方法になる」とかなり強く推しています。私もかなり同意です。こういう“1つの仕組みで複数の用途を自然にこなせる”のは Python らしい気持ちよさがあります。


3. iterator がスレッド安全になりやすくなる

iterator は便利だけど、スレッドでは油断できない

iterator は、データを「必要なぶんだけ順番に取り出す」仕組みです。
たとえばイベントストリームをこんな感じで扱えます。

def stream_events(...) -> Iterator[str]:
    while True:
        yield blocking_get_event(...)

これ自体はきれいな抽象化です。
でも、これを threading や free-threading の世界に持ち込むと話が変わります。iterator は基本的に thread-safe ではないので、複数スレッドから同時に触ると、値が飛んだり、内部状態が壊れたりします。

Python 3.15の解決策

3.15 では threading.serialize_iterator が追加され、iterator をラップするだけで安全に扱いやすくなります。

events = threading.serialize_iterator(stream_events(...))

さらに、generator 関数に直接使える threading.synchronized_iterator もあります。
そして面白いのが threading.concurrent_tee です。これは iterator を分割するのではなく、複数の iterator に同じ値を配るものです。

source1, source2 = threading.concurrent_tee(squares(10), n=2)

以前はこういう用途を Queue で頑張ることが多かったはずですが、これが標準で用意されると、設計をかなりシンプルにできそうです。
個人的には、これは派手ではないけれどかなり実用的な改善だと思います。マルチスレッド周りは、ちょっとしたズレが地獄につながりやすいので、標準ライブラリで吸収してくれるのはありがたいです。


4. Counter に XOR ^ が追加される

Counter は何をするもの?

collections.Counter は、要素が何回出たかを数えるためのクラスです。
たとえば a が3回、b が1回、という具合に数を持てます。

c = Counter(a=3, b=1)
d = Counter(a=1, b=2)

Counter にはすでに、足し算 +、引き算 -、積集合 &、和集合 | のような操作があります。

3.15では XOR ^ が来る

ここに XOR が加わります。
XOR は「重なっていない部分だけを残す」と考えるとわかりやすいです。

記事の筆者は、正直この用途をあまり使ったことがなく、使い道を考えるのが難しいとも書いています。ここはかなり率直で、好感が持てます。私も同じ感想で、便利というより「整ってきたな」という印象です。API の完成度が上がる感じですね。


5. immutable な JSON 表現がしやすくなる

JSON を「変更できない形」で扱いたいことがある

JSON は普通、配列・真偽値・数値・null・文字列・object などで表されます。
Python ではこれを listdict に変換して使うことが多いですが、時には「変更できない形」にしたいことがあります。

例えば:

そこで 3.15 では frozendict が加わり、JSON の object を immutable に表現しやすくなります。さらに json.load / json.loadsarray_hook が追加され、object 用の object_hook と組み合わせられるようになります。

json.loads(
    '{"a": [1, 2, 3, 4]}',
    array_hook=tuple,
    object_hook=frozendict
) == frozendict({'a': (1, 2, 3, 4)})

これ、かなり実用的です。
JSON を読み込んだあとに「中身がいつの間にか変わっていた」という事故を防ぎやすくなるからです。tuplefrozendict の組み合わせは、データを安全に持ち回るのに向いています。


まとめ: 3.15は“仕事がラクになる系”の改善が光る

この記事を読んで感じたのは、Python 3.15 は「派手な新機能で驚かせる」というより、「日々の開発でじわじわ効く改善」が多いということです。

特に印象的だったのはこのあたりです。

こういう変更は、単体ではニュースになりにくいかもしれません。
でも、実際の開発ではこういう改善の積み重ねが効いてくるんですよね。私はかなり好きです。Python らしい“気の利いた進化”だと思いました。


参考: Python 3.15: features that didn't make the headlines

同じ著者の記事