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년 운영 위원회가 거부한 이유는 다음과 같습니다.

  • 일부 지지자들이 서드파티 패키지에 대한 의존성을 줄이기 위해 찬성 의견을 제시했음에도 불구하고, 이 기능이 표준 라이브러리에 필요하다는 강력한 합의의 증거를 찾지 못했습니다. 이러한 기능이 필요한 사용자에게는 attrs와 Pydantic(PEP에서 참조하는 라이브러리)과 같은 기존 서드파티 라이브러리가 허용 가능한 대안이라고 생각합니다.
  • 이 기능은 표준 라이브러리에 추가된, 더 많은 불필요한 잔재로 간주될 수 있는 요소의 누적처럼 보이며, dataclasses가 이상적인 “단순한” 사용 사례에서 점점 더 멀어지게 합니다.
  • PEP의 “이것을 가르치는 방법” 절을 읽어 보면 함정과 주의점이 상당하며, 잠재적인 이점보다 증가한 혼란과 복잡성이 더 크다는 우려가 듭니다.
  • 이 PEP는 라이브러리를 사용하는 사람보다 타입 검사기를 돕는 데 더 초점을 맞춘 것처럼 보입니다.

초록

PEP 557는 Python 표준 라이브러리에 dataclasses를 추가했습니다. PEP 681은 attrs, Pydantic, SQLAlchemy와 Django 같은 객체 관계 매퍼(ORM) 패키지 등 여러 일반적인 dataclass 유사 라이브러리를 타입 검사기가 이해하도록 돕기 위해 dataclass_transform()를 추가했습니다.

다른 라이브러리가 표준 라이브러리 구현에 비해 제공하는 일반적인 기능은 사용자가 제공한 변환 함수를 사용하여 초기화 시 전달된 인자를 각 필드에 대해 예상되는 타입으로 변환하는 기능입니다.

따라서 이 PEP는 각 필드의 입력값을 dataclass에 저장할 표현으로 변환하는 데 사용할 함수를 지정하기 위해 dataclasses.field()converter 매개변수를 추가합니다(dataclasses.Fielddataclass_transform()에 필요한 변경 사항도 함께 추가합니다).

동기

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

  • 사용자 지정 Mypy 플러그인을 선택적으로 사용합니다. 이러한 플러그인은 Mypy가 변환 의미를 이해하도록 돕지만, 다른 도구는 돕지 못합니다.
  • dataclass 생성자를 호출하는 쪽에 변환 책임을 전가합니다. 이로 인해 특정 dataclass를 생성하는 코드가 불필요하게 장황하고 반복적이 될 수 있습니다.
  • “더 넓은” 매개변수 타입을 선언하고 적절한 속성을 설정할 때 해당 타입으로 변환하는 사용자 지정 __init__를 제공합니다. 이는 converter와 __init__사이의 타입 어노테이션을 중복할 뿐만 아니라, 사용자가 dataclasses가 제공하는 여러 기능을 사용하지 못하게 합니다.
  • 변환이 필요한 매개변수 타입에 의미 있는 타입 어노테이션이 없는 사용자 지정 __init__를 제공합니다.

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

근거

인자 변환 의미를 추가하는 것은 대부분의 dataclass 유사 라이브러리가 지원을 제공할 만큼 유용하고 이점이 큽니다. 이 기능을 표준 라이브러리에 추가하면 더 많은 사용자가 서드파티 라이브러리 없이 이러한 이점을 선택적으로 사용할 수 있습니다. 또한 서드파티 라이브러리는 dataclass_transform()에 대한 지원을 추가하여 타입 검사기가 자체 변환 의미를 파악하도록 할 수 있으므로, 이러한 라이브러리의 사용자도 혜택을 받습니다.

사양

새로운 converter 매개변수

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

동결된 데이터클래스에서는 속성을 설정할 때 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는 converter의 입력 타입이고 X는 converter의 출력 타입입니다.

defaultdefault_factory의 타입 검사

기본값은 converter를 사용하여 무조건 변환되므로, default또는 default_factory와 함께 converter인자가 제공되면 기본값의 타입(제공된 경우 default인자, 그렇지 않으면 default_factory의 반환값)을 converter호출 가능 객체의 단일 인자 타입을 사용하여 검사해야 합니다.

