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

Python Enhancement Proposals

PEP 800 – Disjunkte Basisklassen im Typsystem

Autor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Entwurf
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
21. Jul. 2025
Python-Version:
3.15
Post-History:
18. Jul. 2025

Inhaltsverzeichnis

Zusammenfassung

Um Python-Programme präzise zu analysieren, müssen Typ-Checker wissen, wann zwei Klassen eine gemeinsame Kindklasse haben können und wann nicht. Die notwendigen Informationen zur Bestimmung dessen sind derzeit jedoch nicht Teil des Typsystems. Dieses PEP fügt einen neuen Dekorator hinzu, @typing.disjoint_base, der angibt, dass eine Klasse eine „disjunkte Basisklasse“ ist. Zwei Klassen, die unterschiedliche, nicht zusammenhängende disjunkte Basisklassen haben, können keine gemeinsame Kindklasse haben.

Motivation

Beim Typ-Checking von Python ist das Konzept der Erreichbarkeit wichtig. Python-Typ-Checker erkennen im Allgemeinen, wenn ein Code-Zweig niemals erreicht werden kann, und sie warnen die Benutzer vor solchem Code. Dies ist nützlich, da unerreichbarer Code das Programm unnötig verkompliziert und seine Anwesenheit auf einen Fehler hinweisen kann.

Zum Beispiel wird in diesem Programm

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

sowohl Pyright als auch Mypy (mit --warn-unreachable), zwei beliebte Typ-Checker, warnen, dass der Körper des if-Blocks unerreichbar ist, da, wenn x ein bool ist, es nicht auch ein str sein kann.

Die Erreichbarkeit wird in Python durch die Anwesenheit von Mehrfachvererbung kompliziert. Wenn anstelle von bool und str zwei benutzerdefinierte Klassen verwendet werden, zeigen Mypy und Pyright keine Warnungen an

class A: pass
class B: pass

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

Dies ist korrekt, da eine Klasse, die sowohl von A als auch von B erbt, existieren könnte.

Wir sehen eine Abweichung zwischen Typ-Checkern in einem anderen Fall, in dem wir int und str verwenden

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

Für diesen Code zeigt Pyright keine Fehler an, aber Mypy behauptet, dass der Zweig unerreichbar ist. Mypy hat hier technisch gesehen Recht: CPython erlaubt es einer Klasse nicht, sowohl von int als auch von str zu erben, daher ist der Zweig unerreichbar. Die notwendigen Informationen, um festzustellen, dass diese Basisklassen inkompatibel sind, sind jedoch derzeit im Typsystem nicht verfügbar. Mypy verwendet tatsächlich eine Heuristik, die auf der Anwesenheit von inkompatiblen Methoden basiert; diese Heuristik funktioniert in der Praxis einigermaßen gut, insbesondere bei integrierten Typen, ist aber im Allgemeinen falsch, wie weiter unten im Detail diskutiert wird besprochen.

Der experimentelle Typ-Checker ty verwendet einen dritten Ansatz, der näher am Laufzeitverhalten von Python liegt: Er erkennt bestimmte Klassen als „solide Basisklassen“, die die Mehrfachvererbung einschränken. Grob gesagt muss jede Klasse von höchstens einer eindeutigen soliden Basisklasse erben, und wenn keine eindeutige solide Basisklasse vorhanden ist, kann die Klasse nicht existieren; eine präzisere Definition werden wir unten geben. Der Ansatz von ty stützt sich jedoch auf hartkodiertes Wissen über bestimmte integrierte Typen. Der Begriff „solide Basisklasse“ leitet sich von der CPython-Implementierung ab; dieses PEP verwendet den neu vorgeschlagenen Begriff „disjunkte Basisklasse“ stattdessen.

Dieses PEP schlägt eine Erweiterung des Typsystems vor, die es ermöglicht auszudrücken, wann Mehrfachvererbung zur Laufzeit nicht erlaubt ist: ein Dekorator @disjoint_base, der Klassen als disjunkte Basisklasse kennzeichnet. Dies gibt Typ-Checkern ein präziseres Verständnis von Erreichbarkeit und hilft in mehreren konkreten Bereichen.

