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

Python 개선 제안 한국어 번역

PEP 591 – typing에 final 한정자 추가하기

Author:
Michael J. Sullivan <sully at msully.net>, Ivan Levkivskyi <levkivskyi at gmail.com>
BDFL-Delegate:
Guido van Rossum <guido at python.org>
Discussions-To:
Typing-SIG list
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
15-Mar-2019
Python-Version:
3.8
Post-History:

Resolution:
Typing-SIG message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document: see @final/@typing.final 및 Final/typing.Final for up-to-date specs and documentation. Canonical typing specs are maintained at the typing specs site; runtime typing behaviour is described in the CPython documentation.

×

See the typing specification update process for how to propose changes to the typing spec.

초록

이 PEP는 typing 모듈에 “final” 한정자를 final 데코레이터와 Final 타입 어노테이션의 형태로 추가하여, 서로 관련된 다음 세 가지 목적을 달성할 것을 제안합니다.

  • 메서드를 오버라이드해서는 안 된다고 선언
  • 클래스를 서브클래싱해서는 안 된다고 선언
  • 변수나 속성이 재대입되어서는 안 된다고 선언하는 것

동기

final 데코레이터

현재 typing 모듈에는 타입 검사기 수준에서 상속이나 오버라이드의 사용을 제한할 방법이 없습니다. 이는 다른 객체 지향 언어(예: Java)에서 흔한 기능이며, 클래스가 가질 수 있는 동작의 범위를 줄여 추론을 쉽게 하는 데 유용합니다.

final 클래스나 메서드가 유용할 수 있는 상황은 다음과 같습니다.

  • 클래스가 서브클래싱되도록 설계되지 않았거나 메서드가 오버라이드되도록 설계되지 않은 경우입니다. 예상대로 동작하지 않거나 오류가 발생하기 쉬울 수 있습니다.
  • 서브클래싱이나 오버라이드로 인해 코드를 이해하거나 유지 보수하기가 더 어려워지는 경우입니다. 예를 들어, 베이스 클래스와 서브클래스 사이에 불필요하게 강한 결합이 생기는 것을 막고 싶을 수 있습니다.
  • 향후 클래스 구현을 임의로 변경할 자유를 유지하고 싶은데, 이러한 변경이 서브클래스를 망가뜨릴 수 있는 경우입니다.

Final 어노테이션

현재 typing 모듈에는 변수에 값이 할당되지 않을 것임을 나타낼 방법이 없습니다. 이는 다음과 같은 여러 상황에서 유용한 기능입니다.

  • 모듈 수준 및 클래스 수준 상수가 의도치 않게 수정되는 것을 방지하고, 이를 검사 가능한 방식으로 상수라고 문서화합니다.
  • 서브클래스에서 오버라이드할 수 없는 읽기 전용 속성을 만드는 것. (@property로 속성을 읽기 전용으로 만들 수는 있지만 오버라이드를 막지는 못합니다)
  • 일반적으로 리터럴이 기대되는 상황에서 이름을 사용할 수 있게 합니다(예를 들어 NamedTuple의 필드 이름, isinstance에 전달되는 타입의 튜플, 또는 Literal 타입(PEP 586)의 인자를 받는 함수에 전달하는 인자로 사용할 수 있습니다).

명세

final 데코레이터

typing.final 데코레이터는 상속과 오버라이드의 사용을 제한하는 데 사용됩니다.

타입 검사기는 @final로 데코레이트된 클래스가 서브클래싱되는 것과 @final로 데코레이트된 메서드가 서브클래스에서 오버라이드되는 것을 금지해야 합니다. 메서드 데코레이터 버전은 인스턴스 메서드, 클래스 메서드, 정적 메서드, 프로퍼티 모두에 사용할 수 있습니다.

예를 들면 다음과 같습니다.:

from typing import final

@final
class Base:
    ...

class Derived(Base):  # Error: Cannot inherit from final class "Base"
    ...

그리고:

from typing import final

class Base:
    @final
    def foo(self) -> None:
        ...

class Derived(Base):
    def foo(self) -> None:  # Error: Cannot override final attribute "foo"
                            # (previously declared in base class "Base")
        ...

오버로드된 메서드의 경우 @final을 구현에 붙여야 합니다(스텁에서는 첫 번째 오버로드에 붙입니다).:

from typing import Any, overload

class Base:
    @overload
    def method(self) -> None: ...
    @overload
    def method(self, arg: int) -> int: ...
    @final
    def method(self, x=None):
        ...

메서드가 아닌 함수에 @final을 사용하는 것은 오류입니다.

Final 어노테이션

typing.Final 타입 한정자는 변수나 속성을 다시 할당하거나 재정의하거나 오버라이드해서는 안 된다는 것을 나타내는 데 사용합니다.

문법

Final은 다음 형태 중 하나로 사용할 수 있습니다:

  • Final[<type>] 문법을 사용하여 명시적 타입과 함께 사용합니다. 예시:
    ID: Final[float] = 1
    
  • 타입 어노테이션 없이 사용합니다. 예시:
    ID: Final = 1
    

    타입 검사기는 일반적인 타입 추론 메커니즘을 적용하여 ID의 타입(여기서는 아마도 int)을 결정해야 합니다. 제네릭 클래스의 경우와 달리, 이는 Final[Any]와 같지 않다는 점에 유의하십시오.

  • 클래스 본문과 스텁 파일에서는 우변을 생략하고 ID: Final[float]만 작성할 수 있습니다. 우변을 생략하는 경우에는 Final에 명시적인 타입 인자가 있어야 합니다.
  • 마지막으로, self.id: Final = 1처럼 사용할 수 있습니다(이때도 대괄호 안에 타입을 선택적으로 지정할 수 있습니다). 이 형태는 __init__ 메서드 안에서만 허용되므로, final 인스턴스 속성은 인스턴스가 생성될 때 한 번만 할당됩니다.

