Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 712 – dataclasses.field에 “converter” 매개변수 추가

Author:
Joshua Cannon <joshdcannon at gmail.com>
Sponsor:
Eric V. Smith <eric at trueblade.com>
Discussions-To:
Discourse thread
Status:
Rejected
Type:
Standards Track
Created:
01-Jan-2023
Python-Version:
3.13
Post-History:
27-Dec-2022, 19-Jan-2023, 23-Apr-2023
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

거부 공지

2024년 운영 위원회(Steering Council)가 거부한 이유는 다음과 같습니다.

  • 일부 지지자들이 서드파티 패키지에 대한 의존성을 줄이기 위해 찬성 의견을 냈음에도 불구하고, 이 기능이 표준 라이브러리에 필요하다는 강한 합의가 있다는 근거를 찾지 못했습니다. 이러한 기능이 필요한 사용자에게는 attrs나 Pydantic(PEP에서 언급하는 라이브러리) 같은 기존 서드파티 라이브러리가 수용 가능한 대안이라고 생각합니다.
  • 이 기능은 표준 라이브러리에 군더더기로 여겨질 수 있는 것을 더 쌓는 일처럼 보이며, dataclasses가 가장 잘 맞는 “단순한” 사용 사례에서 점점 더 멀어지게 합니다.
  • PEP의 “이것을 가르치는 방법(How to Teach This)” 절을 읽어 보면 함정과 주의할 점이 상당하고, 커진 혼란과 복잡성이 잠재적인 이점보다 크다는 점에서 망설여집니다.
  • 이 PEP는 라이브러리를 사용하는 사람보다 타입 검사기를 돕는 데 더 초점을 맞추고 있는 것으로 보입니다.

초록

PEP 557은 Python 표준 라이브러리에 dataclasses를 추가했습니다. PEP 681은 타입 검사기가 attrs, Pydantic, 그리고 SQLAlchemy와 Django 같은 객체 관계 매퍼(ORM) 패키지 등 널리 쓰이는 여러 데이터클래스 유사 라이브러리를 이해할 수 있도록 dataclass_transform()을 추가했습니다.

다른 라이브러리가 표준 라이브러리 구현보다 더 제공하는 흔한 기능은, 사용자가 제공한 변환 함수를 사용하여 초기화 시점에 주어진 인자를 라이브러리가 각 필드에 기대되는 타입으로 변환하는 기능입니다.

따라서 이 PEP는 각 필드의 입력값을 데이터클래스에 저장될 표현으로 변환하는 데 사용할 함수를 지정할 수 있도록 dataclasses.field()에 converter 매개변수를 추가합니다(dataclasses.Field와 dataclass_transform()에 필요한 변경 사항도 함께 추가합니다).

동기

dataclasses나 서드파티 데이터클래스 유사 라이브러리가 타입 검사가 가능한 방식으로 인자 변환을 지원할 수 있는 기존의 표준 방법은 없습니다. 이 제약을 우회하기 위해 라이브러리 작성자와 사용자는 다음 중 하나를 선택할 수밖에 없습니다.

  • 사용자 정의 Mypy 플러그인을 도입합니다. 이러한 플러그인은 Mypy가 변환 의미론을 이해하도록 돕지만, 다른 도구에는 도움이 되지 않습니다.
  • 변환 책임을 데이터클래스 생성자의 호출자에게 넘깁니다. 이렇게 하면 특정 데이터클래스를 생성하는 코드가 불필요하게 장황하고 반복적이 될 수 있습니다.
  • “더 넓은” 매개변수 타입을 선언하고 해당 속성을 설정할 때 이를 변환하는 사용자 정의 __init__을 제공합니다. 이 방법은 변환기와 __init__ 사이에 타입 어노테이션을 중복시킬 뿐만 아니라, 사용자가 dataclasses가 제공하는 많은 기능을 쓰지 못하게 만듭니다.
  • 사용자 정의 __init__을 제공하되, 변환이 필요한 매개변수 타입에는 의미 있는 타입 어노테이션을 달지 않습니다.

이러한 선택지 중 어느 것도 이상적이지 않습니다.

근거

인자 변환 의미론은 대부분의 데이터클래스 유사 라이브러리가 지원할 만큼 유용하고 이점이 큽니다. 이 기능을 표준 라이브러리에 추가하면 더 많은 사용자가 서드파티 라이브러리 없이도 이러한 이점을 누릴 수 있습니다. 또한 서드파티 라이브러리는 dataclass_transform()에 추가된 지원을 통해 자체 변환 의미론을 타입 검사기에 알려 줄 수 있으므로, 해당 라이브러리의 사용자도 이점을 얻습니다.

명세

새로운 converter 매개변수

이 명세는 dataclasses.field() 함수에 converter라는 이름의 새 매개변수를 도입합니다. 이 매개변수가 제공되면, 연관된 속성에 값을 할당할 때 모든 값을 변환하는 데 사용되는 단일 인자 호출 가능 객체를 나타냅니다.

