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

Python 개선 제안 한국어 번역

PEP 800 – 타입 시스템의 분리 베이스

Author:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
21-Jul-2025
Python-Version:
3.15
Post-History:
18-Jul-2025, 23-Jul-2025
Resolution:
15-Apr-2026

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document: see @disjoint_base and @typing.disjoint_base 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.

초록

Python 프로그램을 정확하게 분석하려면 타입 검사기는 두 클래스가 공통 자식 클래스를 가질 수 있는지 여부를 알아야 합니다. 그러나 이를 판단하는 데 필요한 정보는 현재 타입 시스템의 일부가 아닙니다. 이 PEP는 클래스가 “분리 베이스”임을 나타내는 새로운 데코레이터인 @typing.disjoint_base를 추가합니다. 서로 다르고 관련이 없는 분리 베이스를 가진 두 클래스는 공통 자식 클래스를 가질 수 없습니다.

이 데코레이터는 대부분의 사용자가 직접 사용할 것으로 예상되지 않습니다. 이는 주로 표준 라이브러리 및 확장 모듈 클래스의 스텁 파일에서 사용하도록 설계되었으며, 타입 검사기가 런타임 제한을 일관되게 반영하는 데 도움이 됩니다.

동기

Python의 타입 검사에서 중요한 개념 중 하나는 도달 가능성입니다. Python 타입 검사기는 일반적으로 코드의 어떤 분기가 절대 실행될 수 없는 경우를 감지하고, 그러한 코드에 대해 사용자에게 경고합니다. 도달할 수 없는 코드는 프로그램을 불필요하게 복잡하게 만들며, 그 존재가 버그의 징후일 수 있으므로 이는 유용합니다.

예를 들어, 다음 프로그램에서는:

def f(x: bool) -> None:
    if isinstance(x, str):
        print("It's both!")

두 가지 인기 있는 타입 검사기인 pyright와 mypy(--warn-unreachable 사용)는 xbool이면 동시에 str일 수 없으므로 if 블록의 본문에 도달할 수 없다고 경고합니다.

다중 상속이 존재하기 때문에 Python에서 도달 가능성을 판단하는 일은 복잡합니다. boolstr 대신 사용자가 정의한 두 클래스를 사용하면 mypy와 pyright는 아무런 경고도 표시하지 않습니다.:

class A: pass
class B: pass

def f(x: A):
    if isinstance(x, B):
        print("It's both!")

두 클래스 AB를 모두 상속하는 클래스가 존재할 수 있으므로 이는 올바른 동작입니다.

intstr을 사용하는 또 다른 경우에는 타입 검사기 간의 차이를 확인할 수 있습니다.:

def f(x: int):
    if isinstance(x, str):
        print("It's both!")

이 코드에서 pyright는 오류를 표시하지 않지만 mypy는 해당 분기에 도달할 수 없다고 주장합니다. 여기서 mypy가 기술적으로 올바릅니다. CPython은 클래스가 intstr을 모두 상속하는 것을 허용하지 않으므로 해당 분기에 도달할 수 없습니다. 그러나 이러한 베이스 클래스가 호환되지 않는다는 것을 판단하는 데 필요한 정보는 현재 타입 시스템에서 사용할 수 없습니다. 실제로 mypy는 호환되지 않는 메서드의 존재를 기반으로 한 휴리스틱을 사용합니다. 이 휴리스틱은 실제로, 특히 내장 타입에서 상당히 잘 작동하지만, 일반적으로는 올바르지 않으며 아래에서 더 자세히 설명합니다.

실험적인 ty 타입 검사기는 Python의 런타임 동작 에 더 밀접하게 부합하는 세 번째 접근 방식을 사용합니다. 즉, 다중 상속을 제한하는 특정 클래스를 “견고한 베이스”로 인식합니다. 대략적으로 말하면 모든 클래스는 고유한 견고한 베이스를 최대 하나만 상속해야 하며, 고유한 견고한 베이스가 없으면 해당 클래스는 존재할 수 없습니다. 이에 대한 더 정확한 정의는 아래에서 제공합니다. 그러나 현재 ty의 접근 방식은 특정 내장 타입에 대한 하드코딩된 지식에 의존합니다. “견고한 베이스”라는 용어는 CPython 구현에서 유래했으며, 이 PEP에서는 대신 새롭게 제안하는 “분리 베이스”라는 용어를 사용합니다.

