1 điểm bởi GN⁺ 2 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Với việc Python 3.15.0b1 đóng băng tính năng, ngoài lazy imports và profiler Tachyon, nhiều cải tiến thực dụng khác cũng đã được chốt
  • TaskGroup.cancel() của asyncio cho phép hủy nhóm tác vụ một cách gọn gàng mà không cần ngoại lệ tự định nghĩa và contextlib.suppress
  • ContextDecorator được thay đổi để bao bọc toàn bộ vòng đời của hàm bất đồng bộ, generator và iterator bất đồng bộ
  • Tiện ích mới của threading giúp tuần tự hóa hoặc nhân bản việc tiêu thụ iterator giữa các luồng mà vẫn giữ nguyên mức trừu tượng, không cần Queue
  • Phép toán xor được thêm vào Counter, còn json.loads hỗ trợ phân tích JSON bất biến với array_hookfrozendict

Những thay đổi ít được biết đến trong Python 3.15

  • Với việc Python 3.15.0b1 đóng băng tính năng, các tính năng sẽ có mặt trong Python năm nay đã được chốt; những thay đổi lớn gồm có lazy importsprofiler Tachyon
  • Python 3.15 cũng bao gồm nhiều thay đổi tính năng nhỏ mang tính thực dụng, tuy không nổi bật như các PEP lớn, với các cải tiến ở asyncio, context manager, iterator an toàn luồng, Counter và phân tích JSON

Hủy asyncio TaskGroup

  • Thay đổi cốt lõi trong asyncio là bổ sung khả năng hủy một cách gọn gàng TaskGroup
  • TaskGroup là một dạng của structured concurrency, cho phép tạo nhiều công việc đồng thời một cách gọn gàng và chờ đến khi tất cả hoàn tất
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())




# Waits for all the tasks to complete
  • Trước Python 3.15, để chờ tín hiệu nền rồi dừng việc thực thi TaskGroup, cần phải phát sinh một ngoại lệ tự định nghĩa và lọc nó bằng 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()
  • Cách này hoạt động vì khi có ngoại lệ xảy ra trong task group, các tác vụ khác sẽ bị hủy, ngoại lệ Interrupt tự định nghĩa sẽ xuất hiện như một phần của ExceptionGroup, rồi được lọc bởi contextlib.suppress
  • Cách suppress hoạt động cùng ExceptionGroup được bổ sung từ Python 3.12 nhưng không được chú ý nhiều
  • TaskGroup.cancel trong Python 3.15 giúp làm cùng việc đó đơn giản hơn rất nhiều
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())

    if await wait_for_signal():
        tg.cancel()
  • TaskGroup.cancel() hủy nhóm mà không phát sinh ngoại lệ, nên không còn cần kết hợp ngoại lệ riêng và suppress

Cải tiến context manager

  • Từ Python 3.3, context manager đã có thể được dùng trực tiếp như một decorator
@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)(...)
  • Context manager như duration() để in ra thời gian thực thi của một khối lệnh rất tiện khi dùng như function decorator, nhưng trong hàm bất đồng bộ, generator và iterator bất đồng bộ, nó có thể không hoạt động đúng
@duration('async workload')
async def async_workload():
    ...

@duration('generator workload')
def workload():
    while True:
        yield ...
  • Iterator, hàm bất đồng bộ và iterator bất đồng bộ có ngữ nghĩa khác với hàm thông thường: khi được gọi, chúng lập tức trả về lần lượt generator object, coroutine object và async generator object
  • Decorator trước đây không bao quát được toàn bộ vòng đời của đối tượng được bao bọc mà kết thúc ngay lập tức, nên không thể bọc trọn thời gian thực thi thực tế
  • Trong Python 3.15, ContextDecorator được thay đổi để kiểm tra kiểu của hàm được bao bọc và để decorator phủ lên toàn bộ vòng đời của đối tượng đó
  • Nhờ vậy có thể tránh được những cạm bẫy thường gặp khi dùng context manager làm decorator và dùng cú pháp gọn gàng hơn

Iterator an toàn luồng

  • Iterator là một trong những mức trừu tượng cốt lõi của Python, giúp tách biệt nguồn dữ liệu và bên tiêu thụ dữ liệu để tạo cấu trúc sạch hơn
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)
  • Mức trừu tượng này có thể bị phá vỡ trong môi trường threading hoặc free-threading; iterator mặc định không an toàn luồng, nên giá trị có thể bị bỏ qua hoặc trạng thái nội bộ của iterator có thể bị hỏng
  • threading.serialize_iterator trong Python 3.15 bọc một iterator sẵn có để tuần tự hóa việc tiêu thụ nó giữa các luồng
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)
  • Trước đây, để đồng bộ việc tiêu thụ giữa các luồng, người ta chủ yếu dựa vào Queue; nhưng với các tiện ích mới này, có thể giữ nguyên mức trừu tượng iterator hiện có ngay cả trong mã đa luồng

