Bốn Thay Đổi Nhỏ Trong Python 3.15 Mà Ít Ai Để Ý

7 phút đọc English
Featured image for Bốn Thay Đổi Nhỏ Trong Python 3.15 Mà Ít Ai Để Ý

Mỗi phiên bản Python đều có hai lần ra mắt. Lần đầu là cái tên flashy trên mạng xã hội - những PEP được PR rầm rộ, những tính năng chiếm headline. Lần thứ hai là vài tuần sau, trong bài blog của ai đó đã thực sự ngồi đọc hết changelog.

Jamie Chang đã làm việc đó cho Python 3.15. Hai ngôi sao năm nay là lazy importsTachyon profiler. Bên dưới hai cái đó, có bốn thay đổi giải quyết những vấn đề mà bất kỳ lập trình viên Python nào cũng đã gặp ít nhất một lần.

⚡ TL;DR

  • Sửa gì: Những điểm xấu xí tồn tại nhiều năm trong asyncio, threading, và context manager decorator
  • Tại sao quan trọng: Workaround cũ rắc rối đến mức nhiều người tránh dùng hẳn những pattern đó
  • Dành cho ai: Người viết asyncio worker, data pipeline đa luồng, hoặc utility decorator
  • Điểm khác biệt: Không phải tính năng mới - là tính năng cũ cuối cùng chạy đúng như tài liệu mô tả
  • Lưu ý: Python 3.15 vẫn đang beta; feature freeze xong, ngày ra chính thức cuối năm nay

TaskGroup.cancel() - cái mà bạn lẽ ra đã có từ lâu

TaskGroup là tính năng structured concurrency hay nhất trong Python những năm gần đây. Thứ còn thiếu: làm thế nào để dừng toàn bộ một cách sạch sẽ từ bên ngoài?

Trước 3.15, cách làm là tự chế ra một exception:

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

Cách này chạy được. Exception tự chế bị nuốt bởi contextlib.suppress, các task hủy gọn gàng. Nhưng bạn phải biết rằng suppress hoạt động được với ExceptionGroup (bản thân đây cũng là tính năng 3.12 mà nhiều người chưa dùng), phải viết thêm một exception class vứt đi, và mỗi lần phải nhớ lại cái pattern này.

Trong 3.15:

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

Không exception. Không suppress. Không class thừa. Chỉ tg.cancel().

Context manager decorator cuối cùng không nói dối nữa

Bạn hẳn biết rằng một @contextmanager có thể dùng như decorator từ 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():
    ...

Điều bạn có thể đã phát hiện ra theo cách đau đớn: nó vỡ im lặng với mọi thứ không phải hàm đồng bộ thông thường.

@duration('async workload')
async def async_workload():
    ...  # decorator chạy xong ngay, đo được số 0

@duration('generator workload')
def workload():
    while True:
        yield ...  # tương tự

Khi bạn gọi async def, bạn nhận lại ngay một coroutine object - chưa có code nào chạy. Decorator bọc cái object đó, đo thời gian từ lúc gọi đến lúc Python trả coroutine về, và con số đó gần bằng 0. Context manager fire và exit trước khi công việc thực sự bắt đầu.

Trong 3.15, ContextDecorator kiểm tra kiểu của hàm được bọc và xử lý đúng với async function, generator, và async generator - bao phủ toàn bộ vòng đời thực sự của từng loại, không chỉ khoảnh khắc gọi hàm.

Đây là kiểu thay đổi khiến bạn quay lại audit tất cả context manager decorator bạn đã viết trong ba năm qua.

Iterator thread-safe không còn phải chuyển sang Queue

Giả sử bạn có một generator:

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

Và bạn muốn chia nó cho thread pool. Trước 3.15, chia sẻ iterator qua nhiều thread là undefined behavior - có thể bỏ sót giá trị, nhân đôi giá trị, hay làm vỡ trạng thái bên trong iterator. Cách chữa chuẩn là đưa mọi thứ qua Queue, tức là viết lại generator thành producer thread và consumer đọc từ queue thay vì iterate. Abstraction khác, code khác.