이 PEP는 런타임에서 다중 상속이 허용되지 않는 경우를 표현할 수 있도록 타입 시스템을 확장하는 방안을 제안합니다. 즉, 클래스를 분리 베이스로 표시하는 @disjoint_base 데코레이터를 추가합니다. 이를 통해 타입 검사기는 도달 가능성을 더 정확하게 이해할 수 있으며, 여러 구체적인 영역에도 도움이 됩니다.

잘못된 클래스 정의

intstr이 서로 다른 분리 베이스이므로 다음 클래스 정의는 런타임에서 오류를 발생시킵니다.:

class C(int, str): pass

분리 베이스에 대한 지식이 없으면 타입 검사기는 현재 이 클래스 정의가 잘못된 이유를 감지할 수 없습니다. 다만 이 클래스가 존재한다고 가정할 경우 일부 메서드가 호환되지 않는다는 점은 감지할 수 있습니다. (이 클래스 정의를 보면 mypy는 __add__와 여러 다른 메서드의 호환되지 않는 정의를 지적합니다.)

그 자체로는 특별히 설득력 있는 문제는 아닙니다. 코드를 가져오는 첫 시점에 오류가 대개 발견되기 때문이지만, 완전성을 위해 여기에서 언급합니다.

도달 가능성

isinstance()를 사용하는 코드의 도달 가능성은 이미 언급했습니다. match 문과 같은 다른 타입 축소 구문에서도 비슷한 문제가 발생합니다. 도달 가능성을 올바르게 추론하려면 서로소 베이스를 이해해야 합니다.

class A: pass
class B: pass

def f(x: A):
    match x:
        case B():  # reachable
            print("It's both!")

def g(x: int):
    match x:
        case str():  # unreachable
            print("It's both!")

오버로드

일부 오버로드의 매개변수 타입은 겹치지만 반환 타입은 겹치지 않는 경우, @overload로 데코레이터된 함수는 안전하지 않을 수 있습니다. 예를 들어 다음 오버로드 집합은 sound하지 않은 동작을 달성하는 데 악용될 수 있습니다:

from typing import overload

class A: pass
class B: pass

@overload
def f(x: A) -> str: ...
@overload
def f(x: B) -> int: ...

AB를 모두 상속하는 클래스가 존재한다면, 타입 검사기는 f()를 호출할 때 잘못된 오버로드를 선택할 수 있습니다.

타입 검사기는 이러한 안전하지 않은 동작의 원인을 감지하여 경고할 수 있지만, 올바른 구현을 위해서는 서로소 베이스를 이해해야 합니다. 이는 AB의 인스턴스인 값이 존재할 수 있는지를 알아야 하기 때문입니다. 많은 타입 검사기가 이미 겹치는 오버로드에 대해 이 검사의 한 형태를 수행하고 있지만, 타이핑 명세는 현재 이 검사가 어떻게 작동해야 하는지 규정하지 않습니다. 이 PEP는 이를 변경하자고 제안하지 않지만, 겹치는 오버로드를 건전하게 검사하기 위한 구성 요소를 제공하는 데 도움이 됩니다.

교집합 타입

주어진 모든 타입의 인스턴스인 값을 포함하는 타입을 나타내는 명시적 교집합 타입은 현재 타입 시스템의 일부가 아닙니다. 그러나 이러한 타입은 타입 축소의 결과로 Python과 같은 집합론적 타입 시스템에서 자연스럽게 나타나며, 향후 타입 시스템 확장에서는 명시적 교집합 타입을 지원할 수 있습니다.

교집합 타입을 사용할 때는 특정 교집합이 원소를 가지는지, 즉 해당 교집합의 멤버가 될 수 있는 값이 존재하는지를 아는 것이 중요한 경우가 많습니다. 이를 통해 타입 검사기는 도달 가능성을 이해하고 사용자에게 더 정확한 타입 정보를 제공할 수 있습니다.

