Python 3.15:表舞台には立たなかった機能

2026/05/21 20:10

Python 3.15:表舞台には立たなかった機能

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

Python 3.15.0b1 が機能の凍結(feature freeze)に入り、その主要な拡張は今年中にリリースされますが、すぐには強力な新しい同期ツールへのアクセスが可能になっています。最も重要なのは、この更新によって並行タスクの管理における長年の複雑さが解消され、改善された

asyncio.TaskGroup.cancel()
メソッドによる優雅なキャンセルを可能にし、コンテキストマネージャ装飾子が非同期関数およびイテレーターの全ライフサイクルを正しくラップすることを保証することです。開発者は今や
threading.serialize_iterator()
を介してビルトインのスレッドセーフなイテレータにアクセスでき、
threading.concurrent_tee()
を使用してイテレータ値を複数のコンシューマー間で複製することができるようになり、単一スレッド環境からマルチスレッド環境へ移行する際に外部のキューまたは複雑な状態管理が必要であったような手間がなくなります。さらに、このリリースは
json
モジュールの新しい
array_hook
パラメータによってサポートされる不変辞書タイプである
frozendict
を導入し、
collections.Counter
クラスにビットごとの XOR 演算を追加するとともに、
ExceptionGroup
による堅牢な例外グループ化を導入します。これらの変更は、広範な再設計を必要とせずにデータ整合性を直接高め、コード構造を簡素化します。遅延インポートなどの高度な機能は今後のリリースに留まりますが、現在のユーティリティ上の改善により、エンジニアらはより安定したマルチスレッドアプリケーションをすぐに構築できるようになり、構造化された並行性パターンに関連していた以前のコストのオーバーヘッドが削減されます。

本文

もう一度この時期が訪れました。Python の新バージョンも間近です。Python 3.15.0b1 では機能の凍結(feature freeze)が行われ、今年後半に Python にどのような新功能が加わるかについては明確になっています。特に注目すべき大きな変更には、「非同期 import」(lazy imports)や私が以前にも取り上げた「タキオンプロファイラ(Tachyon profiler)」などがあります。

昨年は、Python 3.14 の较小的機能についても詳しく調査しましたところ、それらも PEP に掲げられる大規模な仕様変更と同じくらい興味深く、さらに多くの人々の関心を集めるに値することがわかりました。今年の状況も同じです。

asyncio.TaskGroup
の優雅なキャンセル機能

今回のリリースでは asyncio に関する変更点はそれほど多くはありません。主な新機能は、

TaskGroup
を「優雅に(gracefully)」安全にキャンセルできる点にあります。

TaskGroup
は構造化併行処理(structured concurrency)の一つの形態で、開発者が複数のタスクをクリーンな方法で同時に実行できるようにします。

async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())
    # すべてのタスクが完了するのを待ちます

さて、ここではタスクグループの実行を中断するようなシグナルの受け取りをバックグラウンドで待機したいとしましょう。これは asyncio において単純なように思えますが、実際にはやや難解です。

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()

この仕組みは、タスクグループ内で例外が発生すると他のすべてのタスクが自動的にキャンセルされるという点にあります。カスタム例外である

Interrupt
ExceptionGroup
の一部として発生し、その後
contextlib.suppress
でフィルタリングされて、結果的にプログラムが正常に終了します。

ここで使われている

suppress
ExceptionGroup
の動作は、3.12 版から見過ごされていた興味深い機能の一つです。この記事の調査中に偶然この事実を知りました。

新しい

TaskGroup.cancel
メソッドを導入するだけで、この処理は大幅に簡素化されます:

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

以前の手法とは異なり、これほど単純になったことでわざわざ説明する必要もほとんどありません。例外を発生させることなく、グループ全体をただキャンセルするだけです。

コンテキストマネージャ機能の改善

デコレータの作成は意外と難しく、「面接における定番の質問」とさえなっています。しかし、コンテキストマネージャも実はデコレータとして同時利用可能であることを知っていますか?

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

上記は非常に一般的なコンテキストマネージャで、ブロック内の実行時間を出力します。Python 3.3 よりも前から、これをそのままデコレータとして直接使用することも可能です。

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

# もしくは単なるラッパー関数として
duration('stuff')(other_workload)(...)

確かに便利な仕組みですが、すべてのケースで動作するわけではありません:

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

@duration('generator workload')
def workload():
    while True:
        yield ...

