1 ポイント 投稿者 GN⁺ 2 시간 전 | 1件のコメント | WhatsAppで共有
  • Python 3.15.0b1 の機能凍結により、遅延インポートや Tachyon プロファイラ以外にも実用的な改善が確定
  • asyncioTaskGroup.cancel() は、独自例外や contextlib.suppress なしでタスクグループをスマートにキャンセル
  • ContextDecorator は、非同期関数・ジェネレータ・非同期イテレータのライフサイクル全体を包むように変更
  • threading の新ユーティリティは、イテレータ消費をスレッド間で直列化または複製し、Queue なしでも抽象化を維持できるようにする
  • Counter には xor 演算 が追加され、json.loadsarray_hookfrozendict により不変 JSON パースをサポート

Python 3.15 のあまり知られていない変更

  • Python 3.15.0b1 の機能凍結により、今年の Python に入る機能が確定し、大きな変更としては 遅延インポートTachyon プロファイラ がある
  • Python 3.15 には大きな PEP ほど目立たないものの、実用的な 小さな機能変更 も含まれており、asyncio、コンテキストマネージャ、スレッド安全なイテレータ、Counter、JSON パースまわりの改善が入っている

asyncio TaskGroup のキャンセル

  • asyncio の中核的な変更として、TaskGroupスマートにキャンセル できる機能が追加された
  • TaskGroup構造化並行性 の一形態で、複数の並行処理をきれいに生成し、すべて完了するまで待てるようにする
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())




# Waits for all the tasks to complete
  • Python 3.15 より前は、バックグラウンドシグナルを待って TaskGroup の実行を中断するには、独自例外 を発生させて contextlib.suppress で除外しなければならなかった
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 によってフィルタリングされるため機能する
  • ExceptionGroup と連携する suppress の仕組みは Python 3.12 で追加されたが、あまり注目されなかった
  • Python 3.15 の TaskGroup.cancel は、同じ作業をはるかに単純にする
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())

    if await wait_for_signal():
        tg.cancel()
  • TaskGroup.cancel() は例外を発生させずにグループをキャンセルするため、別途例外と suppress の組み合わせは不要になる

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

  • コンテキストマネージャは Python 3.3 から デコレータ としても直接使えた
@contextmanager
def duration(message: str) -> Iterator[None]:
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{message} elapsed {time.perf_counter() - start:.2f} seconds")
@duration('workload')
def workload():
    ...





# Or simple as a wrapper
duration('stuff')(other_workload)(...)
  • duration() のようにブロック実行時間を出力するコンテキストマネージャは、関数デコレータのように使えて便利だが、非同期関数、ジェネレータ、非同期イテレータでは正しく動作しないことがあった
@duration('async workload')
async def async_workload():
    ...

@duration('generator workload')
def workload():
    while True:
        yield ...
  • イテレータ、非同期関数、非同期イテレータは通常の関数と意味論が異なり、呼び出し時点でそれぞれジェネレータオブジェクト、コルーチンオブジェクト、非同期ジェネレータオブジェクトを返す
  • 従来のデコレータはラップ対象のライフサイクル全体をカバーできず、すぐに完了してしまうため、実際の実行時間全体を包めなかった
  • Python 3.15 では ContextDecorator がラップする関数の型を確認し、デコレータがその対象の ライフサイクル全体 を覆うように変更された
  • コンテキストマネージャをデコレータとして使う際に生じがちな落とし穴を避け、よりすっきりした構文を使えるようになる

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

  • イテレータは Python の中核的な抽象化のひとつで、データソースとデータ消費側を分離して、よりきれいな構造を作れる
lazy 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)
source1, source2 = threading.concurrent_tee(squares(10), n=2)

with ThreadPoolExecutor() as executor:
    fut1 = executor.submit(consume, source1)
    fut2 = executor.submit(consume, source2)
  • 従来はスレッド間での消費を同期するため主に Queue に依存していたが、新ユーティリティを使えばマルチスレッドコードでも既存のイテレータ抽象化を変えずに維持できる

追加機能

  • Counter xor 演算

    • collections.Counter は離散的な出現頻度を簡単に数えられるクラスで、dict[KeyType, int] に近いふるまいをしつつ、さまざまな便利な演算を提供する
c = Counter(a=3, b=1)
d = Counter(a=1, b=2)

print(f"{c + d = }") # add two counters together: c[x] + d[x]
print(f"{c - d = }") # subtract (keeping only positive counts)
Counter(a=4, b=3)
Counter(a=1, b=0)
  • Counter には積集合と和集合に相当する &| 演算もある
print(f"{c & d = }") # intersection: min(c[x], d[x])
print(f"{c | d = }") # union: max(c[x], d[x])
Counter(a=1, b=1)
Counter(a=3, b=2)
  • Counter は離散オブジェクトの集合のように見なすことができ、例は次のように解釈できる
{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}
  • Counter の集合演算をあまり使ってこなかったなら、xor の具体的な用途は思い浮かびにくいかもしれないが、演算体系の完成度という観点で追加された機能である
  • 不変 JSON オブジェクト

    • Python 3.15 に frozendict が追加されたことで、JSON の型である配列、ブール値、実数、null、文字列、オブジェクトをすべて 不変かつハッシュ可能な形 で表現できるようになった
    • json.loadjson.loadsarray_hook パラメータが追加され、既存の object_hook を補完する
    • array_hook=tuple, object_hook=frozendict を組み合わせれば、JSON オブジェクトをそのまま不変構造としてパースできる
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})