구체적인 예로, 교집합 타입을 사용한 할당 가능성의 구현은 다음과 같을 수 있습니다. 교집합 타입 A & B가 주어졌을 때, 타입 CAB중 적어도 하나에 할당 가능하고 AB모두와 겹친다면 C를 해당 교집합 타입에 할당할 수 있습니다. (“겹친다”는 여기서 두 타입 모두의 멤버가 될 수 있는 런타임 값이 적어도 하나 존재할 수 있다는 뜻입니다. 즉, A & B가 원소를 가지면 AB는 겹칩니다.) 이 규칙의 두 번째 부분은 strint & Any와 같은 타입에 할당되지 않도록 보장합니다. strAny에 할당 가능하지만 int와는 겹치지 않기 때문입니다. 물론 두 클래스가 모두 서로소 베이스라는 사실을 알고 있을 때만 strint가 겹치지 않는다는 것을 알 수 있습니다.

개요

서로소 베이스는 타입 시스템의 여러 부분에서 유용할 수 있습니다. 이러한 부분 중 일부는 충분히 명세화되지 않았거나 추측에 불과하거나 중요성이 낮지만, 각각의 경우에 서로소 베이스라는 개념을 통해 타입 검사기는 현재 타입 시스템이 허용하는 것보다 더 정확하게 이해할 수 있습니다. 따라서 서로소 베이스는 Python 타입 시스템을 개선하기 위한 확고한 기반(말 그대로 견고한 베이스)을 제공합니다.

근거

“서로소 베이스”라는 개념을 통해 타입 검사기는 두 클래스의 공통 자식 클래스가 존재할 수 있는 경우와 존재할 수 없는 경우를 이해할 수 있습니다. 이 개념을 타입 검사기에 전달하기 위해, 클래스가 서로소 베이스임을 표시하는 @disjoint_base데코레이터를 타입 시스템에 추가합니다. 의미론은 대략적으로 클래스가 서로 관련 없는 두 서로소 베이스를 가질 수 없다는 것입니다.

명명

이 PEP의 초기 버전에서는 CPython 구현에서 사용되는 용어를 따라 “solid base”라는 이름을 사용했습니다. 그러나 이 용어는 다소 모호합니다. 대안인 “disjoint base”라는 용어는 이 데코레이터가 적용된 클래스가 다른 베이스와 서로소임을 시사하며, 이는 이 개념을 일차적으로 설명하기에 적절합니다. (정확한 의미론은 더 미묘하며 아래에서 설명합니다.)

다중 상속에 대한 런타임 제한

Python은 일반적으로 다중 상속을 허용하지만, 런타임은 CPython에 문서화된 대로 다양한 제한을 부과합니다. 일관된 MRO와 일관된 메타클래스에 관한 두 가지 제한 집합은 타입 시스템에서 사용할 수 있는 정보를 사용하여 타입 검사기가 이미 구현할 수 있습니다. 인스턴스 레이아웃에 관한 세 번째 제한에는 분리된 베이스에 관한 지식이 필요합니다. 비어 있지 않은 __slots__ 정의를 포함하는 클래스는 자동으로 분리된 베이스가 되며, C로 구현된 많은 내장 클래스도 마찬가지입니다.

PyPy와 같은 Python의 대체 구현은 대체로 CPython과 유사하게 동작하지만, 정확히 어떤 표준 라이브러리 클래스가 분리된 베이스인지와 같은 세부 사항에서는 다를 수 있습니다. 타입 시스템에는 현재 대체 Python 구현에 대한 명시적인 지원이 없으므로, 이 PEP는 typeshed와 같은 스텁 라이브러리가 @disjoint_base 데코레이터를 사용할 시점을 결정할 때 CPython의 동작을 사용하도록 권장합니다. 향후 타입 시스템 확장에서 대체 구현에 대한 지원이 추가된다면(예를 들어 sys.implementation.name의 값에 따라 분기한다면), 스텁은 필요한 경우 구현에 따라 @disjoint_base 데코레이터의 존재 여부를 조건부로 지정할 수 있습니다.