Ungültige Klassendefinitionen

Die folgende Klassendefinition löst zur Laufzeit einen Fehler aus, da int und str unterschiedliche disjunkte Basisklassen sind

class C(int, str): pass

Ohne Kenntnis von disjunkten Basisklassen sind Typ-Checker derzeit nicht in der Lage zu erkennen, warum diese Klassendefinition ungültig ist, obwohl sie erkennen mögen, dass einige ihrer Methoden inkompatibel wären, wenn diese Klasse existieren würde. (Wenn Mypy diese Klassendefinition sieht, verweist es auf inkompatible Definitionen von __add__ und mehreren anderen Methoden.)

Dies ist für sich genommen kein besonders überzeugendes Problem, da der Fehler normalerweise beim ersten Import des Codes entdeckt würde, wird aber hier der Vollständigkeit halber erwähnt.

Erreichbarkeit

Wir haben bereits die Erreichbarkeit von Code mit isinstance() erwähnt. Ähnliche Probleme treten bei anderen Typ-Narro-Konstrukten wie match-Anweisungen auf: Eine korrekte Inferenz der Erreichbarkeit erfordert ein Verständnis von disjunkten Basisklassen.

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!")

Overloads

Mit @overload dekorierte Funktionen können unsicher sein, wenn sich die Parametertypen einiger Overloads überschneiden, die Rückgabetypen jedoch nicht. Zum Beispiel könnte die folgende Menge von Overloads ausgenutzt werden, um unsicheres Verhalten zu erzielen

from typing import overload

class A: pass
class B: pass

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

Wenn eine Klasse existiert, die sowohl von A als auch von B erbt, könnten Typ-Checker bei einem Aufruf von f() den falschen Overload wählen.

Typ-Checker könnten diese Quelle der Unsicherheit erkennen und davor warnen, aber eine korrekte Implementierung erfordert ein Verständnis von disjunkten Basisklassen, da sie davon abhängt zu wissen, ob Werte, die Instanzen von sowohl A als auch B sind, existieren können. Obwohl viele Typ-Checker bereits eine Version dieser Prüfung für sich überschneidende Overloads durchführen, schreibt die Typisierungsspezifikation derzeit nicht vor, wie diese Prüfung ablaufen soll. Dieses PEP schlägt keine Änderung daran vor, aber es liefert einen Baustein für eine korrekte Prüfung auf sich überschneidende Overloads.

Schnittmengentypen

Explizite Schnittmengentypen, die einen Typ bezeichnen, der Werte enthält, die Instanzen aller gegebenen Typen sind, sind derzeit nicht Teil des Typsystems. Sie treten jedoch natürlich in einem mengenbasierten Typsystem wie dem von Python auf, als Ergebnis von Typ-Narro, und zukünftige Erweiterungen des Typsystems können Unterstützung für explizite Schnittmengentypen hinzufügen.

Bei Schnittmengentypen ist es oft wichtig zu wissen, ob eine bestimmte Schnittmenge bewohnt ist, d.h. ob es Werte gibt, die Mitglieder dieser Schnittmenge sein können. Dies ermöglicht es Typ-Checkern, die Erreichbarkeit zu verstehen und den Benutzern präzisere Typinformationen zu liefern.

Als konkretes Beispiel könnte eine mögliche Implementierung der Zuweisbarkeit mit Schnittmengentypen darin bestehen, dass für einen Schnittmengentyp A & B ein Typ C diesem zugewiesen werden kann, wenn C mindestens einem von A und B zugewiesen werden kann und sich mit A und B überschneidet. („Überschneiden“ bedeutet hier, dass mindestens ein Laufzeitwert existieren könnte, der Mitglied beider Typen wäre. Das heißt, A und B überschneiden sich, wenn A & B bewohnt ist.) Der zweite Teil der Regel stellt sicher, dass str nicht einem Typ wie int & Any zugewiesen werden kann: Obwohl str Any zugewiesen werden kann, überschneidet es sich nicht mit int. Aber natürlich können wir nur wissen, dass str und int sich nicht überschneiden, wenn wir wissen, dass beide Klassen disjunkte Basisklassen sind.

Übersicht