동결된(frozen) 데이터클래스에서 변환기는 dataclass가 합성한 __init__ 내부에서 속성을 설정할 때만 사용됩니다. 동결되지 않은 데이터클래스에서 변환기는 모든 속성 대입(예: obj.attr = value)에 사용되며, 여기에는 기본값 대입도 포함됩니다.

속성은 이미 변환된 상태여야 하므로 속성을 읽을 때는 변환기가 사용되지 않습니다.

이 매개변수를 추가하면 다음과 같이 변경되는 점도 있습니다:

예시

def str_or_none(x: Any) -> str | None:
  return str(x) if x is not None else None

@dataclasses.dataclass
class InventoryItem:
    # `converter` as a type (including a GenericAlias).
    id: int = dataclasses.field(converter=int)
    skus: tuple[int, ...] = dataclasses.field(converter=tuple[int, ...])
    # `converter` as a callable.
    vendor: str | None = dataclasses.field(converter=str_or_none))
    names: tuple[str, ...] = dataclasses.field(
      converter=lambda names: tuple(map(str.lower, names))
    )  # Note that lambdas are supported, but discouraged as they are untyped.

    # The default value is also converted; therefore the following is not a
    # type error.
    stock_image_path: pathlib.PurePosixPath = dataclasses.field(
      converter=pathlib.PurePosixPath, default="assets/unknown.png"
    )

    # Default value conversion extends to `default_factory`;
    # therefore the following is also not a type error.
    shelves: tuple = dataclasses.field(
      converter=tuple, default_factory=list
    )

item1 = InventoryItem(
  "1",
  [234, 765],
  None,
  ["PYTHON PLUSHIE", "FLUFFY SNAKE"]
)
# item1's repr would be (with added newlines for readability):
#   InventoryItem(
#     id=1,
#     skus=(234, 765),
#     vendor=None,
#     names=('PYTHON PLUSHIE', 'FLUFFY SNAKE'),
#     stock_image_path=PurePosixPath('assets/unknown.png'),
#     shelves=()
#   )

# Attribute assignment also participates in conversion.
item1.skus = [555]
# item1's skus attribute is now (555,).

타이핑에 미치는 영향

converter는 위치 인자 하나를 받는 호출 가능 객체여야 합니다. 이 위치 인자에 대응하는 매개변수 타입은 해당 필드와 연관되어 합성되는 __init__ 매개변수의 타입이 됩니다.

다시 말해, converter 매개변수에 제공되는 인자는 Callable[[T], X]와 호환되어야 합니다. 여기서 T는 변환기의 입력 타입이고 X는 변환기의 출력 타입입니다.

default와 default_factory의 타입 검사

기본값은 언제나 converter로 변환됩니다. 따라서 converter 인자가 default나 default_factory와 함께 제공되면, 기본값의 타입(default 인자가 제공된 경우 그 인자, 그렇지 않으면 default_factory의 반환값)은 converter 호출 가능 객체가 받는 단일 인자의 타입으로 검사해야 합니다.

변환기 반환 타입

호출 가능 객체의 반환 타입은 필드에 선언된 타입과 호환되어야 합니다. 필드의 타입과 정확히 같은 타입도 여기에 포함되지만, 더 특수화된 타입일 수도 있습니다(예를 들어 list로 어노테이션된 필드에 list[int]를 반환하는 변환기나, int | str로 어노테이션된 필드에 int를 반환하는 변환기).

허용되는 인자 타입의 간접성

이 PEP로 생기는 단점 하나는 데이터클래스만 읽어서는 데이터클래스의 __init__과 속성 대입에서 어떤 인자 타입이 허용되는지 바로 알기 어렵다는 점입니다. 허용되는 타입은 변환기가 정의합니다.

소스 코드를 직접 읽을 때는 그렇습니다. 하지만 typing.reveal_type이나 IDE의 “IntelliSense” 같은 타이핑 관련 보조 도구를 사용하면 소스 코드를 읽지 않고도 정확히 어떤 타입이 허용되는지 쉽게 알 수 있을 것입니다.

하위 호환성

이 변경 사항들은 사용자가 선택해서 쓰는 새 기능만 추가하므로 호환성 문제를 일으키지 않습니다.

보안 영향

이 변경 사항들로 인해 직접적으로 생기는 보안 문제는 없습니다.

교육 방법

새 매개변수와 동작을 설명하는 문서와 예제를 문서 사이트의 관련 섹션(주로 dataclasses에 관한 섹션)에 추가하고, What’s New 문서에서 링크할 것입니다.

추가되는 문서와 예제에서는 변환기 사용자가 겪기 쉬운 “흔한 함정”도 다룰 것입니다. 그러한 함정에는 다음이 포함됩니다:

  • None/센티널 값을 처리해야 하는 점.
  • 이미 올바른 타입인 값을 처리해야 하는 점.
  • 람다를 변환기로 쓰면 합성되는 __init__ 매개변수의 타입이 Any가 되므로 쓰지 말아야 하는 점.
  • 동결된 데이터클래스에서 사용자 정의 __init__ 본문 안의 값을 변환하는 것을 잊어버리는 경우.
  • 동결되지 않은 데이터클래스에서 사용자 정의 __setattr__ 본문 안의 값을 변환하는 것을 잊어버리는 경우.