CPython 구현에서 “분리된 베이스”( “solid bases”라고도 함)라는 개념은 수십 년 동안 존재해 왔지만, 어떤 클래스가 분리된 베이스인지 결정하는 규칙은 때때로 변경되었습니다. Python 3.12 이전에는 베이스 클래스에 비해 __dict__를 추가하거나 weakref를 지원하면 클래스를 분리된 베이스로 만들 수 있었습니다. 실제로 이는 namedtuple() 클래스와 같이 C로 구현된 클래스를 상속하는 Python 구현 클래스가 스스로 분리된 베이스가 되는 경우가 많다는 것을 의미했습니다. 이 동작은 Python 3.12에서 python/cpython#96028에 의해 변경되었습니다. 이 PEP는 더 단순하고 이해하기 쉬운 Python 3.12 이상의 동작을 지원하는 데 중점을 둡니다. 타입 검사기는 원한다면 Python 3.12 이전 동작의 버전을 구현할 수 있지만, 이를 올바르게 수행하려면 현재 타입 시스템에서 사용할 수 없는 정보가 필요합니다.

런타임에서 분리된 베이스인 클래스의 정확한 집합은 향후 Python 버전에서 다시 변경될 수 있습니다. 이러한 일이 발생하면 타입 검사기가 사용하는 타입 스텁을 업데이트하여 이 새로운 현실을 반영할 수 있습니다. 다시 말해, 이 PEP는 타입 시스템에 분리된 베이스라는 개념을 추가하지만, 어떤 클래스가 분리된 베이스인지 정확히 규정하지는 않습니다.

구현 파일의 @disjoint_base

@disjoint_base 데코레이터의 가장 분명한 사용 사례는 표준 라이브러리와 같은 C 라이브러리의 스텁 파일에서 C로 구현된 분리된 베이스를 표시하는 것입니다.

그러나 구현 파일에서 분리된 베이스를 표시하는 사용 사례도 있으며, 이 경우에는 해당 데코레이터가 적용된 클래스와 표준 라이브러리 클래스 또는 @disjoint_base 데코레이터가 적용된 다른 사용자 클래스와의 또 다른 분리된 베이스를 상속하는 자식 클래스의 존재를 금지하게 됩니다. 예를 들어, 이는 사용자 클래스와 int 또는 str과 같은 표준 라이브러리 클래스를 모두 상속하는 클래스가 존재해야만 도달할 수 있는 코드를 타입 검사기가 표시하도록 할 수 있습니다. 이러한 코드는 기술적으로 가능할 수 있지만 실제로는 개연성이 낮습니다.

@disjoint_base
class BaseModel:
    # ... General logic for model classes
    pass

class Species(BaseModel):
    name: str
    # ... more fields

def process_species(species: Species):
    if isinstance(species, str):  # oops, forgot `.name`
        pass  # type checker should warn about this branch being unreachable
        # BaseModel and str are disjoint bases, so a class that inherits from both cannot exist

이는 서브클래싱을 제한하는 기존 @final 데코레이터와 원칙적으로 유사합니다. 스텁에서는 프로그래밍 방식으로 서브클래싱을 금지하는 클래스를 표시하는 데 사용되지만, 구현 파일에서는 런타임 강제 없이 클래스가 서브클래싱되도록 의도되지 않았음을 나타내는 데 사용되는 경우가 많습니다.

특수 클래스의 @disjoint_base

@disjoint_base 데코레이터는 주로 명목 클래스에 사용하도록 의도되었지만, 타입 시스템에는 문법적으로 클래스 정의를 사용하는 다른 구성 요소도 있으므로 이들에도 데코레이터를 허용해야 하는지, 허용한다면 그 의미가 무엇인지 고려해야 합니다.

Protocol정의의 경우, 가장 일관된 해석은 프로토콜을 구현할 수 있는 유일한 클래스가 해당 프로토콜을 명목 상속하는 클래스 또는 프로토콜을 구현하는 @final 클래스라는 것입니다. 다른 클래스에는 프로토콜이 아닌 분리된 베이스가 있거나 잠재적으로 있을 수 있습니다. 이는 복잡하고 유용하지 않으므로 Protocol정의에는 @disjoint_base를 허용하지 않습니다.