1件のコメント

 
GN⁺ 2 시간 전
Hacker News のコメント
  • 例を見ると lazy from typing import Iterator のように書いていて、Python にとうとう遅延 importが入ったのかと思った。
    この変更を見落としていたようで、これも Python 3.15 からなのか、それとも以前のバージョンにもあったのか気になる

    • 3.15 の機能: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • ここで遅延 importの利点が何なのかよく分からない。どうせモジュールスコープの型ヒントでその値を使うなら import が必要なのでは?
      そうなるとアノテーションの遅延評価が必要だが、デフォルトで有効だったとは思わない
    • 以前の Python バージョンでも、モジュールレベルで def __getattr__(name: str) -> object: を実装すれば回避できる
    • これは Python 3.15 の代表的な機能の 1 つなので、この記事では抜けているようだ。What's New の文書でも最初に挙がっているので、確かに目玉機能と見ていい。
      個人的にもかなり期待している。実際、今週だけでも、アプリケーションで実際には使わないモジュールの import が追加されたせいで Python プロセスがメモリ上限を超え、メモリ不足になったのを見た
    • Python ではほぼ最初期から、関数の中に import 文を書く形で遅延 importができた。その関数が呼ばれるまではライブラリは import されない
  • 3.15 に frozendict が追加されたことで、JSON のすべての型、つまり配列、真偽値、浮動小数点、null、文字列、オブジェクトを不変かつハッシュ可能な形で表現できるようになった。
    この最後の機能は本当に気に入っている

  • Python 3.15 にIterator 同期プリミティブが追加されたのはうれしい: https://docs.python.org/3.15/library/threading.html#iterator...
    自分の threaded-generator パッケージも、スレッド/プロセス + ジェネレーター + キューでまさにこれをやっているので、うまく補完してくれそう: https://pypi.org/project/threaded-generator/

  • Counter の集合演算、とくに xor の使い道が思い浮かばないと言っていたが、対称差を考えればよい。
    https://en.wikipedia.org/wiki/Symmetric_difference

    • その通りではあるが、Counter に適用すると多重集合の対称差になり、これには自然な定義がない。
      提案を正しく理解しているなら、各要素の個数差の絶対値で定義するようだが、これは結合則も成り立たない。パリティだけを見るなら F_2 上の加法として解釈できるのでより自然だが、それでも実際の用途はあまり見えない
  • Counter の例の 1 つは間違っている。3.13 と 3.15.0a の両方で確認した。
    Counter(a=3, b=1) - Counter(a=1, b=2) の結果は Counter({'a': 2})

    • 自分もそれを見た。ドキュメントによれば、Counter オブジェクトを組み合わせて多重集合を作るためのさまざまな数学演算が提供されており、加算と減算は対応する要素数を足し引きし、積集合と和集合はそれぞれ最小/最大の個数を返す。
      各演算は負の個数を持つ入力も受け付けるが、出力では個数が 0 以下の結果を除外する。いずれにせよ、見事な Counter-example だ ;-)
  • 10 年間 Python にどっぷり浸かっていて、作業するのも楽しかったが、AI コードボット以後の世界では今年だけでもすでに 10 万行以上を削除し、より高速な言語へ移した。最近は主に Go へ移行している

    • 最初は簡単だろうが、今後それらのプロジェクトの保守、とくにより複雑な機能追加はどうするつもりなのか気になる。
      1 つの方法として、Python でプロトタイプを作ってから変換するやり方はありそうだ
    • Go は科学計算や機械学習の仕事には本当に向いていない。ライブラリがそろっておらず、C API をラップする話も LLM の助けがあっても弱い。
      フィルタリング、ウィンドウ処理、オーバーラップなどが入る信号処理コードを書いてみると、今あるライブラリだけでは簡単な方法がほとんどない
    • Go 向けに Django のような総合的なWeb フレームワークをずっと探している。そういうものが出てきたらすぐにハマりそうだ
    • そもそもなぜ Python を使うようになったのか気になる。プログラミングをまったく知らない人には何を勧める?
    • 興味深いね。差し支えなければ、それが業務プロジェクトだったのか個人プロジェクトだったのか知りたい
  • Python の内部構造と運用、とくに free-threading に関してよいインタビューがある: https://alexalejandre.com/programming/interview-with-ngoldba...

  • ああ、愛しの Python。15 年近く使ってきた。懐かしいが、もう使っていない。君のせいではなく、人生が変わっただけだ

    • 最近のモダンな Pythonは、会社の仕事でも個人プロジェクトでも本当に楽しく使っている
    • Python とよく連携しつつ、より身軽で強力なPython ライクな言語を誰か作っていないだろうか?
  • イテレータ、非同期関数、非同期イテレータは通常の関数とは意味論が異なるため、デコレータとうまく噛み合っていなかった。呼び出すとそれぞれジェネレーターオブジェクト、コルーチン関数、非同期ジェネレーターオブジェクトを即座に返してしまい、デコレータが包む対象が全ライフサイクルではなく即時に終わってしまう。
    3.15 では ContextDecorator が包む関数の型を確認し、デコレータがライフサイクル全体を覆うように変わるが、この発想自体はとても気に入っている一方で、選択的に適用する仕組みなしに既存の利用箇所の挙動を微妙に変えてしまう点はかなり危うく見える。誰かが昔の壊れたやり方で意図的にデコレータを使っていなければ問題にならない、いわば「スペースバー暖房」的な状況ではあるが、実際にそうしていたなら予期せず壊れる可能性がある

    • Python コアチームは、既存の挙動に依存している人がいる可能性は低いと見ているようだ: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • 最悪の場合って何だろう? 互換性のない変更のせいで開発者が古い Python バージョンを使い続けるくらい? そんなこと起きるわけないよね
  • こういう小さな機能こそ、結局いちばん役に立つことが多い。とくに今のプロジェクトで、新しい標準ライブラリ追加機能を試してみたい