Converter 반환 타입

호출 가능 객체의 반환 타입은 필드에 선언된 타입과 호환되는 타입이어야 합니다. 여기에는 필드의 타입과 정확히 일치하는 경우가 포함되지만, 더 구체적인 타입일 수도 있습니다(예를 들어 list로 어노테이션된 필드에 대해 list[int]를 반환하는 converter 또는 int | str로 어노테이션된 필드에 대해 int를 반환하는 converter가 해당합니다).

허용 가능한 인자 타입의 간접성

이 PEP가 도입하는 한 가지 단점은 데이터 클래스를 읽는 것만으로는 데이터 클래스의 __init__에서와 특성 할당 중에 어떤 인자 타입이 허용되는지 즉시 명확하지 않다는 점입니다. 허용 가능한 타입은 converter에 의해 정의됩니다.

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

하위 호환성

이러한 변경 사항은 선택적으로 활성화할 수 있는 새로운 기능만 도입하므로 호환성 문제를 일으키지 않습니다.

보안 관련 영향

이러한 변경 사항에는 직접적인 보안 문제가 없습니다.

이 내용을 가르치는 방법

새로운 매개변수와 동작을 설명하는 문서 및 예제가 문서 사이트의 관련 섹션(주로 dataclasses에서)에 추가되며, What’s New 문서에서 링크됩니다.

추가되는 문서와 예제에서는 converter 사용자가 흔히 겪을 수 있는 “일반적인 함정”도 다룹니다. 이러한 함정에는 다음이 포함됩니다.

  • None/센티널 값을 처리해야 하는 경우
  • 이미 올바른 타입인 값을 처리해야 하는 경우
  • 합성된 __init__매개변수의 타입이 Any가 되므로 converter에 람다를 사용하지 않아야 하는 경우
  • 동결 데이터 클래스에서 사용자가 정의한 __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_transformfield_specifiers에 “converter”를 추가하는 것만 수행하기

이 추가 사항을 dataclass_transform()로 한정하자는 생각은 Typing-SIG에서잠시 논의되었으며, 여기서 이를 dataclasses로 더 일반적으로 확장하자는 제안이 있었습니다.

추가로, 이를 dataclasses에 추가하면 누구나 추가 라이브러리를 요구하지 않고도 그 이점을 누릴 수 있습니다.

기본값을 변환하지 않기

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

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

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

지정된 필드의 타입(예: str 또는 int)을 제공된 각 인자의 변환기로 사용하도록 허용하는 방안이 있을 수 있습니다. Pydantic의 데이터 변환은 이 접근 방식과 유사해 보이는 의미론을 갖습니다.

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

변환기의 반환 타입에서 속성 타입 추론하기

또 다른 방안은 사용자가 fieldconverter 인자를 제공하는 경우 속성의 타입 어노테이션을 생략할 수 있도록 허용하는 것입니다. 이렇게 하면 이 PEP가 도입하는 흔한 반복(예: x: str = field(converter=str))을 줄일 수 있지만, 현재 데이터클래스 의미론, 즉 합성된 __init__이나 dataclasses.fields와 같은 요소에서 속성 순서가 보존되는 특성을 유지하면서 이를 가장 잘 지원하는 방법은 분명하지 않습니다. 이는 현재 Python에서 정의된 순서대로 어노테이션만 있는 속성과 어노테이션이 없는 속성이 서로 섞여 있는 상태를 가져오는 쉬운 방법이 없기 때문입니다.

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

마지막으로, (변환기가 없는 필드를 포함한) 모든 필드를 dataclasses.field에 할당한다면 이 방안은 실현 가능하며, 이 경우 클래스 자체의 네임스페이스가 순서를 보유하게 됩니다. 그러나 이는 타입과 변환기의 반복을 필드 할당의 반복과 맞바꾸는 것입니다. 최종적으로 반복이 늘거나 줄지는 않지만, 데이터클래스 의미론의 복잡성이 추가됩니다.

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

참고 자료