마찬가지로 TypedDict는 순전히 구조적 타입이므로 TypedDict정의에서 “분리된 베이스”라는 개념은 의미가 없습니다.

타입 시스템에서 일부 특별한 처리를 받기는 하지만, NamedTuple정의는 자식 클래스를 가질 수 있는 실제 명목 클래스를 생성하므로, 해당 정의에 @disjoint_base를 허용하고 분리된 베이스 메커니즘의 목적상 일반 클래스처럼 취급하는 것이 타당합니다. 모든 NamedTuple클래스는 MRO에 분리된 베이스인 tuple을 가지므로 다른 분리된 베이스와 다중 상속을 사용할 수 없습니다.

사양

@typing.disjoint_base 데코레이터가 타입 시스템에 추가됩니다. 이 데코레이터는 NamedTuple정의를 포함한 명목 클래스에서만 사용할 수 있으며, 함수, TypedDict정의 또는 Protocol정의에 데코레이터를 사용하면 타입 검사기 오류입니다.

(명목) 클래스에 두 가지 속성을 정의합니다. 클래스는 분리 베이스일 수도 있고 아닐 수도 있으며, 모든 클래스는 유효한 분리 베이스를 가져야 합니다.

@typing.disjoint_base로 데코레이션되었거나, 비어 있지 않은 __slots__ 정의를 포함하는 경우 해당 클래스는 분리 베이스입니다. 여기에는 @dataclass(slots=True) 데코레이터 때문에 __slots__를 가지는 클래스와, dataclass_transform 메커니즘을 사용하여 슬롯을 추가한 클래스가 포함됩니다. 보편적 베이스 클래스인 object도 분리 베이스입니다.

클래스의 분리 베이스를 결정하기 위해 모든 베이스 클래스를 살펴보고 후보 분리 베이스 집합을 결정합니다. 각 베이스가 그 자체로 분리 베이스이면 후보는 해당 베이스 자체이고, 그렇지 않으면 후보는 해당 베이스의 분리 베이스입니다. 후보 집합에 단 하나의 분리 베이스가 포함되어 있으면 그것이 해당 클래스의 분리 베이스입니다. 후보가 여러 개이지만 그중 하나가 다른 모든 후보의 서브클래스이면 해당 클래스가 분리 베이스입니다. 그러한 후보가 존재하지 않으면 해당 클래스에는 유효한 분리 베이스가 없으며, 따라서 존재할 수 없습니다.

타입 검사기는 클래스 정의를 검사할 때 유효한 분리 베이스가 있는지 확인해야 하며, 유효한 분리 베이스가 없는 클래스 정의를 발견하면 진단을 발생시켜야 합니다. 타입 검사기는 타입이 서로소인지 판별하기 위해 분리 베이스 메커니즘을 사용할 수도 있습니다. 예를 들어 isinstance()같은 타입 축소 구문이 도달할 수 없는 분기를 만드는지 검사할 때 사용할 수 있습니다.

예시:

from typing import disjoint_base, assert_never

@disjoint_base
class Disjoint1:
    pass

@disjoint_base
class Disjoint2:
    pass

@disjoint_base
class DisjointChild(Disjoint1):
    pass

class C1:  # disjoint base is `object`
    pass

# OK: candidate disjoint bases are `Disjoint1` and `object`, and `Disjoint1` is a subclass of `object`.
class C2(Disjoint1, C1):  # disjoint base is `Disjoint1`
    pass

# OK: candidate disjoint bases are `DisjointChild` and `Disjoint1`, and `DisjointChild` is a subclass of `Disjoint1`.
class C3(DisjointChild, Disjoint1):  # disjoint base is `DisjointChild`
    pass

# error: candidate disjoint bases are `Disjoint1` and `Disjoint2`, but neither is a subclass of the other
class C4(Disjoint1, Disjoint2):
    pass

