1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Python 3.15.0b1 เข้าสู่ช่วงฟีเจอร์ฟรีซแล้ว นอกจาก lazy imports และ Tachyon profiler ก็ยังมีการปรับปรุงเชิงปฏิบัติที่ถูกยืนยันเข้ามาด้วย
  • TaskGroup.cancel() ของ asyncio ช่วยยกเลิก task group ได้อย่างสวยงาม โดยไม่ต้องใช้ข้อยกเว้นแบบกำหนดเองและ contextlib.suppress
  • ContextDecorator ถูกปรับให้ครอบคลุมวงจรชีวิตทั้งหมดของฟังก์ชันอะซิงก์ เจเนอเรเตอร์ และอะซิงก์อิเทอเรเตอร์
  • ยูทิลิตีใหม่ใน threading ช่วย serialize หรือทำสำเนาการ consume อิเทอเรเตอร์ข้ามเธรดได้ โดยยังคง abstraction เดิมไว้โดยไม่ต้องใช้ Queue
  • มีการเพิ่ม การดำเนินการ xor ให้ Counter และ json.loads รองรับการพาร์ส JSON แบบ immutable ด้วย array_hook และ frozendict

ความเปลี่ยนแปลงที่ไม่ค่อยถูกพูดถึงใน Python 3.15

  • เมื่อ Python 3.15.0b1 เข้าสู่ช่วงฟีเจอร์ฟรีซ ก็หมายความว่าฟีเจอร์ที่จะเข้ามาใน Python ปีนี้ถูกกำหนดแล้ว โดยความเปลี่ยนแปลงใหญ่มีทั้ง lazy imports และ Tachyon profiler
  • ใน Python 3.15 ยังมี การเปลี่ยนแปลงฟีเจอร์เล็ก ๆ ที่ใช้งานได้จริงซึ่งอาจไม่เด่นเท่า PEP ใหญ่ ๆ รวมอยู่ด้วย ทั้งฝั่ง asyncio, context manager, อิเทอเรเตอร์ที่ปลอดภัยต่อเธรด, Counter และการพาร์ส JSON

การยกเลิก asyncio TaskGroup

  • ความเปลี่ยนแปลงสำคัญใน asyncio คือการเพิ่มความสามารถในการ ยกเลิก TaskGroup อย่างสวยงาม
  • TaskGroup เป็นรูปแบบหนึ่งของstructured concurrency ที่ช่วยให้สร้างงานพร้อมกันหลายงานได้อย่างเป็นระเบียบและรอจนทุกงานเสร็จสิ้น
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()
  • วิธีนี้ทำงานได้เพราะเมื่อมีข้อยกเว้นเกิดขึ้นใน task group งานอื่น ๆ จะถูกยกเลิก และข้อยกเว้น Interrupt ที่ผู้ใช้กำหนดจะถูกสร้างขึ้นมาเป็นส่วนหนึ่งของ ExceptionGroup ก่อนถูกกรองทิ้งโดย contextlib.suppress
  • แม้รูปแบบการทำงานของ suppress ร่วมกับ ExceptionGroup จะถูกเพิ่มเข้ามาตั้งแต่ Python 3.12 แต่ก็ไม่ได้รับความสนใจมากนัก
  • TaskGroup.cancel ใน Python 3.15 ทำสิ่งเดียวกันนี้ได้อย่างง่ายกว่ามาก
async with asyncio.TaskGroup() as tg:
    tg.create_task(run())
    tg.create_task(run())

    if await wait_for_signal():
        tg.cancel()
  • TaskGroup.cancel() ยกเลิกทั้งกลุ่มโดยไม่โยนข้อยกเว้น จึงไม่ต้องใช้การจับคู่ระหว่างข้อยกเว้นเฉพาะกับ suppress อีกต่อไป

การปรับปรุง context manager

  • context manager สามารถถูกใช้เป็น decorator ได้โดยตรงมาตั้งแต่ 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)(...)
  • context manager อย่าง duration() ที่พิมพ์เวลาในการทำงานของบล็อกโค้ดนั้นสะดวกมากเมื่อใช้เหมือน function decorator แต่เคยมีกรณีที่ทำงานไม่ถูกต้องกับ ฟังก์ชันอะซิงก์, เจเนอเรเตอร์ และอะซิงก์อิเทอเรเตอร์
@duration('async workload')
async def async_workload():
    ...

