- 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)
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 ความคิดเห็น
ความคิดเห็นใน Hacker News
ดูจากตัวอย่างที่เขียนแบบ
lazy from typing import Iteratorก็เลยสงสัยว่าในที่สุด Python มี lazy import แล้วหรือเปล่าเหมือนผมพลาดการเปลี่ยนแปลงนี้ไป เลยอยากรู้ว่านี่เริ่มตั้งแต่ Python 3.15 หรือมีมาก่อนหน้านี้แล้ว
ถ้าจะให้ได้ผลก็น่าจะต้องมี การประเมิน annotation แบบหน่วงเวลา ด้วย ซึ่งเท่าที่รู้มันไม่ได้เปิดเป็นค่าเริ่มต้น
def __getattr__(name: str) -> object:ไว้ที่ระดับโมดูลส่วนตัวตื่นเต้นมาก อาทิตย์นี้เองผมเพิ่งเจอว่าแค่เพิ่ม import ของโมดูลที่แอปไม่ได้ใช้งานจริง ก็ทำให้ Python process ทะลุขีดจำกัดหน่วยความจำจน หน่วยความจำไม่พอ
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 ก่อนแล้วค่อยแปลง
ถ้าลองเขียนโค้ด signal processing ที่มีพวก filter, windowing, overlap เข้าไป จะเห็นว่าแทบไม่มีทางทำให้สะดวกด้วยไลบรารีที่มีอยู่ตอนนี้
มีบทสัมภาษณ์ดี ๆ เกี่ยวกับโครงสร้างภายในและการทำงานของ Python โดยเฉพาะเรื่อง free-threading: https://alexalejandre.com/programming/interview-with-ngoldba...
อา Python ที่รักของฉัน ฉันใช้เธอมาเกือบ 15 ปี คิดถึงนะ แต่ตอนนี้ไม่ได้ใช้อีกแล้ว ไม่ใช่ความผิดของเธอหรอก ชีวิตมันเปลี่ยนไป
iterator, async function และ async iterator มีความหมายต่างจากฟังก์ชันปกติ เลยเข้ากับ decorator ได้ไม่ดีนัก เพราะพอเรียกแล้วมันจะคืน generator object, coroutine function หรือ async generator object กลับมาทันที ทำให้ decorator จบทันทีแทนที่จะครอบทั้งวงจรชีวิตของสิ่งที่ห่อไว้
ใน 3.15
ContextDecoratorจะเช็กชนิดของฟังก์ชันที่ห่ออยู่ แล้วทำให้ decorator ครอบคลุมทั้งวงจรชีวิตได้ด้วย ซึ่งผมชอบไอเดียนี้มาก แต่การเปลี่ยนพฤติกรรมเดิมแบบค่อนข้างละเอียดอ่อนโดย ไม่มีตัวเลือกให้ใช้เฉพาะจุด ก็ดูเสี่ยงพอสมควร ถึงจะเป็นสถานการณ์แบบ “spacebar heater” ที่จะมีปัญหาได้ก็ต่อเมื่อมีคนตั้งใจพึ่งพาพฤติกรรมเดิมที่พังอยู่แล้วก็ตาม แต่ถ้ามีจริงก็อาจพังแบบไม่คาดคิดได้ฟีเจอร์เล็ก ๆ แบบนี้แหละที่ท้ายที่สุดมักจะมีประโยชน์ที่สุด โดยเฉพาะตอนนี้ผมอยากลองของใหม่ใน standard library ของโปรเจกต์ปัจจุบันมาก