Disjunkte Basisklassen können in vielen Ecken des Typsystems hilfreich sein. Obwohl einige dieser Ecken unterdefiniert, spekulativ oder von marginaler Bedeutung sind, ermöglicht das Konzept der disjunkten Basisklassen den Typ-Checkern in jedem Fall ein präziseres Verständnis als das aktuelle Typsystem zulässt. Somit bieten disjunkte Basisklassen eine solide Grundlage (eine solide Basis, wenn man so will) zur Verbesserung des Python-Typsystems.

Begründung

Das Konzept der „disjunkten Basisklassen“ ermöglicht es Typ-Checkern zu verstehen, wann eine gemeinsame Kindklasse von zwei Klassen existieren kann und wann nicht. Um dieses Konzept an Typ-Checker zu kommunizieren, fügen wir dem Typsystem einen Dekorator @disjoint_base hinzu, der eine Klasse als disjunkte Basisklasse markiert. Die Semantik ist grob, dass eine Klasse nicht zwei nicht zusammenhängende disjunkte Basisklassen haben kann.

Benennung

Die anfängliche Version dieses PEP verwendete den Namen „solide Basisklasse“, in Anlehnung an die Terminologie der CPython-Implementierung. Dieser Begriff ist jedoch etwas vage. Der alternative Begriff „disjunkte Basisklasse“ suggeriert, dass eine Klasse mit diesem Dekorator von anderen Basisklassen disjunkt ist, was eine gute Erstbeschreibung des Konzepts ist. (Die genaue Semantik ist subtiler und wird unten beschrieben.)

Laufzeitbeschränkungen für Mehrfachvererbung

Obwohl Python Mehrfachvererbung generell erlaubt, gibt es Laufzeitbeschränkungen, wie in CPython dokumentiert. Zwei Sätze von Beschränkungen, ein konsistentes MRO und eine konsistente Metaklasse, können bereits von Typ-Checkern unter Verwendung von im Typsystem verfügbaren Informationen implementiert werden. Die dritte Beschränkung, die die Instanz-Layout betrifft, ist diejenige, die Kenntnisse über disjunkte Basisklassen erfordert. Klassen, die eine nicht-leere __slots__-Definition enthalten, sind automatisch disjunkte Basisklassen, ebenso wie viele in C implementierte eingebaute Klassen.

Alternative Implementierungen von Python, wie PyPy, verhalten sich tendenziell ähnlich wie CPython, können aber in Details abweichen, z.B. welche Standardbibliotheksklassen genau disjunkte Basisklassen sind. Da das Typsystem derzeit keine explizite Unterstützung für alternative Python-Implementierungen enthält, empfiehlt dieses PEP, dass Stub-Bibliotheken wie typeshed das Verhalten von CPython verwenden, um zu entscheiden, wann der Dekorator @disjoint_base verwendet werden soll. Wenn zukünftige Erweiterungen des Typsystems Unterstützung für alternative Implementierungen hinzufügen (z. B. Verzweigung nach dem Wert von sys.implementation.name), könnten Stubs die Anwesenheit des Dekorators @disjoint_base bei Bedarf auf die Implementierung konditionieren.

Obwohl das Konzept der „disjunkten Basisklassen“ (als „solide Basisklassen“ bezeichnet) in der CPython-Implementierung seit Jahrzehnten existiert, haben sich die Regeln zur Entscheidung, welche Klassen disjunkte Basisklassen sind, gelegentlich geändert. Vor Python 3.12 konnten das Hinzufügen eines __dict__ oder die Unterstützung für Weakrefs relativ zur Basisklasse eine Klasse zu einer disjunkten Basisklasse machen. In der Praxis bedeutete dies oft, dass von C-implementierten Klassen abgeleitete, in Python implementierte Klassen, wie z. B. namedtuple()-Klassen, selbst disjunkte Basisklassen waren. Dieses Verhalten wurde in Python 3.12 durch python/cpython#96028 geändert. Dieses PEP konzentriert sich auf die Unterstützung des Verhaltens von Python 3.12 und neuer, das einfacher und leichter zu verstehen ist. Typ-Checker können sich entscheiden, eine Version des Verhaltens vor Python 3.12 zu implementieren, wenn sie möchten, aber eine korrekte Umsetzung erfordert Informationen, die derzeit nicht im Typsystem verfügbar sind.