def narrower(obj: Disjoint1) -> None:
    if isinstance(obj, Disjoint2):
        assert_never(obj)  # OK: child class of `Disjoint1` and `Disjoint2` cannot exist
    if isinstance(obj, C1):
        reveal_type(obj)  # Shows a non-empty type, e.g. `Disjoint1 & C1`

런타임 구현

새로운 데코레이터인 @disjoint_basetyping 모듈에 추가됩니다. 이 데코레이터의 런타임 동작은 @final과 같은 유사한 데코레이터와 일관되게 장식된 객체에 .__disjoint_base__ = True 속성을 설정한 다음 해당 인자를 반환하는 것입니다.:

def disjoint_base(cls):
    cls.__disjoint_base__ = True
    return cls

__disjoint_base__ 속성은 런타임 인트로스펙션에 사용할 수 있습니다. 그러나 사용자 정의 클래스에 이 데코레이터를 런타임에서 강제 적용하지는 않습니다.

스텁에서 @disjoint_base 데코레이터를 적용해야 하는지 검증하는 데 유용합니다. CPython은 어떤 클래스가 분리 베이스인지 정확히 문서화하지 않지만, 런타임 인트로스펙션을 사용하여 인터프리터의 동작을 재현할 수 있습니다 (example implementation). mypy의 stubtest같은 스텁 검증 도구는 이 로직을 사용하여 스텁에서 @disjoint_base 데코레이터가 올바른 클래스에 적용되었는지 확인할 수 있습니다.

하위 호환성

이전 Python 버전과의 호환성을 위해 @disjoint_base 데코레이터가 typing_extensions 백포트 패키지에 추가됩니다.

런타임에서 새로운 데코레이터는 호환성 문제를 일으키지 않습니다.

스텁에서는 아직 모든 타입 검사기가 데코레이터를 이해하지 못하더라도 분리 베이스 클래스에 데코레이터를 추가할 수 있습니다. 그러한 타입 검사기는 단순히 데코레이터를 아무 작업도 하지 않는 것으로 처리해야 합니다.

타입 검사기가 이 PEP에 대한 지원을 추가하면 사용자는 도달 가능성과 교집합을 둘러싼 타입 검사 동작에서 일부 변경을 경험할 수 있습니다. 이러한 변경은 런타임 동작을 더 잘 반영하므로 긍정적일 것입니다. 또한 사용자가 볼 수 있는 변경 규모는 타입 검사기 버전 간의 일반적인 변경량과 비슷하게 제한될 가능성이 높습니다. 이 변경의 영향에 대해 우려하는 타입 검사기는 선택적 활성화 플래그와 같은 전환 메커니즘을 사용할 수 있습니다.

보안 영향

알려진 바가 없습니다.

이것을 가르치는 방법

대부분의 사용자는 @disjoint_base 데코레이터를 직접 사용하거나 이해할 필요가 없습니다. 이 데코레이터는 주로 저수준 라이브러리의 라이브러리 스텁에서 사용될 것으로 예상되기 때문입니다. 사용자가 클래스에 일상적으로 추가해야 하는 데코레이터로 가르쳐서는 안 됩니다. Python 교사는 특정한 경우에 다중 상속이 허용되지 않는 이유를 설명하기 위해 “서로 배타적인 베이스”라는 개념을 소개할 수 있습니다. Python 타입 지정을 가르치는 교사는 isinstance()와 같은 타입 축소 구문을 가르칠 때 이 데코레이터를 소개하여, 타입 검사기가 특정 분기를 도달 불가능한 것으로 처리하는 이유를 사용자에게 설명할 수 있습니다.

참조 구현

@disjoint_base 데코레이터의 런타임 구현은 typing-extensions 4.15.0에서 사용할 수 있습니다. Mypy와 해당 stubtest 도구는 버전 1.18.1부터 이 데코레이터를 지원하며, 이는 python/mypy#19678에서 구현되었습니다. astral-sh/ruff#20084에서 ty 타입 검사기에 대한 지원이 추가되었으며, JelleZijlstra/pycroscope#431에서 pycroscope에도 지원이 추가되었습니다.

부록

