Python 3.15と聞くと、つい大きな話題——たとえば lazy imports や tachyon profiler ——に目が行きがちです。
でも今回紹介する記事は、その“見出しになりにくいけれど実は面白い”変更にフォーカスしています。
正直、こういう記事はかなり好きです。派手な新機能よりも、日々の開発で「うわ、これ地味に助かる…」となる改善のほうが、実務では効いたりするからです。この記事もまさにそのタイプでした。
asyncio.TaskGroup.cancel() で、TaskGroup をきれいに止めやすくなるContextDecorator が async 関数や generator でも、より自然に使えるようになるthreading.serialize_iterator などで iterator をスレッド安全に扱いやすくなるCounter に XOR ^ が追加されるjson.loads() で frozendict や tuple を使い、immutable な JSON 表現を作りやすくなる記事では、Python 3.15 の大きな話題として lazy imports や tachyon profiler がある、と触れたうえで、「でも小さめの改善にも注目したい」と始まります。
この姿勢、すごくわかります。新機能の目玉はニュースになりやすいですが、実際の開発を楽にするのは、こういう“つまらなそうに見えるけど便利”な変更だったりします。
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.suppress が ExceptionGroup をうまく扱うことで、例外を表に出さずに静かに終われる、というわけです。
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()
これ、かなり気持ちいいです。
例外を「キャンセルの道具」として無理やり使う感じがなくなるので、コードの意図が読みやすくなります。個人的には、こういう改善は本当に価値が高いと思います。地味ですが、バグの入り口が減るからです。
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 が「関数を呼んだ瞬間」だけを包んでしまうと、本来包みたいはずの“全体の実行時間”を測れないわけです。
3.15 では ContextDecorator がラップ対象の関数の型を見て、適切に全体を包むようになります。
記事の筆者はこれについて、「context manager は decorator を作る最良の方法になる」とかなり強く推しています。私もかなり同意です。こういう“1つの仕組みで複数の用途を自然にこなせる”のは Python らしい気持ちよさがあります。
iterator は、データを「必要なぶんだけ順番に取り出す」仕組みです。
たとえばイベントストリームをこんな感じで扱えます。
def stream_events(...) -> Iterator[str]:
while True:
yield blocking_get_event(...)
これ自体はきれいな抽象化です。
でも、これを threading や free-threading の世界に持ち込むと話が変わります。iterator は基本的に thread-safe ではないので、複数スレッドから同時に触ると、値が飛んだり、内部状態が壊れたりします。
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 で頑張ることが多かったはずですが、これが標準で用意されると、設計をかなりシンプルにできそうです。
個人的には、これは派手ではないけれどかなり実用的な改善だと思います。マルチスレッド周りは、ちょっとしたズレが地獄につながりやすいので、標準ライブラリで吸収してくれるのはありがたいです。
Counter に XOR ^ が追加されるCounter は何をするもの?collections.Counter は、要素が何回出たかを数えるためのクラスです。
たとえば a が3回、b が1回、という具合に数を持てます。
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)
Counter にはすでに、足し算 +、引き算 -、積集合 &、和集合 | のような操作があります。
+ : 数を足す- : 引き算する& : 小さいほうを取る| : 大きいほうを取る^ が来るここに XOR が加わります。
XOR は「重なっていない部分だけを残す」と考えるとわかりやすいです。
記事の筆者は、正直この用途をあまり使ったことがなく、使い道を考えるのが難しいとも書いています。ここはかなり率直で、好感が持てます。私も同じ感想で、便利というより「整ってきたな」という印象です。API の完成度が上がる感じですね。
JSON は普通、配列・真偽値・数値・null・文字列・object などで表されます。
Python ではこれを list や dict に変換して使うことが多いですが、時には「変更できない形」にしたいことがあります。
例えば:
そこで 3.15 では frozendict が加わり、JSON の object を immutable に表現しやすくなります。さらに json.load / json.loads に array_hook が追加され、object 用の object_hook と組み合わせられるようになります。
json.loads(
'{"a": [1, 2, 3, 4]}',
array_hook=tuple,
object_hook=frozendict
) == frozendict({'a': (1, 2, 3, 4)})
これ、かなり実用的です。
JSON を読み込んだあとに「中身がいつの間にか変わっていた」という事故を防ぎやすくなるからです。tuple と frozendict の組み合わせは、データを安全に持ち回るのに向いています。
この記事を読んで感じたのは、Python 3.15 は「派手な新機能で驚かせる」というより、「日々の開発でじわじわ効く改善」が多いということです。
特に印象的だったのはこのあたりです。
TaskGroup.cancel() で非同期処理の終了が自然になるこういう変更は、単体ではニュースになりにくいかもしれません。
でも、実際の開発ではこういう改善の積み重ねが効いてくるんですよね。私はかなり好きです。Python らしい“気の利いた進化”だと思いました。