@duration('generator workload')
def workload():
    while True:
        yield ...
  • อิเทอเรเตอร์, ฟังก์ชันอะซิงก์ และอะซิงก์อิเทอเรเตอร์ มีความหมายต่างจากฟังก์ชันทั่วไป เพราะเมื่อถูกเรียกจะคืนค่าเป็น generator object, coroutine object และ async generator object ทันทีตามลำดับ
  • decorator แบบเดิมไม่สามารถครอบคลุมวงจรชีวิตทั้งหมดของสิ่งที่ห่อไว้ได้ และจบการทำงานทันที ทำให้ไม่ได้ครอบการทำงานจริงตลอดช่วงเวลา
  • ใน Python 3.15 ContextDecorator จะตรวจชนิดของฟังก์ชันที่ถูกห่อ และปรับให้ decorator ครอบคลุม วงจรชีวิตทั้งหมด ของเป้าหมายนั้น
  • ช่วยหลีกเลี่ยงกับดักที่พบบ่อยเมื่อใช้ context manager เป็น decorator และทำให้ใช้ไวยากรณ์ที่สะอาดขึ้นได้

อิเทอเรเตอร์ที่ปลอดภัยต่อเธรด

  • อิเทอเรเตอร์เป็นหนึ่งใน abstraction หลักของ 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)
  • abstraction นี้อาจพังได้ในสภาพแวดล้อมแบบ threading หรือ free-threading เพราะอิเทอเรเตอร์ปกติ ไม่ปลอดภัยต่อเธรด และอาจทำให้ค่าบางตัวถูกข้ามไปหรือสถานะภายในของอิเทอเรเตอร์เสียหาย
  • threading.serialize_iterator ใน Python 3.15 ใช้ห่ออิเทอเรเตอร์เดิมเพื่อ serialize การ consume ข้ามเธรด
import threading

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

with ThreadPoolExecutor() as executor:
    fut1 = executor.submit(consume, events)
    fut2 = executor.submit(consume, events)
  • threading.synchronized_iterator เป็น decorator ที่ใช้ threading.serialize_iterator กับผลลัพธ์ของ generator function
  • 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 เพื่อซิงโครไนซ์การ consume ข้ามเธรด แต่ยูทิลิตีใหม่เหล่านี้ช่วยให้ยังคง abstraction แบบอิเทอเรเตอร์เดิมไว้ได้แม้อยู่ในโค้ดแบบมัลติเธรด

ฟีเจอร์เพิ่มเติม

  • การดำเนินการ xor ของ Counter

    • 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 แบบ immutable

    • เมื่อ Python 3.15 เพิ่ม frozendict เข้ามา ก็ทำให้สามารถแทนชนิดข้อมูล JSON ทั้งอาร์เรย์ บูลีน จำนวนจริง null สตริง และออบเจ็กต์ในรูปแบบที่ immutable และ hashable ได้ทั้งหมด
    • มีการเพิ่มพารามิเตอร์ array_hook ให้ json.load และ json.loads เพื่อเสริม object_hook เดิม
    • เมื่อใช้ array_hook=tuple และ object_hook=frozendict ร่วมกัน ก็สามารถพาร์สออบเจ็กต์ JSON ให้เป็นโครงสร้างแบบ immutable ได้โดยตรง
json.loads('{"a": [1, 2, 3, 4]}', array_hook=tuple, object_hook=frozendict) == frozendict({'a': (1, 2, 3, 4)})