이 부록에서는 타입 시스템과 CPython 런타임에서 다중 상속을 둘러싼 기존 상황을 더 자세히 설명합니다.

CPython의 솔리드 베이스

“솔리드 베이스”라는 개념은 오랫동안 CPython 구현의 일부였으며, 이 개념은 2001년 커밋 하나까지 거슬러 올라갑니다. 그럼에도 불구하고 이 개념은 문서에서 거의 주목받지 못했습니다. 이 메커니즘의 세부 사항은 CPython의 내부 객체 표현과 밀접하게 연관되어 있지만, CPython이 이런 방식으로 작동하는 방식과 이유를 높은 수준에서 설명하는 것은 유용합니다.

CPython의 모든 객체는 본질적으로 객체에 관한 정보를 포함하는 연속된 메모리 영역인 C 구조체를 가리키는 포인터입니다. 객체의 타입에 대한 참조와 사용자 정의 객체의 __dict__ attribute처럼 일부 정보는 인터프리터가 관리하며 많은 객체 또는 모든 객체가 공유합니다. 일부 클래스는 해당 클래스에만 해당하는 추가 정보를 포함합니다. 예를 들어, __slots__가 있는 사용자 정의 클래스는 각 슬롯을 위한 메모리 공간을 포함하며, 내장 float 클래스는 float 값을 저장하는 C double 값을 포함합니다. 이 메모리 배치는 해당 클래스의 모든 인스턴스에서 유지되어야 합니다. float와 상호작용하는 C 코드는 객체 메모리의 특정 오프셋에서 값을 찾을 것으로 예상합니다.

자식 클래스가 생성되면 CPython은 모든 부모 클래스와 호환되는 새 클래스의 메모리 배치를 생성해야 합니다. 예를 들어 float의 자식 클래스가 생성될 때, 자식 클래스의 인스턴스를 float 클래스의 기반 구조체와 직접 상호작용하는 C 코드에 전달할 수 있어야 합니다. 따라서 이러한 서브클래스는 부모 float 클래스와 동일한 오프셋에 double 값을 저장해야 합니다. 그러나 구조체 끝에 추가 필드를 추가할 수는 있습니다. CPython은 __dict__ attribute를 사용하여 이를 처리할 수 있으므로, __dict__를 추가하는 float의 자식 클래스를 생성할 수 있습니다.

그러나 구조체에 double이 있어야 하는 float을 같은 위치에 다른 데이터를 저장하는 int와 같은 또 다른 C 타입과 결합할 방법은 없습니다. 따라서 floatint의 공통 서브클래스는 존재할 수 없습니다. 이를 floatint가 솔리드 베이스라고 말합니다.

C로 구현된 클래스는 고정된 오프셋에 데이터를 저장하는 기반 구조체를 가지며, 그 구조체가 부모 클래스의 구조체와 다르면 솔리드 베이스입니다. C 클래스는 데이터의 가변 크기 배열(문자열의 내용 등)을 저장할 수도 있습니다. 이것이 부모 클래스와 다르면 해당 클래스 역시 솔리드 베이스가 됩니다. CPython 구현은 타입 객체의 tp_itemsizetp_basicsize 필드에서 이를 추론하며, 이 필드들은 Python 코드에서 타입 객체의 문서화되지 않은 __itemsize____basicsize__ attribute로도 접근할 수 있습니다.

마찬가지로 Python으로 구현된 클래스는 __slots__가 있으면 솔리드 베이스입니다. 슬롯이 특정 메모리 배치를 강제하기 때문입니다.

Mypy의 호환성 검사

mypy 타입 검사기는 호환되지 않는 메서드를 가진 두 클래스를 호환되지 않는 것으로 간주합니다. 예를 들어, 여러 메서드의 정의가 호환되지 않기 때문에 mypy는 클래스 int와 클래스 str를 호환되지 않는 것으로 간주합니다. 다음과 같은 클래스 정의가 주어지면:

class C(int, str):
    pass