Tính năng bổ sung

  • Phép xor cho Counter

    • collections.Counter là lớp giúp đếm tần suất xuất hiện rời rạc một cách dễ dàng; nó hoạt động gần giống dict[KeyType, int] và cung cấp nhiều phép toán hữu ích
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 cũng có các phép &, | tương ứng với giao và hợp
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 có thể được xem như một tập hợp các đối tượng rời rạc, và ví dụ trên có thể được hiểu như sau
{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}
  • Trong Python 3.15, phép xor cũng được bổ sung
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}
  • Nếu trước đây không thường dùng các phép toán tập hợp của Counter, có thể sẽ khó nghĩ ra ngay trường hợp sử dụng cụ thể cho xor, nhưng đây là một bổ sung giúp hoàn thiện bộ phép toán
  • Đối tượng JSON bất biến

    • Việc bổ sung frozendict trong Python 3.15 giúp có thể biểu diễn đầy đủ các kiểu JSON như mảng, boolean, số thực, null, chuỗi và object dưới dạng bất biến và có thể băm
    • json.loadjson.loads được thêm tham số array_hook để bổ sung cho object_hook
    • Khi dùng cùng array_hook=tupleobject_hook=frozendict, có thể phân tích JSON trực tiếp thành cấu trúc bất biến
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})