1 ความคิดเห็น

 
GN⁺ 3 시간 전
ความคิดเห็นใน Hacker News
  • ดูจากตัวอย่างที่เขียนแบบ lazy from typing import Iterator ก็เลยสงสัยว่าในที่สุด Python มี lazy import แล้วหรือเปล่า
    เหมือนผมพลาดการเปลี่ยนแปลงนี้ไป เลยอยากรู้ว่านี่เริ่มตั้งแต่ Python 3.15 หรือมีมาก่อนหน้านี้แล้ว

    • เป็นฟีเจอร์ของ 3.15: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • ผมยังไม่ค่อยเข้าใจว่าประโยชน์ของ lazy import ตรงนี้คืออะไร ยังไงถ้าใช้ค่านั้นใน type hint ระดับโมดูลก็ต้อง import อยู่ดีไม่ใช่เหรอ?
      ถ้าจะให้ได้ผลก็น่าจะต้องมี การประเมิน annotation แบบหน่วงเวลา ด้วย ซึ่งเท่าที่รู้มันไม่ได้เปิดเป็นค่าเริ่มต้น
    • ใน Python เวอร์ชันก่อนหน้านี้ก็เลี่ยงได้ด้วยการใส่ def __getattr__(name: str) -> object: ไว้ที่ระดับโมดูล
    • นี่น่าจะเป็นหนึ่งในฟีเจอร์เด่นของ Python 3.15 เลย เลยเหมือนบทความนี้ตกหล่นไป เพราะในเอกสาร What's New ก็พูดถึงเป็นอย่างแรก จึงนับเป็น ฟีเจอร์เด่น ได้เต็มตัว
      ส่วนตัวตื่นเต้นมาก อาทิตย์นี้เองผมเพิ่งเจอว่าแค่เพิ่ม import ของโมดูลที่แอปไม่ได้ใช้งานจริง ก็ทำให้ Python process ทะลุขีดจำกัดหน่วยความจำจน หน่วยความจำไม่พอ
    • Python ทำ lazy import แบบเอา import ไปไว้ในฟังก์ชันได้แทบจะตั้งแต่วันแรกแล้ว เพราะไลบรารีจะยังไม่ถูก import จนกว่าจะมีการเรียกฟังก์ชันนั้น
  • พอ frozendict ถูกเพิ่มเข้ามาใน 3.15 ตอนนี้ก็สามารถแทนทุกชนิดข้อมูลของ JSON ไม่ว่าจะเป็นอาร์เรย์ บูลีน floating point null สตริง และอ็อบเจ็กต์ ให้เป็น immutable และ hashable ได้ทั้งหมด
    ฟีเจอร์สุดท้ายนี่ผมชอบมากจริง ๆ

  • ชอบที่ Python 3.15 เพิ่ม primitive สำหรับซิงก์ iterator เข้ามา: https://docs.python.org/3.15/library/threading.html#iterator...
    แพ็กเกจ threaded-generator ที่ผมทำก็ใช้ thread/process + generator + queue ทำเรื่องนี้อยู่พอดี น่าจะมาเติมเต็มกันได้ดี: https://pypi.org/project/threaded-generator/

  • เขาบอกว่านึก use case ของ set operation ใน Counter โดยเฉพาะ xor ไม่ออก แต่ถ้าดูเป็น symmetric difference ก็พอเห็นภาพ
    https://en.wikipedia.org/wiki/Symmetric_difference

    • ก็จริง แต่พอเอามาใช้กับ Counter มันจะกลายเป็น symmetric difference ของ multiset ซึ่งไม่มีนิยามที่เป็นธรรมชาติชัดเจน
      ถ้าผมเข้าใจข้อเสนอนี้ถูก มันเหมือนนิยามจากค่าสัมบูรณ์ของผลต่างจำนวนครั้งของแต่ละสมาชิก ซึ่งไม่เป็น associative ด้วย ถ้ามองแค่ parity ก็พอตีความเป็นการบวกใน F_2 ได้ เลยดูเป็นธรรมชาติกว่า แต่ก็ยังนึกไม่ออกว่าจะเอาไปใช้จริงตรงไหน
  • ตัวอย่าง Counter อันหนึ่งผิด ผมลองทั้งใน 3.13 และ 3.15.0a แล้ว
    ผลของ Counter(a=3, b=1) - Counter(a=1, b=2) คือ Counter({'a': 2})

    • ผมก็เห็นเหมือนกัน ตามเอกสารบอกว่า Counter มีชุดการดำเนินการทางคณิตศาสตร์หลายแบบสำหรับเอาอ็อบเจ็กต์มาประกอบเป็น multiset โดยการบวกและลบจะบวกหรือลบจำนวนครั้งของสมาชิกที่ตรงกัน ส่วน intersection และ union จะคืนค่าจำนวนครั้งต่ำสุด/สูงสุดตามลำดับ
      การดำเนินการแต่ละแบบรับอินพุตที่มีจำนวนครั้งติดลบได้ แต่ผลลัพธ์จะตัดสมาชิกที่มีจำนวนครั้งน้อยกว่าหรือเท่ากับ 0 ออกไป ยังไงก็ตาม นี่เป็น Counter-example ที่เจ๋งดี ;-)
  • ผมอินกับ Python มากมาตลอด 10 ปีและสนุกกับการทำงานด้วยมัน แต่ในโลกหลัง AI codebot ปีนี้ปีเดียวผมลบโค้ดไปแล้วเกิน 100,000 บรรทัด แล้วค่อย ๆ ย้ายไปภาษาที่เร็วกว่า ช่วงนี้ย้ายไป Go เป็นหลัก

    • ตอนเริ่มต้นมันคงง่าย แต่ผมสงสัยว่าต่อไปจะดูแลรักษาโปรเจกต์พวกนั้นยังไง โดยเฉพาะเวลาเพิ่มฟีเจอร์ที่ซับซ้อนขึ้น
      วิธีหนึ่งอาจเป็นทำ prototype ด้วย Python ก่อนแล้วค่อยแปลง
    • Go ไม่เหมาะกับงานคำนวณเชิงวิทยาศาสตร์หรือแมชชีนเลิร์นนิงจริง ๆ ไลบรารียังไม่พร้อม และเรื่องห่อ C API ก็ยังอ่อน แม้จะมี LLM ช่วยก็ตาม
      ถ้าลองเขียนโค้ด signal processing ที่มีพวก filter, windowing, overlap เข้าไป จะเห็นว่าแทบไม่มีทางทำให้สะดวกด้วยไลบรารีที่มีอยู่ตอนนี้
    • ผมหา เว็บเฟรมเวิร์ก แบบครบเครื่องสไตล์ Django สำหรับ Go อยู่ตลอด ถ้ามีแบบนั้นออกมาคงโดนใจมาก
    • ว่าแต่เดิมทีทำไมถึงเลือกใช้ Python? แล้วถ้าจะให้แนะนำคนที่ไม่เคยเขียนโปรแกรมเลย จะให้เริ่มจากอะไร?
    • น่าสนใจนะ ถ้าไม่ติดอะไรผมอยากรู้ว่านั่นเป็น โปรเจกต์งาน หรือโปรเจกต์ส่วนตัว
  • มีบทสัมภาษณ์ดี ๆ เกี่ยวกับโครงสร้างภายในและการทำงานของ Python โดยเฉพาะเรื่อง free-threading: https://alexalejandre.com/programming/interview-with-ngoldba...

  • อา Python ที่รักของฉัน ฉันใช้เธอมาเกือบ 15 ปี คิดถึงนะ แต่ตอนนี้ไม่ได้ใช้อีกแล้ว ไม่ใช่ความผิดของเธอหรอก ชีวิตมันเปลี่ยนไป

    • ส่วนตัวผมยังใช้ Python สมัยใหม่ แล้วสนุกมาก ทั้งในงานบริษัทและโปรเจกต์ส่วนตัว
    • มีใครกำลังสร้าง ภาษาคล้าย Python ที่พลังมากกว่าแต่ภาระน้อยกว่า และยังทำงานร่วมกับ Python ได้ดีไหม?
  • iterator, async function และ async iterator มีความหมายต่างจากฟังก์ชันปกติ เลยเข้ากับ decorator ได้ไม่ดีนัก เพราะพอเรียกแล้วมันจะคืน generator object, coroutine function หรือ async generator object กลับมาทันที ทำให้ decorator จบทันทีแทนที่จะครอบทั้งวงจรชีวิตของสิ่งที่ห่อไว้
    ใน 3.15 ContextDecorator จะเช็กชนิดของฟังก์ชันที่ห่ออยู่ แล้วทำให้ decorator ครอบคลุมทั้งวงจรชีวิตได้ด้วย ซึ่งผมชอบไอเดียนี้มาก แต่การเปลี่ยนพฤติกรรมเดิมแบบค่อนข้างละเอียดอ่อนโดย ไม่มีตัวเลือกให้ใช้เฉพาะจุด ก็ดูเสี่ยงพอสมควร ถึงจะเป็นสถานการณ์แบบ “spacebar heater” ที่จะมีปัญหาได้ก็ต่อเมื่อมีคนตั้งใจพึ่งพาพฤติกรรมเดิมที่พังอยู่แล้วก็ตาม แต่ถ้ามีจริงก็อาจพังแบบไม่คาดคิดได้

    • ดูเหมือนทีม Python core จะมองว่าโอกาสที่มีคนพึ่งพาพฤติกรรมเดิมนั้นต่ำ: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • กรณีเลวร้ายที่สุดจะเป็นอะไรไปได้ล่ะ? ก็แค่นักพัฒนายังใช้ Python เวอร์ชันเก่าต่อเพราะการเปลี่ยนแปลงที่ไม่เข้ากัน แหม เรื่องแบบนั้นคงไม่มีทางเกิดหรอก
  • ฟีเจอร์เล็ก ๆ แบบนี้แหละที่ท้ายที่สุดมักจะมีประโยชน์ที่สุด โดยเฉพาะตอนนี้ผมอยากลองของใหม่ใน standard library ของโปรเจกต์ปัจจุบันมาก