1P by GN⁺ 3시간전 | ★ favorite | 댓글 1개
  • Python 3.15.0b1 기능 동결로 지연 임포트와 Tachyon 프로파일러 외에도 실용적인 개선들이 확정됨
  • asyncioTaskGroup.cancel() 은 사용자 정의 예외와 contextlib.suppress 없이 태스크 그룹을 우아하게 취소함
  • ContextDecorator는 비동기 함수·제너레이터·비동기 반복자의 전체 생애주기를 감싸도록 바뀜
  • threading 새 유틸리티는 반복자 소비를 스레드 간 직렬화하거나 복제해 Queue 없이 추상화를 유지하게 해줌
  • Counter에는 xor 연산이 추가되고, json.loadsarray_hookfrozendict로 불변 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.loadsarray_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)})

댓글과 토론

Hacker News 의견들
  • 예시를 보면 lazy from typing import Iterator처럼 쓰는데, Python에 드디어 지연 import가 생긴 건가 싶음
    이 변경을 놓친 것 같은데, 이것도 Python 3.15부터인지 아니면 이전 버전에도 있던 건지 궁금함

    • 3.15 기능임: https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-...
    • 여기서 지연 import가 주는 이점이 뭔지 모르겠음. 어차피 모듈 범위의 타입 힌트에서 그 값을 쓰면 import가 필요하지 않나?
      그러려면 어노테이션 지연 평가가 필요할 텐데, 기본으로 켜져 있지는 않은 걸로 알고 있음
    • 이전 Python 버전에서도 모듈 수준에 def __getattr__(name: str) -> object:를 구현하면 우회 가능함
    • 이건 Python 3.15의 대표 기능 중 하나라서 이 글에는 빠진 듯함. What's New 문서에서도 첫 번째로 언급되니 확실히 대표 기능으로 봐도 됨
      개인적으로 정말 기대 중임. 바로 이번 주에도 애플리케이션에 실제로 쓰지 않는 모듈 import가 추가된 것만으로 Python 프로세스가 메모리 한계를 넘어서 메모리 부족이 난 걸 봤음
    • Python은 거의 첫날부터 함수 안에 import 문을 넣는 식의 지연 import가 가능했음. 그 함수가 호출되기 전까지는 라이브러리가 import되지 않음
  • frozendict가 3.15에 추가되면서 이제 JSON의 모든 타입, 즉 배열, 불리언, 부동소수점, null, 문자열, 객체를 불변이자 해시 가능한 형태로 표현할 수 있게 됨
    이 마지막 기능은 정말 마음에 듦

  • Python 3.15에 Iterator 동기화 원시 도구가 추가된 게 좋음: https://docs.python.org/3.15/library/threading.html#iterator...
    내가 만든 threaded-generator 패키지도 스레드/프로세스 + 생성기 + 큐로 바로 이걸 하고 있는데, 잘 보완해 줄 듯함: https://pypi.org/project/threaded-generator/

  • Counter의 집합 연산에서 특히 xor의 사용처를 떠올리기 어렵다고 했는데, 대칭 차집합을 보면 됨
    https://en.wikipedia.org/wiki/Symmetric_difference

    • 맞긴 한데 Counter에 적용하면 다중집합의 대칭 차집합이 되고, 이건 자연스러운 정의가 없음
      제안을 제대로 이해했다면 각 원소 개수 차이의 절댓값으로 정의하는 것 같은데, 이건 결합법칙도 성립하지 않음. 패리티만 본다면 F_2에서의 덧셈으로 해석할 수 있어 더 자연스럽지만, 그래도 실제로 어디에 쓸지는 잘 안 보임
  • Counter 예시 중 하나는 틀렸음. 3.13과 3.15.0a 둘 다에서 확인했음
    Counter(a=3, b=1) - Counter(a=1, b=2)의 결과는 Counter({'a': 2})

    • 나도 그걸 봤음. 문서에 따르면 Counter 객체를 결합해 다중집합을 만들기 위한 여러 수학 연산이 제공되고, 덧셈과 뺄셈은 대응하는 원소 개수를 더하거나 빼며, 교집합과 합집합은 각각 최소/최대 개수를 반환함
      각 연산은 음수 개수를 가진 입력도 받을 수 있지만, 출력에서는 개수가 0 이하인 결과를 제외함. 어쨌든 멋진 Counter-example임 ;-)
  • 10년 동안 Python에 정말 빠져 있었고 작업하기 즐거웠지만, AI 코드봇 이후의 세계에서는 올해만 이미 10만 줄 넘게 지우고 더 빠른 언어로 옮겼음. 요즘은 주로 Go로 옮기는 중임

    • 처음에는 간단하겠지만, 앞으로 그 프로젝트들의 유지보수, 특히 더 복잡한 기능 추가는 어떻게 할 생각인지 궁금함
      한 가지 방법은 Python으로 프로토타입을 만들고 변환하는 식일 수 있겠음
    • Go는 과학 계산이나 머신러닝 작업에는 정말 별로임. 라이브러리가 갖춰져 있지 않고, C API를 감싸는 이야기도 LLM 도움을 받아도 약함
      필터, 윈도잉, 오버랩 같은 게 들어간 신호 처리 코드를 써보면, 현재 라이브러리만으로는 쉽게 할 방법이 거의 없음
    • Go용으로 Django 같은 종합적인 웹 프레임워크를 계속 찾고 있음. 그런 게 나오면 바로 꽂힐 듯함
    • 애초에 왜 Python을 쓰게 됐는지 궁금함. 프로그래밍을 전혀 모르는 사람에게는 무엇을 추천하겠음?
    • 흥미롭네. 괜찮다면 그게 업무 프로젝트였는지 개인 프로젝트였는지 궁금함
  • Python 내부 구조와 운영, 특히 free-threading과 관련해 좋은 인터뷰가 있음: https://alexalejandre.com/programming/interview-with-ngoldba...

  • 아, 내 사랑 Python. 거의 15년 동안 너를 썼음. 그립지만 이제는 더 이상 쓰지 않음. 네 잘못은 아니고, 삶이 바뀌었음

    • 요즘의 현대적인 Python은 회사 일과 개인 프로젝트 양쪽에서 정말 즐겁게 쓰고 있음
    • Python과 잘 연동되면서도 짐은 덜 가진, 더 강력한 Python 비슷한 언어를 누가 만들고 있나?
  • 반복자, 비동기 함수, 비동기 반복자는 일반 함수와 의미가 달라서 데코레이터와 잘 맞지 않았음. 호출하면 각각 생성기 객체, 코루틴 함수, 비동기 생성기 객체를 즉시 반환하므로, 데코레이터가 감싸는 전체 생명주기가 아니라 즉시 끝나버림
    3.15에서는 ContextDecorator가 감싸는 함수 타입을 확인해 데코레이터가 전체 생명주기를 덮도록 바뀌는데, 아이디어는 아주 마음에 들지만 선택 적용 장치 없이 기존 사용처의 동작을 미묘하게 바꾸는 점은 꽤 위험해 보임. 누군가 예전의 깨진 방식으로 의도적으로 데코레이터를 썼어야 문제가 되는 “스페이스바 난방” 같은 상황이긴 해도, 실제로 그랬다면 예상치 못하게 깨질 수 있음

    • Python 코어 팀은 기존 동작에 의존하는 사람이 있을 가능성을 낮게 보는 듯함: https://github.com/python/cpython/pull/136212#issuecomment-4...
    • 최악의 경우가 뭐겠음? 호환되지 않는 변경 때문에 개발자들이 예전 Python 버전을 계속 쓰는 정도? 그런 일이 생길 리 없잖아
  • 이런 작은 기능들이 결국 가장 유용해지는 경우가 많음. 특히 현재 프로젝트에서 새 표준 라이브러리 추가 기능들을 시험해 보고 싶음