Mypy는 Definition of "__add__" in base class "int" is incompatible with definition in base class "str",라는 출력을 내고, 여러 다른 메서드에 대해서도 이와 유사한 오류를 출력합니다. 이러한 오류는 올바릅니다. 두 클래스에서 __add__의 정의가 실제로 호환되지 않기 때문입니다. int.__add__int 인자를 요구하는 반면, str.__add__str 인자를 요구합니다. 이 클래스가 존재한다면 런타임에 __add__int.__add__로 해석됩니다. C의 인스턴스는 str 타입의 멤버이기도 하지만, 다른 str와의 연결처럼 str이 지원하는 일부 연산을 지원하지 않습니다.

지금까지는 좋습니다. 그러나 mypy는 매우 유사한 논리를 사용하여 어떤 클래스도 intstr를 모두 상속할 수 없다고 결론 내립니다. 그럼에도 불구하고 다음 클래스 정의를 오류 없이 받아들입니다:

from typing import Never

class C(int, str):
    def __add__(self, other: object) -> Never:
        raise TypeError
    def __mod__(self, other: object) -> Never:
        raise TypeError
    def __mul__(self, other: object) -> Never:
        raise TypeError
    def __rmul__(self, other: object) -> Never:
        raise TypeError
    def __ge__(self, other: int | str) -> bool:
        return int(self) >= other if isinstance(other, int) else str(self) >= other
    def __gt__(self, other: int | str) -> bool:
        return int(self) > other if isinstance(other, int) else str(self) > other
    def __lt__(self, other: int | str) -> bool:
        return int(self) < other if isinstance(other, int) else str(self) < other
    def __le__(self, other: int | str) -> bool:
        return int(self) <= other if isinstance(other, int) else str(self) <= other
    def __getnewargs__(self) -> Never:
        raise TypeError

속성에서도 비슷한 상황이 발생합니다. 호환되지 않는 속성을 가진 두 클래스가 주어지면 mypy는 공통 서브클래스가 존재할 수 없다고 주장하지만, 이러한 속성을 재정의하여 호환되게 만드는 서브클래스는 받아들입니다:

from typing import Never

class X:
    a: int

class Y:
    a: str

class Z(X, Y):
    @property
    def a(self) -> Never:
        raise RuntimeError("no luck")
    @a.setter
    def a(self, value: int | str) -> None:
        pass

지금까지 제시한 예는 Never를 반환하는 재정의에 의존하지만, mypy의 규칙은 실제로 더 유용한 구현을 가진 클래스도 거부할 수 있습니다:

from typing import Literal

class Carnivore:
    def eat(self, food: Literal["meat"]) -> None:
        print("devouring meat")

class Herbivore:
    def eat(self, food: Literal["plants"]) -> None:
        print("nibbling on plants")

class Omnivore(Carnivore, Herbivore):
    def eat(self, food: str) -> None:
        print(f"eating {food}")

def is_it_both(obj: Carnivore):
    # mypy --warn-unreachable:
    # Subclass of "Carnivore" and "Herbivore" cannot exist: would have incompatible method signatures
    if isinstance(obj, Herbivore):
        pass

mypy의 규칙은 두 클래스의 교집합에 원소가 존재하는지를 추론하는 데 실무적으로 상당히 잘 작동합니다. 서로소 베이스인 대부분의 내장 클래스는 __add____iter__같은 공통 던더 메서드를 서로 호환되지 않는 방식으로 구현하는 경우가 많으므로, mypy는 이러한 클래스들을 호환되지 않는 것으로 간주합니다. 몇 가지 예외가 있습니다. mypy는 class C(BaseException, int): ...를 허용하지만, 두 클래스 모두 서로소 베이스이며 런타임에는 클래스 정의가 거부됩니다. 반대로 다중 상속이 실제로 사용되는 경우에는 일반적으로 부모 클래스에 호환되지 않는 메서드가 없습니다.

따라서 두 클래스가 교집합을 가질 수 없다고 판단하는 mypy의 접근 방식은 너무 포괄적이면서도(일부 교집합을 잘못 비어 있다고 간주함) 너무 제한적입니다(서로소 베이스 때문에 비어 있는 일부 교집합을 놓침). 이는 mypy 추적기의 이슈에서 논의됩니다.