1 bình luận

 
Ý kiến trên Hacker News
  • Nhìn ví dụ lazy from typing import Iterator thì tôi tự hỏi có phải cuối cùng Python cũng đã có lazy import rồi không
    Có vẻ tôi đã bỏ lỡ thay đổi này, không rõ đây có phải từ Python 3.15 hay đã có ở các bản trước

    • Là tính năng của 3.15: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • Tôi không rõ lợi ích của lazy import ở đây là gì. Dù sao nếu dùng giá trị đó trong type hint ở phạm vi module thì vẫn phải import nó chứ?
      Làm vậy thì cần đánh giá trì hoãn annotation, mà theo tôi biết thì cái đó chưa được bật mặc định
    • Ở các phiên bản Python trước cũng có thể lách bằng cách cài def __getattr__(name: str) -> object: ở cấp module
    • Đây có vẻ là một trong những tính năng tiêu biểu của Python 3.15 nên bài viết này mới bỏ sót. Trong tài liệu What's New nó cũng được nhắc đến đầu tiên, nên hoàn toàn có thể xem là tính năng nổi bật
      Cá nhân tôi rất mong chờ. Ngay tuần này tôi đã thấy chỉ vì thêm import cho một module mà ứng dụng thực tế còn không dùng tới, tiến trình Python đã vượt giới hạn bộ nhớ và bị hết bộ nhớ
    • Python gần như từ ngày đầu đã hỗ trợ lazy import theo kiểu đặt câu lệnh import bên trong hàm. Thư viện sẽ không được import cho tới khi hàm đó được gọi
  • Việc thêm frozendict vào 3.15 có nghĩa là giờ có thể biểu diễn mọi kiểu trong JSON — mảng, boolean, số thực dấu phẩy động, null, chuỗi, object — dưới dạng bất biến và băm được
    Tính năng cuối cùng này tôi thật sự rất thích

  • Tôi thích việc Python 3.15 thêm công cụ nguyên thủy đồng bộ hóa Iterator: https://docs.python.org/3.15/library/threading.html#iterator...
    Gói threaded-generator tôi viết cũng làm đúng việc này bằng thread/process + generator + queue, nên có vẻ sẽ bổ sung cho nó rất tốt: https://pypi.org/project/threaded-generator/

  • Có người nói khó nghĩ ra chỗ dùng cho các phép toán tập hợp của Counter, nhất là xor, nhưng chỉ cần nhìn vào hiệu đối xứng là được
    https://en.wikipedia.org/wiki/Symmetric_difference

    • Đúng là vậy, nhưng khi áp dụng vào Counter thì nó trở thành hiệu đối xứng của đa tập, mà khái niệm này không có định nghĩa tự nhiên
      Nếu tôi hiểu đúng đề xuất thì nó được định nghĩa bằng trị tuyệt đối của chênh lệch số lượng từng phần tử, nhưng như vậy còn không thỏa tính kết hợp. Nếu chỉ xét parity thì có thể diễn giải như phép cộng trong F_2, nghe tự nhiên hơn, nhưng tôi vẫn chưa hình dung được sẽ dùng thực tế ở đâu
  • Một ví dụ về Counter là sai. Tôi đã kiểm tra ở cả 3.13 và 3.15.0a
    Kết quả của Counter(a=3, b=1) - Counter(a=1, b=2)Counter({'a': 2})

    • Tôi cũng thấy chỗ đó. Theo tài liệu, các đối tượng Counter hỗ trợ nhiều phép toán để kết hợp thành đa tập; phép cộng và trừ cộng/trừ số lượng phần tử tương ứng, còn giao và hợp trả về số lượng tối thiểu/tối đa
      Mỗi phép toán có thể nhận đầu vào với số lượng âm, nhưng đầu ra sẽ loại bỏ các kết quả có số lượng nhỏ hơn hoặc bằng 0. Dù sao đây đúng là một Counter-example rất hay ;-)
  • Tôi từng cực kỳ mê Python suốt 10 năm và rất thích làm việc với nó, nhưng trong thế giới sau AI codebot, chỉ riêng năm nay tôi đã xóa hơn 100 nghìn dòng và chuyển sang ngôn ngữ nhanh hơn. Dạo này chủ yếu là chuyển sang Go

    • Ban đầu có thể đơn giản, nhưng tôi tò mò rồi về sau anh sẽ làm gì với bảo trì các dự án đó, nhất là khi thêm tính năng phức tạp hơn
      Một cách có thể là tạo prototype bằng Python rồi chuyển đổi
    • Go thực sự không ổn cho tính toán khoa học hay machine learning. Hệ sinh thái thư viện không đủ mạnh, và ngay cả việc bọc C API với trợ giúp của LLM cũng còn yếu
      Thử viết mã xử lý tín hiệu có filter, windowing, overlap... thì gần như không có cách dễ dàng nào với thư viện hiện tại
    • Tôi vẫn luôn tìm một web framework toàn diện kiểu Django cho Go. Nếu có thứ như vậy chắc tôi sẽ mê ngay
    • Ngay từ đầu vì sao anh lại dùng Python? Anh sẽ khuyên gì cho một người hoàn toàn chưa biết lập trình?
    • Thú vị đấy. Nếu không phiền thì tôi muốn biết đó là dự án công việc hay dự án cá nhân
  • Có một cuộc phỏng vấn hay về cấu trúc nội bộ và cách vận hành của Python, nhất là liên quan tới free-threading: https://alexalejandre.com/programming/interview-with-ngoldba...

  • Ôi Python yêu dấu của tôi. Tôi đã dùng bạn gần 15 năm. Tôi nhớ bạn, nhưng giờ không còn dùng nữa. Không phải lỗi của bạn, chỉ là cuộc sống đã thay đổi

    • Python hiện đại ngày nay tôi thấy dùng rất vui, cả trong công việc lẫn dự án cá nhân
    • Có ai đang làm một ngôn ngữ kiểu Python mạnh hơn nhưng ít gánh nặng hơn, đồng thời vẫn tích hợp tốt với Python không?
  • Iterator, hàm bất đồng bộ và iterator bất đồng bộ có ngữ nghĩa khác với hàm thông thường nên trước giờ không hợp lắm với decorator. Khi được gọi, chúng lập tức trả về generator object, coroutine function hoặc async generator object, nên decorator kết thúc ngay thay vì bao trọn toàn bộ vòng đời mà nó bọc
    Ở 3.15, ContextDecorator được thay đổi để kiểm tra kiểu hàm mà nó bọc, nhờ đó decorator sẽ bao trùm toàn bộ vòng đời. Tôi rất thích ý tưởng này, nhưng việc thay đổi tinh vi hành vi hiện có mà không có cơ chế chọn áp dụng có vẻ khá rủi ro. Đây đúng là kiểu tình huống “sưởi ấm bằng phím cách”, tức chỉ thành vấn đề nếu ai đó cố tình dùng decorator theo cách cũ vốn đã hỏng, nhưng nếu thực sự có người làm vậy thì có thể họ sẽ vỡ bất ngờ

    • Có vẻ nhóm phát triển Python đánh giá khả năng có người phụ thuộc vào hành vi cũ là khá thấp: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • Trường hợp xấu nhất là gì? Các thay đổi không tương thích khiến lập trình viên cứ bám lấy phiên bản Python cũ à? Chuyện đó chắc đâu thể xảy ra
  • Những tính năng nhỏ kiểu này rốt cuộc lại thường là thứ hữu ích nhất. Đặc biệt tôi muốn thử các bổ sung mới trong thư viện chuẩn này với dự án hiện tại