Trong 3.15, ba primitive mới:

import threading

# Bọc iterator có sẵn - thread-safe, mỗi lần chỉ một value ra
events = threading.serialize_iterator(stream_events(...))

# Hoặc dùng như decorator trên generator function
@threading.synchronized_iterator
def stream_events(...) -> Iterator[str]:
    ...

# Hoặc broadcast - mỗi consumer nhận đủ mọi value
source1, source2 = threading.concurrent_tee(squares(10), n=2)

Pattern dùng Queue vẫn hoạt động. Nhưng ba cái này cho phép giữ nguyên abstraction iterator qua nhiều thread.

frozendict và JSON bất biến hoàn toàn

PEP 814 đưa frozendict vào 3.15. Đây là mapping bất biến, hashable - hiểu nôm na là tuple của dict. Hữu ích hơn bản thân kiểu dữ liệu là thay đổi trong json.loads:

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

Trước đây, json.loadobject_hook nhưng không có gì cho array. Để có cây JSON bất biến hoàn toàn, bạn phải post-process đệ quy. Giờ cả hai hook đều tồn tại, bạn có thể get một Python object hoàn toàn frozen trong một lần parse.

HN đang tranh luận về điều gì thật ra

353 điểm, 170 bình luận. Như thường lệ, cuộc thảo luận đi xa khỏi kỹ thuật.

Có người phát hiện code mẫu trong bài gốc có dòng lazy from typing import Iterator - trông như tính năng lazy import đã lọt vào bài và làm bối rối những người chưa biết đến nó.

Nhưng luồng suy nghĩ nặng hơn trong thread là về quỹ đạo của Python:

“Tôi đã yêu Python suốt 10 năm… Nhưng năm nay đã xóa 100k+ dòng, chuyển sang ngôn ngữ nhanh hơn trong thế giới post-AI codebot. Chủ yếu sang Go.”

Và một cái khác:

“Buồn cười là chúng ta có thể phải chờ thêm khá lâu để các LLM cập nhật được Python 3.15 vào pre-training của chúng.”

Bình luận thứ hai thú vị một cách thầm lặng. Python 3.15 vẫn đang beta. Các AI assistant bạn đang dùng hôm nay được train trên dữ liệu từ trước khi bản này tồn tại. Khi bạn hỏi AI viết asyncio code để hủy TaskGroup, nó sẽ đưa cho bạn pattern exception-class - vì đó là thứ duy nhất tồn tại lúc training data được thu thập. Không phải nó sai. Chỉ là nó chưa biết tg.cancel(). Hãy nhớ khoảng cách đó.

Tradeoff và thực tế

Những thay đổi này có thật, nhưng vẫn đang trong beta. Feature freeze nghĩa là tính năng bị khóa; không có nghĩa là tất cả edge case đã được xử lý. Nếu bạn đang đánh giá Python cho production hôm nay, bạn đang dùng 3.13 hoặc 3.14.

Cho bốn tính năng này cụ thể:

Tính năngAi được lợi nhấtĐọc thêm
TaskGroup.cancel()Code asyncio serviceasyncio.TaskGroup docs
Sửa ContextDecoratorThư viện decorator tiện íchcontextlib docs
Iterator thread-safeData pipeline, async streamthreading.serialize_iterator
frozendict + json.loadsConfig parser, immutable data modelPEP 814

Bài gốc: Python 3.15: features that didn’t make the headlines của Jamie Chang. Thread HN: news.ycombinator.com/item?id=48220696

Hoang Yell

Một nhà phát triển phần mềm và là người kể chuyện kỹ thuật. Tôi đọc Hacker News mỗi ngày và kể lại những câu chuyện hay nhất ở đây — bằng tiếng Việt và tiếng Anh, cho người tò mò nhưng không có thời gian.