Die genaue Menge der Klassen, die zur Laufzeit disjunkte Basisklassen sind, kann sich in zukünftigen Python-Versionen wieder ändern. Sollte dies geschehen, könnten die von Typ-Checkern verwendeten Stub-Dateien aktualisiert werden, um dieser neuen Realität Rechnung zu tragen. Mit anderen Worten, dieses PEP fügt dem Typsystem das Konzept der disjunkten Basisklassen hinzu, schreibt aber nicht genau vor, welche Klassen disjunkte Basisklassen sind.

@disjoint_base in Implementierungsdateien

Der offensichtlichste Anwendungsfall für den Dekorator @disjoint_base wird in Stub-Dateien für C-Bibliotheken, wie z.B. der Standardbibliothek, zum Markieren von in C implementierten disjunkten Basisklassen sein.

Es gibt jedoch auch Anwendungsfälle für die Kennzeichnung von disjunkten Basisklassen in Implementierungsdateien, bei denen der Effekt darin besteht, die Existenz von Kindklassen zu verbieten, die von der dekorierten Klasse und einer anderen disjunkten Basisklasse erben, wie z.B. einer Standardbibliotheksklasse oder einer anderen benutzerdefinierten Klasse, die mit @disjoint_base dekoriert ist. Dies könnte Typ-Checkern beispielsweise ermöglichen, Code zu kennzeichnen, der nur erreichbar ist, wenn eine Klasse existiert, die sowohl von einer Benutzerklasse als auch von einer Standardbibliotheksklasse wie int oder str erbt, was technisch möglich, aber praktisch nicht plausibel sein mag.

@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

Dies ähnelt prinzipiell dem bestehenden Dekorator @final, der ebenfalls die Unterklassenbildung einschränkt: In Stubs wird er verwendet, um Klassen zu markieren, die die Unterklassenbildung programmatisch verbieten, aber in Implementierungsdateien wird er oft verwendet, um anzuzeigen, dass eine Klasse nicht zur Unterklasse vorgesehen ist, ohne Laufzeit-Durchsetzung.

@disjoint_base bei speziellen Klassen

Der Dekorator @disjoint_base ist primär für nominale Klassen gedacht, aber das Typsystem enthält einige andere Konstrukte, die syntaktisch Klassendefinitionen verwenden, daher müssen wir überlegen, ob der Dekorator auch auf sie angewendet werden sollte und wenn ja, was er bedeuten würde.

Für Protocol-Definitionen wäre die konsistenteste Interpretation, dass die einzigen Klassen, die das Protokoll implementieren können, Klassen sind, die eine nominale Vererbung vom Protokoll verwenden, oder @final-Klassen, die das Protokoll implementieren. Andere Klassen haben oder könnten potenziell eine disjunkte Basisklasse haben, die nicht das Protokoll ist. Das ist kompliziert und nicht nützlich, daher verbieten wir @disjoint_base auf Protocol-Definitionen.

Ebenso ist das Konzept einer „disjunkten Basisklasse“ bei TypedDict-Definitionen nicht sinnvoll, da TypedDicts rein strukturelle Typen sind.

Obwohl sie eine gewisse Sonderbehandlung im Typsystem erhalten, erstellen NamedTuple-Definitionen echte nominale Klassen, die Kindklassen haben können, daher ist es sinnvoll, @disjoint_base auf ihnen zuzulassen und sie für die Zwecke des Disjoint-Base-Mechanismus wie normale Klassen zu behandeln. Alle NamedTuple-Klassen haben tuple, eine disjunkte Basisklasse, in ihrem MRO, daher können sie nicht mehrfach von anderen disjunkten Basisklassen erben.

Spezifikation

Ein Dekorator @typing.disjoint_base wird zum Typsystem hinzugefügt. Er darf nur auf nominale Klassen angewendet werden, einschließlich NamedTuple-Definitionen; es ist ein Fehler für Typ-Checker, den Dekorator auf eine Funktion, TypedDict-Definition oder Protocol-Definition anzuwenden.