イテレータ、非同期関数、および非同期イテレータは、通常の関数とは異なるセマンティクスを持つため、これらのケースではうまく機能しません。これらを呼び出すと、直ちにそれぞれ「ジェネレーターオブジェクト」「コルーチン関数」「アシンクロジェネレーターオブジェクト」を返します。そのため、デコレータの処理が対象となる全体のスコープ(ライフサイクル)ではなく、呼び出し直後だけで終了してしまいます。

この問題は私自身も何回か遭遇した不運な事例ですが、通常のデコレータにおいてもよく見られる問題です。しかし Python 3.15 では状況が変わりました。

ContextDecorator
は包装される関数のタイプを確認し、デコレーション対象の整个なライフサイクルをカバーするように振る舞います。

私の見解では、これによりコンテキストマネージャが「デコレータを作るための最適な方法」として再評価されます。一般的な落とし穴を避けつつ、より洗練された構文を提供するためです。このアプローチを多くの人々が採用することを強く推奨します。

スレッド安全なイテレータ

イテレータは現代の Python の基盤の一つであり、イテレータ型により「データソース」と「データ消費者」を明確に分離できます。これにより、より洗練された抽象化が可能になります。

from typing import Iterator

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

events = stream_events(...)

for event in events:
    consume(event)

しかし、この抽象化はスレッド処理や「フリースレッド(free-threading)」環境下では破綻します。デフォルトのイテレータはスレッド安全ではないため、データがスキップしたり、内部状態が壊れたりする可能性があります。

Python 3.15 では

threading.serialize_iterator
を用いてこの問題が解決します。オリジナルのイテレータをこの関数でラップするだけで、問題が解消されます:

import threading

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

with ThreadPoolExecutor() as executor:
    fut1 = executor.submit(consume, events)
    fut2 = executor.submit(consume, events)

また、ジェネレーター関数の結果に対して

threading.serialize_iterator
を自動的に適用するデコレータである
threading.synchronized_iterator
も用意されています。

さらに、複数のイテレータ間で値を複製(分岐させるのではなく)するための

threading.concurrent_tee
もあります:

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

with ThreadPoolExecutor() as executor:
    fut1 = executor.submit(consume, source1)
    fut2 = executor.submit(consume, source2)

これらのユーティリティが存在する以前には、マルチスレッド間での消費処理の同期は主にキュー(Queue)に頼っていました。これら新增された機能によって、マルチスレッド用コードであっても、既存の抽象化を変更する必要がなくなります。

余談:さらに興味深い機能

昨年は 3 つの機能を重点的に紹介しましたが、今年はこれよりも多くの新機能が発表されており、特に魅力的です。今回はインパクトはさほど大きくありませんが、それでも非常に興味深い 2 つの変更点をご紹介します。

Counter
クラスへの XOR 演算子の追加

collections.Counter
は非常に有用なクラスです。離散な発生頻度を簡潔にカウントできます。これは文字列マップ(
dict[KeyType, int]
)のような振る舞いをしますが、さらに多くの有用な演算子を内蔵しています。

c = Counter(a=3, b=1)
d = Counter(a=1, b=2)
print(f"{c + d = }")  # 2 つの Counter を加算:各要素について c[x] + d[x]
print(f"{c - d = }")  # 減算(正値のみ保持)

出力例:

Counter(a=4, b=3)
Counter(a=1, b=0)

さらに奇妙ですが面白い演算もあります:

print(f"{c & d = }")  # インターセクション:min(c[x], d[x])
print(f"{c | d = }")  # ユニオン:max(c[x], d[x])

出力例:

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

これは、「

Counter
も離散なオブジェクトの集合(multiset)を表現できる」という考え方です。つまり、上記の例では本質的には以下のようになります:

{a_0, a_1, a_2, b_0} & {a_0, b_0, b_1} == {a_0, b_0}
{a_0, a_1, a_2, b_0} | {a_0, b_0, b_1} == {a_0, a_1, a_2, b_0, b_1}

Python 3.15 ではさらに「排他的和(XOR)」演算も追加されます:

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

c ^ d == c | d - c & d 
# == Counter(a=3, b=2) - Counter(a=1, b=1) 
# == Counter(a=2, b=1)

これも再び、先ほど説明した記法で考えると理解しやすいです:

{a_0, a_1, a_2, b_0} ^ {a_0, b_0, b_1} == {a_1, a_2, b_1}

