Python 3.15: 헤드라인을 장식하지 못한 기능들

6 days ago 8
  • Python 3.15.0b1 기능 동결로 지연 임포트와 Tachyon 프로파일러 외에도 실용적인 개선들이 확정됨
  • asyncio의 TaskGroup.cancel() 은 사용자 정의 예외와 contextlib.suppress 없이 태스크 그룹을 우아하게 취소함
  • ContextDecorator는 비동기 함수·제너레이터·비동기 반복자의 전체 생애주기를 감싸도록 바뀜
  • threading 새 유틸리티는 반복자 소비를 스레드 간 직렬화하거나 복제해 Queue 없이 추상화를 유지하게 해줌
  • Counter에는 xor 연산이 추가되고, json.loads는 array_hook과 frozendict로 불변 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)
  • 이 추상화는 스레딩이나 자유 스레딩 환경에서 깨질 수 있으며, 기본 반복자는 스레드 안전하지 않아 값이 건너뛰어지거나 내부 반복자 상태가 망가질 수 있음
  • 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.loads에 array_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)})
Read Entire Article