Wir definieren zwei Eigenschaften für (nominale) Klassen: Eine Klasse kann eine disjunkte Basisklasse *sein* oder nicht, und jede Klasse muss eine gültige disjunkte Basisklasse *haben*.

Eine Klasse ist eine disjunkte Basisklasse, wenn sie mit @typing.disjoint_base dekoriert ist oder wenn sie eine nicht-leere __slots__-Definition enthält. Dies schließt Klassen ein, die __slots__ aufgrund des @dataclass(slots=True)-Dekorators oder aufgrund der Verwendung des dataclass_transform-Mechanismus zur Hinzufügung von Slots haben. Die universelle Basisklasse, object, ist ebenfalls eine disjunkte Basisklasse.

Um die disjunkte Basisklasse einer Klasse zu bestimmen, betrachten wir alle ihre Basisklassen, um eine Menge von Kandidaten für disjunkte Basisklassen zu ermitteln. Für jede Basis, die selbst eine disjunkte Basisklasse ist, ist der Kandidat die Basis selbst; andernfalls ist es die disjunkte Basisklasse der Basis. Wenn die Kandidatenmenge eine einzelne disjunkte Basisklasse enthält, ist dies die disjunkte Basisklasse der Klasse. Wenn es mehrere Kandidaten gibt, aber einer von ihnen ein Unterklasse von allen anderen Kandidaten ist, ist diese Klasse die disjunkte Basisklasse. Wenn kein solcher Kandidat existiert, hat die Klasse keine gültige disjunkte Basisklasse und kann daher nicht existieren.

Typ-Checker müssen eine gültige disjunkte Basisklasse beim Überprüfen von Klassendefinitionen prüfen und eine Diagnose ausgeben, wenn sie eine Klassendefinition antreffen, der eine gültige disjunkte Basisklasse fehlt. Typ-Checker können den Disjoint-Base-Mechanismus auch verwenden, um zu bestimmen, ob Typen disjunkt sind, z. B. beim Prüfen, ob ein Typ-Narro-Konstrukt wie isinstance() zu einem unerreichbaren Zweig führt.