私はこれを「余談(bonus)」のセクションに載せました。なぜなら、私はこれまで Counter に対する集合演算を使用する機会はほとんどなかったし、「XOR」に特化した具体的な使用例を想像することが極めて困難だったからです。しかし、コンプリートネスのために開発者がこの機能を追加してくれたことに感謝します。

インミュータブルな JSON オブジェクト

Python 3.15 で

frozendict
が追加されることで、すべての JSON 型(配列、ブーリアン、浮動小数点数、null、文字列、オブジェクト)を「不変かつハシシャブル(hashable)」な形で表現できるようになりました。

さらに

json.load
および
json.loads
に、
object_hook
パラメータと対となる
array_hook
パラメータが追加されました。これにより、JSON オブジェクトを以下のような不変形に直接パースできます:

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

同じ日のほかのニュース

一覧に戻る →

2026/05/22 3:54

Ubuntu 16.04 で 10 ユーアー運用したブログを、FreeBSD に移行しました。

2026/05/22 4:32

視覚障害者の方々が Kagi シーチを使用してする方法

## Japanese Translation: Kagi は、ユーザーのサブスクリプションによって完全に資金提供され、SEO やエンゲージメント指標よりも結果の品質を優先する有料かつ広告非表示の検索エンジンです。主流の検索エンジンで見られる乱立した結果、自動再生動画、詐欺的なサイト、AI 生成ノイズなどによる「視覚疲労」に備えて採用されました。結果は有料配置やキーワードではなく品質に基づいてランク付けされ、プラットフォームは「small web」の高品質なソース(開発者ドキュメントやインデペンデントブログなど)を統合するとともに、よりクリーンで信頼性の高いリサーチ環境を構築するために豊富なカスタマイズ機能を備えています。 Kagi のサブスクリプションモデルには、無料トライアル(100 回検索)から Starter(月額 $5 で 300 回検索)、Professional(月額 $10 で無限検索)、Ultimate(月額 $25)までのティアが含まれます。「Fair Pricing」ポリシーにより、ユーザーは未使用の検索クレジットを将来の利用プランに転換したり、適切なクレジット調整とともにプランをダウングレードしたりすることができます。ユーザーは内蔵された CSS エディタを通じて不要な要素を非表示にし、テーマをカスタマイズし、フォントや高コントラスト設定を調整し、AI 生成画像を削除できます。追加の機能として、「Lenses」によるトピックフィルター(例:Academic、Programming)の保存、ドメイン制御による結果のブロック・低減・増加・ピン留め、Bangs コマンド(!w で Wikipedia など)、キーボードショートカット(`?` キーでアクセス可能)があります。「Share this Search」機能により、個々のリンクをコピーせずに特定の Kagi 検索結果を共有することが可能です。Chrome、Edge、Firefox のデフォルト統合を有効にすることで、Kagi は広告、自動再生コンテンツ、詐欺的なサイトへの露出を減らし、より高い検索品質を促すシームレスでプライバシー重視のエコシステムを提供しています。

2026/05/21 23:34

[Show HN] Freenet:分散型アプリケーション向けのピア・ツー・ピア・プラットフォーム

## Japanese Translation: Freenet プラットフォームは、大規模テック企業のインフラに依存せず動作する通信、コラボレーション、商用向けの分散型アプリケーションを可能にする転換的なアプローチをデジタル領域にもたらします。位置情報に基づいたリング構造で組織化されたグローバルピアツーピアネットワークとして機能し、ユーザーのコンピュータがノードとして作用することを許可するとともに、中央集権サーバーへの依存を排除します。ユーザーはこれらのアプリケーションをブラウザ経由で直接アクセスでき、通常のウェブサイトのように表示されますが、撤去されることができず、追跡不可能であり、従来のクラウドモデルで見られるデータ監視から自由です。開発者は Rust や TypeScript などの熟悉的なプログラミング言語を活用して、これら頑健なツールの構築が可能になり、高額なホスティングコストや依存関係に縛られずに済みます。汎用アクセシビリティのための開放標準を用いて構造化されており、ベンチャー資金によるモデルとは異なり、少数チームによる助成金と寄付に完全に頼っています。今後の成長は、持続可能で無料のデジタルインフラを求める開発者とユーザー双方における採用拡大に大きく依存しており、個人に必須ツールのアクセスを止められないようにするという/serverレス代替手段へと業界をシフトさせる可能性があります。