또한 혼란을 줄 수 있는 패턴 매칭 의미 체계도 다루어야 합니다:

@dataclass
class Point:
    x: int = field(converter=int)
    y: int

match Point(x="0", y=0):
    case Point(x="0", y=0):  # Won't be matched
        ...
    case Point():  # Will be matched
        ...
    case _:
        ...

하지만 이 동작은 초기화 메서드에서 변환을 수행하는 모든 타입에 해당하며, 타입 검사기가 이 함정을 잡아낼 수 있어야 한다는 점에 주목할 필요가 있습니다:

match int("0"):
  case int("0"):  # Won't be matched
      ...
  case _:  # Will be matched
      ...

참조 구현

attrs 라이브러리는 @define 클래스 데코레이터를 사용할 때 동일한 변환기 의미 체계(초기화 메서드에서의 변환 및 속성 설정 시의 변환)를 보이는 converter 매개변수를 이미 포함하고 있습니다.

CPython 지원은 저자 포크의 브랜치에 구현되어 있습니다.

기각된 아이디어

typing.dataclass_transform의 field_specifiers에 “converter”만 추가하기

이 추가 사항을 dataclass_transform()으로 한정하자는 아이디어는 Typing-SIG에서 잠시 논의되었으며, 그 자리에서 이를 좀 더 일반적으로 dataclasses까지 확장하자는 제안이 나왔습니다.

또한 이를 dataclasses에 추가하면 추가 라이브러리 없이도 누구나 그 이점을 누릴 수 있습니다.

기본값을 변환하지 않기

기본값을 변환하는 것과 변환하지 않는 것 모두 장단점이 있습니다. 기본값을 그대로 두면 타입 검사기와 데이터클래스 작성자는 기본값의 타입이 필드의 타입과 일치한다고 기대할 수 있습니다. 하지만 기본값을 변환하면 세 가지 큰 장점이 있습니다:

  1. 일관성. 속성에 할당되는 모든 값을 무조건 변환하면 사용자가 기억해야 할 “특수 규칙”이 줄어듭니다.
  2. 더 간단한 기본값. 기본값이 사용자가 제공하는 값과 같은 타입을 가질 수 있게 하면, 데이터클래스 작성자도 호출자와 똑같은 편의를 누릴 수 있습니다.
  3. attrs와의 호환성. attrs는 기본값을 무조건 변환기로 변환합니다.

필드의 타입을 사용한 자동 변환

한 가지 아이디어는 지정된 필드의 타입(예: str 또는 int)을 제공된 각 인자의 변환기로 사용할 수 있도록 하는 것입니다. Pydantic의 데이터 변환은 이 접근 방식과 비슷해 보이는 의미 체계를 가지고 있습니다.

이 방식은 꽤 단순한 타입에서는 잘 동작하지만, 제네릭 같은 복잡한 타입에서는 기대되는 동작이 모호해집니다. 예를 들어 tuple[int, ...]의 경우, 변환기가 단순히 이터러블을 튜플로 변환해야 하는지, 아니면 각 요소의 타입까지 int로 변환해야 하는지 모호합니다. 또는 호출 가능 객체가 아닌 int | None의 경우도 그렇습니다.

변환기의 반환 타입으로부터 속성 타입 추론하기

또 다른 아이디어는 converter 인자가 있는 field를 제공하는 경우 사용자가 속성의 타입 어노테이션을 생략할 수 있게 하는 것입니다. 이렇게 하면 이 PEP가 도입하는 흔한 반복(예: x: str = field(converter=str))을 줄일 수 있겠지만, 현재의 데이터클래스 의미 체계(즉, 합성된 __init__이나 dataclasses.fields 같은 것에서 속성 순서가 보존된다는 점)를 유지하면서 이를 어떻게 가장 잘 지원할 수 있을지는 분명하지 않습니다. (현재) Python에는 어노테이션만 있는 속성과 어노테이션이 없는 속성이 섞여 있을 때 이들을 정의된 순서대로 쉽게 얻을 방법이 없기 때문입니다.

센티널 어노테이션(예: x: FromConverter = ...)을 적용할 수도 있지만, 이는 타입 어노테이션의 근본적인 전제를 깨뜨립니다.

마지막으로, (변환기가 없는 필드를 포함한) 모든 필드를 dataclasses.field에 할당한다면 클래스 자체의 네임스페이스가 순서를 보존하므로 이 방식이 가능하지만, 이는 타입+변환기의 반복을 필드 할당의 반복으로 맞바꾸는 것에 불과합니다. 결국 반복이 늘지도 줄지도 않으면서 데이터클래스 의미 체계만 더 복잡해집니다.

이 PEP는 그것을 할 수 없다거나 해서는 안 된다고 말하는 것이 아닙니다. 단지 이 PEP에 포함되지 않았을 뿐입니다.

참고 문헌