의미와 예시

final 이름을 정의하는 두 가지 주요 규칙은 다음과 같습니다:

  • 주어진 속성에 대한 final 선언은 모듈 또는 클래스마다 최대 하나만 있을 수 있습니다. 같은 이름으로 클래스 수준 상수와 인스턴스 수준 상수를 따로 둘 수 없습니다.
  • final 이름에 대한 할당은 정확히 한 번이어야 합니다.

즉, 타입 검사기는 타입 검사 대상 코드에서 final 이름에 대한 추가 할당을 막아야 합니다:

from typing import Final

RATE: Final = 3000

class Base:
    DEFAULT_ID: Final = 0

RATE = 300  # Error: can't assign to final attribute
Base.DEFAULT_ID = 1  # Error: can't override a final attribute

런타임은 이후 반복에서 같은 변수에 대한 여러 번의 할당을 보게 되므로, 타입 검사기가 루프 안의 Final 선언을 허용할 필요는 없다는 점에 유의하십시오.

또한 타입 검사기는 서브클래스에서 final 속성을 오버라이드하지 못하도록 막아야 합니다:

from typing import Final

class Window:
    BORDER_WIDTH: Final = 2.5
    ...

class ListView(Window):
    BORDER_WIDTH = 3  # Error: can't override a final attribute

클래스 본문에서 초기값 없이 선언한 final 속성은 __init__ 메서드에서 초기화해야 합니다(스텁 파일은 예외입니다):

class ImmutablePoint:
    x: Final[int]
    y: Final[int]  # Error: final attribute without an initializer

    def __init__(self) -> None:
        self.x = 1  # Good

타입 검사기는 클래스 본문에서 초기화되는 final 속성을 클래스 변수로 추론해야 합니다. 변수에 ClassVar와 Final을 함께 어노테이션해서는 안 됩니다.

Final은 할당문이나 변수 어노테이션에서 가장 바깥쪽 타입으로만 사용할 수 있습니다. 그 밖의 위치에서 사용하면 오류입니다. 특히 Final은 함수 인자의 어노테이션에 사용할 수 없습니다:

x: List[Final[int]] = []  # Error!

def fun(x: Final[List[int]]) ->  None:  # Error!
    ...

이름을 final로 선언해도 그 이름이 다른 값에 다시 바인딩되지 않는다는 것만 보장할 뿐, 값이 불변이 되지는 않는다는 점에 유의하십시오. 이러한 값이 변경되지 않도록 하려면 불변 ABC와 컨테이너를 Final과 함께 사용할 수 있습니다:

x: Final = ['a', 'b']
x.append('c')  # OK

y: Final[Sequence[str]] = ['a', 'b']
y.append('x')  # Error: "Sequence[str]" has no attribute "append"
z: Final = ('a', 'b')  # Also works

타입 검사기는 리터럴로 초기화한 final 이름을 사용하는 곳을 그 리터럴로 바꾼 것처럼 취급해야 합니다. 예를 들어, 다음 코드는 허용되어야 합니다:

from typing import NamedTuple, Final

X: Final = "x"
Y: Final = "y"
N = NamedTuple("N", [(X, int), (Y, int)])

참조 구현

mypy [1] 타입 검사기는 Final과 final을 지원합니다. 런타임 구성 요소의 참조 구현은 typing_extensions [2] 모듈에서 제공됩니다.

거부되었거나 보류된 아이디어

Final 타입 어노테이션의 이름으로 Const라는 이름도 고려했습니다. 두 개념이 서로 관련되어 있으므로 둘 사이에 일관성을 유지하는 것이 가장 좋다고 보았기 때문에, 대신 Final이라는 이름을 선택했습니다.

final까지 도입하는 대신 Final이라는 단일 이름만 사용하는 방안도 고려했지만, @Final은 저희가 보기에 너무 어색했습니다.

final 클래스와 관련된 기능으로는 Scala 스타일의 봉인된(sealed) 클래스를 들 수 있습니다. 봉인된 클래스는 같은 모듈에 정의된 클래스만 상속할 수 있습니다. 봉인된 클래스는 패턴 매칭과 함께 사용할 때 가장 유용해 보이므로, 저희의 경우에는 그로 인해 늘어나는 복잡성을 정당화하기 어려워 보입니다. 이 부분은 향후 다시 검토될 수 있습니다.

클래스에 붙은 @final 데코레이터가 런타임에 동적으로 서브클래싱을 막도록 만드는 것도 가능합니다. 하지만 typing의 다른 어떤 요소도 런타임 강제를 수행하지 않으므로, final도 이를 수행하지 않습니다. 런타임 강제와 정적 검사가 모두 필요한 경우에는 우회 방법으로 다음 관용구를 사용하면 됩니다(필요하다면 별도의 지원 모듈에 두어 사용할 수 있습니다).:

if typing.TYPE_CHECKING:
    from typing import final
else:
    from runtime_final import final

참고 문헌