Python 3.15: 헤드라인을 장식하지 못한 기능들
요약
Python 3.15.0b1 기능 동결에 따라 확정된 실용적인 개선 사항들을 소개합니다. asyncio의 TaskGroup 취소 방식 간소화, 컨텍스트 매니저 개선, threading 및 JSON 파싱 기능 업데이트 등 주요 변화를 다룹니다.
핵심 포인트
- TaskGroup.cancel() 도입으로 예외 처리 없이 우아한 태스크 취소 가능
- ContextDecorator가 비동기 함수 및 제너레이터 생애주기를 지원하도록 개선
- threading 유틸리티를 통한 스레드 간 반복자 직렬화/복제 지원
- Counter xor 연산 추가 및 json.loads의 불변 JSON 파싱 지원
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에 의존했지만, 새 유틸리티를 쓰면 멀티스레드 코드에서도 기존 반복자 추상화를 바꾸지 않고 유지할 수 있음
추가 기능
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.load와 json.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)})
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기