PEP 702 – 타입 시스템을 사용한 폐지 예정 표시
- Author:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 30-Dec-2022
- Python-Version:
- 3.13
- Post-History:
- 01-Jan-2023, 22-Jan-2023
- Resolution:
- 07-Nov-2023
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 클래스나 함수를 폐지 예정으로 표시하는 @warnings.deprecated() 데코레이터를 추가하여, 해당 클래스나 함수가 사용될 때 정적 검사기가 경고할 수 있도록 합니다. 기본적으로 이 데코레이터는 런타임에 DeprecationWarning도 발생시킵니다.
동기
소프트웨어가 발전함에 따라 새로운 기능이 추가되고 오래된 기능은 쓸모없어집니다. 라이브러리 개발자는 사용자에게 새 API로 이전할 시간을 주면서 쓸모없어진 코드를 제거해 나가기를 원합니다. Python은 이러한 목표를 달성하기 위한 메커니즘을 제공합니다. 바로 폐지 예정인 기능이 사용될 때 경고를 표시하는 데 쓰이는 DeprecationWarning 경고 클래스입니다. 이 메커니즘은 널리 사용됩니다. 이 PEP를 작성하는 시점에 CPython main 브랜치에는 DeprecationWarning을 발생시키는 서로 다른 코드 경로가 약 150개 있습니다. 많은 서드파티 라이브러리도 폐지 예정을 표시하기 위해 DeprecationWarning을 사용합니다. 상위 5000개 PyPI 패키지에는 다음과 같은 결과가 있습니다:
- 정규식
warnings\.warn.*\bDeprecationWarning\b에 일치하는 항목 1911건으로,DeprecationWarning의 사용을 나타냅니다(경고가 여러 줄에 걸쳐 나뉜 경우는 포함하지 않습니다); - 정규식
^\s*@deprecated에 일치하는 항목 1661건으로, 일종의 폐지 예정 데코레이터 사용을 나타냅니다.
그러나 현재 메커니즘은 폐지 예정 기능의 사용자가 제때 코드를 갱신하도록 보장하기에 불충분한 경우가 많습니다. 예를 들어, 오래전부터 폐지 예정이던 여러 unittest 기능의 제거는 사용자에게 코드를 갱신할 시간을 더 주기 위해 Python 3.11에서 되돌려야 했습니다. 사용자가 현실적인 이유로 경고를 끈 채 테스트 스위트를 실행할 수도 있고, 테스트가 다루지 않는 코드 경로에서 폐지 예정 경고가 발생할 수도 있습니다.
사용자가 폐지 예정 기능을 알아낼 수 있는 방법을 더 많이 제공하면 이전 과정을 앞당길 수 있습니다. 이 PEP는 정적 타입 검사기를 활용하여 사용자에게 폐지 예정을 알릴 것을 제안합니다. 이러한 검사기는 사용자 코드를 의미적으로 철저히 이해하므로, grep을 한 번 호출해서는 찾을 수 없는 폐지 예정 사용도 감지하고 보고할 수 있습니다. 또한 많은 타입 검사기가 IDE와 통합되어 있어, 사용자는 편집기에서 바로 폐지 예정 경고를 볼 수 있습니다.
근거
언뜻 보기에 폐지 예정은 타입 검사기가 다룰 주제가 아닌 것처럼 보일 수 있습니다. 어쨌든 타입 검사기는 코드가 현재 상태 그대로 동작할지를 검사하는 데 관심이 있을 뿐, 향후 있을 수 있는 변경에는 관심이 없습니다. 그러나 타입 검사기가 타입 오류를 찾기 위해 코드에 수행하는 분석은 많은 폐지 예정 사용을 감지하는 데 필요한 분석과 매우 비슷합니다. 따라서 타입 검사기는 폐지 예정을 찾아 보고하기에 적합합니다.
다른 언어에는 이미 비슷한 기능이 있습니다:
- GCC는 함수 선언에
deprecated속성을 지원합니다. CPython의Py_DEPRECATED매크로가 이것을 기반으로 동작합니다. - GraphQL은 필드를
@deprecated로 표시하는 것을 지원합니다. - Kotlin은
Deprecated어노테이션을 지원합니다. - Scala는
@deprecated어노테이션을 지원합니다. - Swift는
@available속성을 사용하여 API를 폐지된 것으로 표시하는 기능을 지원합니다. - TypeScript는
@deprecatedJSDoc 태그를 사용하여 폐기 예정 기능이 사용된 곳을 표시하는 힌트를 제공합니다.
여러 사용자가 이러한 기능의 지원을 요청했습니다.
이미 존재하는 비슷한 서드파티 도구는 다음과 같습니다.
- Deprecated는 클래스, 함수 또는 메서드를 폐기 예정으로 표시하는 데코레이터를 제공합니다. 데코레이트된 객체에 접근하면 런타임 경고가 발생하지만, 타입 검사기는 이를 감지하지 못합니다.
- flake8-deprecated는 폐기 예정 기능의 사용에 대해 경고하는 린터 플러그인입니다. 그러나 이는 짧은 하드코딩된 폐지 목록으로 제한됩니다.
명세
새로운 데코레이터 @deprecated()가 warnings 모듈에 추가됩니다. 이 데코레이터는 클래스, 함수 또는 메서드에 사용하여 해당 대상을 폐기 예정으로 표시할 수 있습니다. 여기에는 typing.TypedDict 및 typing.NamedTuple 정의도 포함됩니다. 오버로드된 함수에서는 데코레이터를 개별 오버로드에 적용하여 해당 오버로드가 폐기 예정임을 나타낼 수 있습니다. 데코레이터를 오버로드 구현 함수에 적용하여 함수 전체가 폐기 예정임을 나타낼 수도 있습니다.
데코레이터는 다음 인자를 받습니다.
- 폐지 메시지를 나타내는 필수 위치 전용 인자입니다.
- 런타임 동작을 제어하는 두 개의 키워드 전용 인자
category및stacklevel(아래 “런타임 동작” 참조)
위치 전용 인자는 str 타입이며, 타입 검사기가 데코레이트된 객체의 사용을 발견했을 때 보여 주어야 하는 메시지를 담습니다. 도구는 표시를 위해 폐지 메시지를 정리할 수 있습니다. 예를 들어 inspect.cleandoc() 또는 이와 동등한 로직을 사용할 수 있습니다. 메시지는 반드시 문자열 리터럴이어야 합니다. 폐지 메시지의 내용은 사용자가 정하지만, 폐지된 객체가 제거될 버전과 권장되는 대체 API에 관한 정보를 포함할 수 있습니다.
타입 검사기는 폐기 예정으로 표시된 객체의 사용을 발견할 때마다 진단을 생성해야 합니다. 폐기 예정 오버로드의 경우, 여기에는 폐기 예정 오버로드로 해석되는 모든 호출이 포함됩니다. 폐기 예정 클래스와 함수의 경우, 여기에는 다음이 포함됩니다.
- 모듈, 클래스 또는 인스턴스 속성을 통한 참조(
module.deprecated_object,module.SomeClass.deprecated_method,module.SomeClass().deprecated_method) - 폐기 예정 객체를 정의한 모듈 안에서의 모든 사용 (
module.py안의x = deprecated_object()) import *가 사용된 경우, 해당 모듈에서 가져온 폐기 예정 객체의 사용 (from module import *; x = deprecated_object())from임포트 (from module import deprecated_object)- 함수 호출을 간접적으로 일으키는 모든 문법입니다. 예를 들어, 클래스
C의__add__메서드가 폐기 예정(deprecated)이라면,C() + C()코드는 진단을 발생시켜야 합니다. 마찬가지로, 프로퍼티의 setter가 폐기 예정으로 표시되어 있다면, 해당 프로퍼티를 설정하려는 시도는 진단을 발생시켜야 합니다.
메서드가 PEP 698의 typing.override() 데코레이터로 표시되어 있고 그 메서드가 오버라이드하는 베이스 클래스 메서드가 폐기 예정이라면, 타입 검사기는 진단을 생성해야 합니다.
폐지가 관련될 수 있는 추가 시나리오가 있습니다. 예를 들어, 어떤 객체가 typing.Protocol을 구현하는데, 프로토콜 준수에 필요한 메서드 중 하나가 폐기 예정일 수 있습니다. 이와 같은 시나리오는 복잡해 보이고 실제로 발생할 가능성도 비교적 낮으므로, 이 PEP는 타입 검사기가 이를 감지하도록 요구하지 않습니다.
예시
예를 들어, library.pyi라는 이름의 다음 라이브러리 스텁을 생각해 봅시다:
from warnings import deprecated
@deprecated("Use Spam instead")
class Ham: ...
@deprecated("It is pining for the fiords")
def norwegian_blue(x: int) -> int: ...
@overload
@deprecated("Only str will be allowed")
def foo(x: int) -> str: ...
@overload
def foo(x: str) -> str: ...
class Spam:
@deprecated("There is enough spam in the world")
def __add__(self, other: object) -> object: ...
@property
@deprecated("All spam will be equally greasy")
def greasy(self) -> float: ...
@property
def shape(self) -> str: ...
@shape.setter
@deprecated("Shapes are becoming immutable")
def shape(self, value: str) -> None: ...
타입 검사기는 이 라이브러리의 사용을 다음과 같이 처리해야 합니다:
from library import Ham # error: Use of deprecated class Ham. Use Spam instead.
import library
library.norwegian_blue(1) # error: Use of deprecated function norwegian_blue. It is pining for the fiords.
map(library.norwegian_blue, [1, 2, 3]) # error: Use of deprecated function norwegian_blue. It is pining for the fiords.
library.foo(1) # error: Use of deprecated overload for foo. Only str will be allowed.
library.foo("x") # no error
ham = Ham() # no error (already reported above)
spam = library.Spam()
spam + 1 # error: Use of deprecated method Spam.__add__. There is enough spam in the world.
spam.greasy # error: Use of deprecated property Spam.greasy. All spam will be equally greasy.
spam.shape # no error
spam.shape = "cube" # error: Use of deprecated property setter Spam.shape. Shapes are becoming immutable.
진단 메시지의 정확한 문구는 타입 검사기가 정하며 명세의 일부가 아닙니다.
런타임 동작
위치 전용 인자인 message 외에도, @deprecated 데코레이터는 두 개의 키워드 전용 인자를 받습니다:
category: 경고 클래스입니다. 기본값은DeprecationWarning입니다. 이것이None으로 설정되면 런타임에 경고가 발생하지 않으며, 데코레이터는__deprecated__속성을 설정하는 것을 제외하고는 원래 객체를 그대로 반환합니다(아래 참조).stacklevel: 경고를 발생시킬 때 건너뛸 스택 프레임의 수입니다. 기본값은 1이며, 폐기 예정 객체가 호출되는 위치에서 경고가 발생해야 함을 나타냅니다. 내부적으로, 구현은 래퍼 코드에서 사용하는 스택 프레임의 수를 더합니다.
데코레이트된 객체가 클래스라면, 데코레이터는 클래스를 인스턴스화할 때 경고가 발생하도록 __new__ 메서드를 감쌉니다. 데코레이트된 객체가 호출 가능 객체라면, 데코레이터는 원래의 호출 가능 객체를 감싸되 호출될 때 경고를 발생시키는 새로운 호출 가능 객체를 반환합니다. 그 밖의 경우에는 데코레이터가 TypeError를 발생시킵니다(category=None을 전달한 경우는 제외).
오버로드, Protocol 클래스, 추상 메서드를 비롯하여, 데코레이트된 객체를 사용할 때 경고를 발생시킬 수 없는 시나리오가 몇 가지 있습니다. 이러한 경우에 category=None 없이 @deprecated를 사용하면 타입 검사기가 경고를 표시할 수 있습니다.
런타임 인트로스펙션을 지원하기 위해 데코레이터는 전달받은 객체뿐 아니라 폐지된 클래스와 함수에 대해 생성하는 래퍼 호출 가능 객체에도 __deprecated__ 속성을 설정합니다. 이 속성의 값은 데코레이터에 전달된 메시지입니다. 이 속성을 설정할 수 없는 객체를 데코레이트하는 것은 지원되지 않습니다.
@runtime_checkable 데코레이터가 적용된 Protocol이 폐지된 것으로 표시된 경우, __deprecated__ 속성은 프로토콜의 멤버로 간주되어서는 안 되며, 따라서 이 속성의 존재가 isinstance 검사에 영향을 주어서는 안 됩니다.
typing.get_overloads()와의 호환성을 위해, @deprecated 데코레이터는 @overload 데코레이터 뒤에 배치해야 합니다.
타입 검사기 동작
이 PEP는 타입 검사기가 폐지 진단을 사용자에게 정확히 어떻게 보여 주어야 하는지 명시하지 않습니다. 그러나 일부 사용자(예: 특정 Python 버전만을 대상으로 하는 애플리케이션 개발자)는 폐지에 관심이 없을 수 있는 반면, 다른 사용자(예: 자신의 라이브러리가 향후 Python 버전과도 호환되기를 원하는 라이브러리 개발자)는 CI 파이프라인에서 폐지된 기능의 사용을 모두 잡아내기를 원할 것입니다. 따라서 타입 검사기는 두 사용 사례를 모두 지원하는 설정 옵션을 제공하는 것을 권장합니다. 다른 타입 검사기 오류와 마찬가지로 # type: ignore 주석을 사용하여 폐지를 무시할 수도 있습니다.
폐지 정책
CPython의 폐지 정책(PEP 387)을 갱신하여, 가능한 경우 새로운 폐지가 이 PEP의 기능을 사용해 사용자에게 폐지 사실을 알리도록 요구할 것을 제안합니다. 구체적으로 이는 새로운 폐지가 있을 때 적절한 위치에 @deprecated 데코레이터를 추가하는 typeshed 저장소 변경이 함께 이루어져야 한다는 의미입니다. 이 요구 사항은 이 PEP의 기능으로 표현할 수 없는 폐지에는 적용되지 않습니다.
하위 호환성
새로운 데코레이터를 만드는 것은 하위 호환성 문제를 일으키지 않습니다. 다른 모든 새로운 타이핑 기능과 마찬가지로, @deprecated 데코레이터는 typing_extensions 모듈에 추가되어 이전 버전의 Python에서도 사용할 수 있게 됩니다.
교육 방법
IDE나 타입 검사기 출력에서 폐지 경고를 접하는 사용자에게는 명확하고 그 자체로 이해 가능한 메시지가 제공되어야 합니다. @deprecated 데코레이터의 사용은 주로 라이브러리 작성자와 관련된 고급 기능이 될 것입니다. 이 데코레이터는 관련 문서(예: PEP 387 및 DeprecationWarning 문서)에서 폐기된 기능을 표시하는 추가적인 방법으로 언급되어야 합니다.
참조 구현
@deprecated 데코레이터의 런타임 구현은 버전 4.5.0부터 typing-extensions 라이브러리에서 사용할 수 있습니다. Pyright와 마찬가지로 pyanalyze 타입 검사기도 폐지 오류를 내보내는 프로토타입 지원을 제공합니다.
기각된 아이디어
모듈과 속성의 폐지
이 PEP는 클래스, 함수, 오버로드의 폐지를 다룹니다. 이를 통해 타입 검사기는 가능한 모든 폐지는 아니더라도 상당수를 감지할 수 있습니다. 추가 기능이 가치가 있을지 평가하기 위해, 저는 CPython 표준 라이브러리의 현재 모든 폐지를 검토했습니다.
그 결과는 다음과 같습니다:
- 함수, 메서드, 클래스의 폐지 74건(이 PEP에서 지원)
- 모듈 전체의 폐지 28건(주로 PEP 594 때문)
- 함수 매개변수의 폐지 9건(오버로드를 데코레이트하는 방식으로 이 PEP에서 지원)
- 상수의 폐지 1건
- 타입 시스템에서 쉽게 감지할 수 없는 폐지 38건(예: 활성 이벤트 루프 없이
asyncio.get_event_loop()을 호출하는 경우)
모듈 수준 상수 __deprecated__를 추가하여 모듈을 사용 중단된 것으로 표시할 수도 있습니다. 그러나 이러한 기능이 필요한 경우는 제한적이며, 사용 중단된 모듈이 사용되는 곳은 grep만으로도 비교적 쉽게 찾아낼 수 있습니다. 따라서 이 PEP는 모듈 전체의 폐지에 대한 지원을 생략합니다. 대안으로, 사용자는 모듈 수준의 모든 클래스와 함수를 @deprecated로 표시할 수 있습니다.
모듈 수준 상수, 객체 속성, 함수 매개변수를 사용 중단하기 위해 Annotated와 유사한 Deprecated[type, message] 타입 수정자를 추가할 수도 있습니다. 그러나 이렇게 하면 타입 시스템 안에 문자열이 전방 참조가 아닌 단순한 문자열로 취급되는 새로운 영역이 생겨 타입 검사기 구현이 복잡해집니다. 또한 제 데이터에 따르면 이 기능이 필요한 경우는 흔하지 않습니다.
더 많은 종류의 객체를 사용 중단하는 기능은 향후 PEP에서 추가될 수 있습니다.
데코레이터를 typing 모듈에 두는 방안
이 PEP의 이전 버전에서는 @deprecated 데코레이터를 typing 모듈에 두는 방안을 제안했습니다. 그러나 typing 모듈의 데코레이터가 런타임 동작을 가지는 것은 예상 밖이라는 피드백이 있었습니다. 따라서 이제 이 PEP는 데코레이터를 대신 warnings 모듈에 추가할 것을 제안합니다.
감사의 글
typing-sig 밋업 그룹과의 통화를 통해 이 제안에 대한 유용한 피드백을 얻었습니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.