Beispiel

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`

Laufzeitimplementierung

Ein neuer Dekorator, @disjoint_base, wird dem Modul typing hinzugefügt. Sein Laufzeitverhalten (im Einklang mit ähnlichen Dekoratoren wie @final) besteht darin, ein Attribut .__disjoint_base__ = True auf das dekorierte Objekt zu setzen und dann sein Argument zurückzugeben.

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

Das Attribut __disjoint_base__ kann zur Laufzeit-Introspektion verwendet werden. Es gibt jedoch keine Laufzeit-Durchsetzung dieses Dekorators für benutzerdefinierte Klassen.

Es wird nützlich sein zu validieren, ob der Dekorator @disjoint_base in einem Stub angewendet werden sollte. Obwohl CPython nicht präzise dokumentiert, welche Klassen disjunkte Basisklassen sind, ist es möglich, das Verhalten des Interpreters durch Laufzeit-Introspektion zu replizieren (Beispielimplementierung). Stub-Validierungswerkzeuge, wie Mypys stubtest, könnten diese Logik verwenden, um zu überprüfen, ob der Dekorator @disjoint_base auf die richtigen Klassen in Stubs angewendet wird.

Abwärtskompatibilität

Für die Kompatibilität mit früheren Python-Versionen wird der Dekorator @disjoint_base zum Backport-Paket typing_extensions hinzugefügt.

Zur Laufzeit stellt der neue Dekorator keine Kompatibilitätsprobleme dar.

In Stubs kann der Dekorator zu disjunkten Basisklassen hinzugefügt werden, auch wenn noch nicht alle Typ-Checker den Dekorator verstehen; solche Typ-Checker sollten den Dekorator einfach als No-Op behandeln.

Wenn Typ-Checker die Unterstützung für dieses PEP hinzufügen, können Benutzer einige Änderungen im Typ-Checking-Verhalten in Bezug auf Erreichbarkeit und Schnittmengen feststellen. Diese Änderungen sollten positiv sein, da sie das Laufzeitverhalten besser widerspiegeln werden, und das Ausmaß der für Benutzer sichtbaren Änderungen ist wahrscheinlich begrenzt, ähnlich dem normalen Änderungsaufwand zwischen Typ-Checker-Versionen. Typ-Checker, die sich um die Auswirkungen dieser Änderung sorgen, könnten Übergangsmechanismen wie Opt-in-Flags verwenden.

Sicherheitsimplikationen

Keine bekannt.

Wie man das lehrt

Die meisten Benutzer müssen den Dekorator @disjoint_base nicht direkt verwenden oder verstehen, da die Erwartung ist, dass er hauptsächlich in Bibliotheks-Stubs für Low-Level-Bibliotheken verwendet wird. Python-Lehrer können das Konzept der „disjunkten Basisklassen“ einführen, um zu erklären, warum Mehrfachvererbung in bestimmten Fällen nicht erlaubt ist. Python-Typing-Lehrer können den Dekorator einführen, wenn sie Typ-Narro-Konstrukte wie isinstance() lehren, um den Benutzern zu erklären, warum Typ-Checker bestimmte Zweige als unerreichbar behandeln.

Referenzimplementierung

Die Laufzeitimplementierung des Dekorators @disjoint_base ist in typing-extensions 4.15.0 verfügbar. python/mypy#19678 implementiert die Unterstützung für disjunkte Basisklassen in mypy und im stubtest-Tool. astral-sh/ruff#20084 implementiert die Unterstützung für disjunkte Basisklassen im ty-Typ-Checker.

Anhang

Dieser Anhang diskutiert die bestehende Situation bezüglich Mehrfachvererbung im Typsystem und in der CPython-Laufzeit genauer.

Solide Basisklassen in CPython

Das Konzept der „soliden Basisklassen“ ist seit langem Teil der CPython-Implementierung; das Konzept reicht bis zu einem Commit aus dem Jahr 2001 zurück. Dennoch hat das Konzept wenig Aufmerksamkeit in der Dokumentation erhalten. Obwohl die Details des Mechanismus eng mit der internen Objekt-Repräsentation von CPython verbunden sind, ist es nützlich, auf hoher Ebene zu erklären, wie und warum CPython auf diese Weise funktioniert.

Jedes Objekt in CPython ist im Wesentlichen ein Zeiger auf eine C-Struktur, ein zusammenhängendes Stück Speicher, das Informationen über das Objekt enthält. Einige Informationen werden vom Interpreter verwaltet und von vielen oder allen Objekten gemeinsam genutzt, z. B. ein Verweis auf den Typ des Objekts und das Attribut __dict__ für benutzerdefinierte Objekte. Einige Klassen enthalten zusätzliche Informationen, die für diese Klasse spezifisch sind. Zum Beispiel enthalten benutzerdefinierte Klassen mit __slots__ einen Speicherplatz für jeden Slot, und die eingebaute float-Klasse enthält einen C double-Wert, der den Wert des Floats speichert. Dieses Speicherlayout muss für alle Instanzen der Klasse beibehalten werden: C-Code, der mit einem float interagiert, erwartet, den Wert an einem bestimmten Offset im Speicher des Objekts zu finden.

Wenn eine Kindklasse erstellt wird, muss CPython ein Speicherlayout für die neue Klasse erstellen, das mit allen ihren Elternklassen kompatibel ist. Wenn beispielsweise eine Kindklasse von float erstellt wird, muss es möglich sein, Instanzen der Kindklasse an C-Code zu übergeben, der direkt mit der zugrundeliegenden Struktur für die float-Klasse interagiert. Daher muss eine solche Unterklasse den double-Wert am selben Offset speichern wie die Elternklasse float. Sie kann jedoch zusätzliche Felder am Ende der Struktur hinzufügen. CPython weiß, wie das mit dem Attribut __dict__ geht, weshalb es möglich ist, eine Kindklasse von float zu erstellen, die ein __dict__ hinzufügt.

Es gibt jedoch keine Möglichkeit, einen float, der ein double in seiner Struktur haben muss, mit einem anderen C-Typ wie int, der unterschiedliche Daten an derselben Stelle speichert, zu kombinieren. Daher kann eine gemeinsame Kindklasse von float und int nicht existieren. Wir sagen, dass float und int solide Basisklassen sind.

Eine in C implementierte Klasse ist eine solide Basisklasse, wenn sie eine zugrundeliegende Struktur hat, die Daten an einem festen Offset speichert, und diese Struktur sich von der Struktur ihrer Elternklasse unterscheidet. Eine C-Klasse kann auch ein Array von variabler Größe speichern (wie der Inhalt eines Strings); wenn dies von der Elternklasse abweicht, wird die Klasse ebenfalls zu einer soliden Basisklasse. CPython leitet dies aus den Feldern tp_itemsize und tp_basicsize des Typobjekts ab, die aus Python-Code auch als undokumentierte Attribute __itemsize__ und __basicsize__ auf Typobjekten zugänglich sind.

Ebenso sind in Python implementierte Klassen solide Basisklassen, wenn sie __slots__ haben, da Slots ein bestimmtes Speicherlayout erzwingen.

Mypys Inkompatibilitätsprüfung

Der Mypy-Typ-Checker betrachtet zwei Klassen als inkompatibel, wenn sie inkompatible Methoden haben. Zum Beispiel betrachtet Mypy die Klassen int und str als inkompatibel, da sie inkompatible Definitionen verschiedener Methoden haben. Bei einer Klassendefinition wie

class C(int, str):
    pass

Mypy gibt Folgendes aus: Definition of "__add__" in base class "int" is incompatible with definition in base class "str", und ähnliche Fehler für eine Reihe anderer Methoden. Diese Fehler sind korrekt, da die Definitionen von __add__ in den beiden Klassen tatsächlich inkompatibel sind: int.__add__ erwartet ein int-Argument, während str.__add__ ein str-Argument erwartet. Wenn diese Klasse existieren würde, würde zur Laufzeit __add__ auf int.__add__ aufgelöst. Instanzen von C wären auch Mitglieder des str-Typs, aber sie würden einige der Operationen, die str unterstützt, wie die Verkettung mit einem anderen str, nicht unterstützen.

Bisher alles gut. Aber Mypy verwendet auch eine sehr ähnliche Logik, um zu dem Schluss zu kommen, dass keine Klasse sowohl von int als auch von str erben kann. Dennoch akzeptiert es die folgende Klassendefinition ohne Fehler

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

Es gibt eine ähnliche Situation bei Attributen. Bei zwei Klassen mit inkompatiblen Attributen behauptet Mypy, dass keine gemeinsame Unterklasse existieren kann, und doch akzeptiert es eine Unterklasse, die diese Attribute überschreibt, um sie kompatibel zu machen.

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

Während die bisherigen Beispiele Überschreibungen verwenden, die Never zurückgeben, kann Mypys Regel auch Klassen ablehnen, die praktisch nützlichere Implementierungen haben.

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

Mypys Regel funktioniert in der Praxis einigermaßen gut, um abzuleiten, ob eine Schnittmenge von zwei Klassen bewohnt ist. Die meisten eingebauten Klassen, die disjunkte Basisklassen sind, implementieren gemeinsame Dunder-Methoden wie __add__ und __iter__ auf inkompatible Weise, sodass Mypy sie als inkompatibel betrachtet. Es gibt einige Ausnahmen: Mypy erlaubt class C(BaseException, int): ..., obwohl beide Klassen disjunkte Basisklassen sind und die Klassendefinition zur Laufzeit abgelehnt wird. Umgekehrt, wenn Mehrfachvererbung in der Praxis verwendet wird, haben die Elternklassen normalerweise keine inkompatiblen Methoden.

Somit ist Mypys Ansatz, zu entscheiden, dass zwei Klassen sich nicht schneiden können, sowohl zu breit (er betrachtet fälschlicherweise einige Schnittmengen als unbewohnt) als auch zu eng (er übersieht einige Schnittmengen, die wegen disjunkter Basisklassen unbewohnt sind). Dies wird in einem Issue im mypy-Tracker diskutiert.


Quelle: https://github.com/python/peps/blob/main/peps/pep-0800.rst

Zuletzt geändert: 2025-08-25 19:44:56 GMT