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

Python Enhancement Proposals

PEP 3133 – Einführung von Rollen

Autor:
Collin Winter <collinwinter at google.com>
Status:
Abgelehnt
Typ:
Standards Track
Benötigt:
3115, 3129
Erstellt:
01. Mai 2007
Python-Version:
3.0
Post-History:
13. Mai 2007

Inhaltsverzeichnis

Ablehnungsbescheid

Diese PEP hat dazu beigetragen, PEP 3119 zu einem vernünftigeren, minimalistischeren Ansatz zu drängen. Aber angesichts der neuesten Version von PEP 3119 bevorzuge ich das deutlich. GvR.

Zusammenfassung

Das bestehende Objektmodell von Python organisiert Objekte nach ihrer Implementierung. Es ist oft wünschenswert – insbesondere in einer auf Duck-Typing basierenden Sprache wie Python –, Objekte nach der Rolle, die sie in einem größeren System spielen (ihrer Absicht), anstatt danach, wie sie diese Rolle erfüllen (ihrer Implementierung), zu organisieren. Diese PEP führt das Konzept der Rollen ein, einen Mechanismus zur Organisation von Objekten nach ihrer Absicht und nicht nach ihrer Implementierung.

Begründung

Am Anfang waren Objekte. Sie ermöglichten Programmierern, Funktion und Zustand zu vereinen und die Wiederverwendbarkeit von Code durch Konzepte wie Polymorphie und Vererbung zu erhöhen, und siehe da, es war gut. Es kam jedoch eine Zeit, in der Vererbung und Polymorphie nicht mehr ausreichten. Mit der Erfindung von Hunden und Bäumen konnten wir uns nicht mehr damit begnügen, nur zu wissen: „Versteht es ‚bellen‘?“ Wir mussten nun wissen, was ein gegebenes Objekt unter „bellen“ verstand.

Eine Lösung, die hier detailliert beschrieben wird, ist die der Rollen, ein Mechanismus, der orthogonal und komplementär zum traditionellen Klassen-/Instanzsystem ist. Während sich Klassen mit Zustand und Implementierung befassen, beschäftigt sich der Rollenmechanismus ausschließlich mit den Verhaltensweisen, die in einer gegebenen Klasse verkörpert sind.

Dieses System wurde ursprünglich „traits“ genannt und für Squeak Smalltalk [4] implementiert. Seitdem wurde es für die Verwendung in Perl 6 [3] adaptiert, wo es „Rollen“ genannt wird, und hauptsächlich von dort wird das Konzept nun für Python 3 interpretiert. Python 3 wird den Namen „Rollen“ beibehalten.

Kurz gesagt: Rollen sagen Ihnen, *was* ein Objekt tut, Klassen sagen Ihnen, *wie* ein Objekt es tut.

In dieser PEP werde ich ein System für Python 3 skizzieren, das es ermöglicht, leicht festzustellen, ob das Verständnis von „bellen“ eines gegebenen Objekts baumartig oder hundeartig ist. (Es kann auch ernstere Beispiele geben.)

Eine Anmerkung zur Syntax

Die in dieser PEP vorgeschlagenen Syntaxvorschläge sind vorläufig und sollten als Strohmänner betrachtet werden. Die notwendigen Teile, von denen diese PEP abhängt – nämlich die Klassendefinitionssyntax von PEP 3115 und Klassendekoratoren von PEP 3129 – werden noch formalisiert und können sich ändern. Funktionsnamen werden natürlich langen Debatten über die „Bikeshedding“-Diskussionen unterliegen.

Ihre Rolle ausüben

Statische Rollenzuweisung

Beginnen wir mit der Definition der Klassen Tree und Dog.

class Tree(Vegetable):

  def bark(self):
    return self.is_rough()


class Dog(Animal):

  def bark(self):
    return self.goes_ruff()

Obwohl beide eine bark()-Methode mit der gleichen Signatur implementieren, tun sie sich wild unterschiedliche Dinge. Wir brauchen eine Möglichkeit, das, was wir erwarten, zu differenzieren. Die Abhängigkeit von Vererbung und ein einfacher isinstance()-Test schränkt die Code-Wiederverwendung ein und/oder zwingt jede hundeartige Klasse, von Dog zu erben, unabhängig davon, ob dies sinnvoll ist. Mal sehen, ob Rollen helfen können.

@perform_role(Doglike)
class Dog(Animal):
  ...

@perform_role(Treelike)
class Tree(Vegetable):
  ...

@perform_role(SitThere)
class Rock(Mineral):
  ...

Wir verwenden Klassendekoratoren aus PEP 3129, um einer Klasse eine bestimmte Rolle oder Rollen zuzuweisen. Client-Code kann nun überprüfen, ob ein eingehendes Objekt die Rolle Doglike ausführt, was es ermöglicht, auch Instanzen von Wolf, LaughingHyena und Aibo [1] zu verarbeiten.

Rollen können durch normale Vererbung zusammengesetzt werden.

@perform_role(Guard, MummysLittleDarling)
class GermanShepherd(Dog):

  def guard(self, the_precious):
    while True:
      if intruder_near(the_precious):
        self.growl()

  def get_petted(self):
    self.swallow_pride()

Hier führen Instanzen von GermanShepherd drei Rollen aus: Guard und MummysLittleDarling werden direkt zugewiesen, während Doglike von Dog geerbt wird.

Rollen zur Laufzeit zuweisen

Rollen können auch zur Laufzeit zugewiesen werden, indem die durch Dekoratoren bereitgestellte syntaktische Zuckerung entpackt wird.

Nehmen wir an, wir importieren eine Robot-Klasse aus einem anderen Modul, und da wir wissen, dass Robot bereits unsere Guard-Schnittstelle implementiert, möchten wir, dass sie auch gut mit Wachschutz-bezogenem Code funktioniert.

>>> perform(Guard)(Robot)

Dies tritt sofort in Kraft und wirkt sich auf alle Instanzen von Robot aus.

Fragen zu Rollen stellen

Nur weil wir unserer Roboterarmee gesagt haben, dass sie Wachen sind, möchten wir sie gelegentlich überprüfen und sicherstellen, dass sie noch bei ihrer Aufgabe sind.

>>> performs(our_robot, Guard)
True

Was ist mit diesem einen Roboter dort drüben?

>>> performs(that_robot_over_there, Guard)
True

Die Funktion performs() wird verwendet, um zu fragen, ob ein gegebenes Objekt eine gegebene Rolle erfüllt. Sie kann jedoch nicht verwendet werden, um einer Klasse zu fragen, ob ihre Instanzen eine Rolle erfüllen.

>>> performs(Robot, Guard)
False

Dies liegt daran, dass die Klasse Robot nicht mit einer Instanz von Robot austauschbar ist.

Neue Rollen definieren

Leere Rollen

Rollen werden wie eine normale Klasse definiert, verwenden aber die Metaklasse Role.

class Doglike(metaclass=Role):
  ...

Metaklassen werden verwendet, um anzuzeigen, dass Doglike eine Role ist, genauso wie 5 eine int ist und tuple ein type ist.

Rollen durch Vererbung zusammensetzen

Rollen können von anderen Rollen erben; dies hat den Effekt, sie zu komponieren. Hier führen Instanzen von Dog sowohl die Rollen Doglike als auch FourLegs aus.

class FourLegs(metaclass=Role):
  pass

class Doglike(FourLegs, Carnivor):
  pass

@perform_role(Doglike)
class Dog(Mammal):
  pass

Konkrete Methoden erfordern

Bisher haben wir nur leere Rollen definiert – nicht sehr nützliche Dinge. Lassen Sie uns nun verlangen, dass alle Klassen, die behaupten, die Rolle Doglike zu erfüllen, eine Methode bark() definieren.

class Doglike(FourLegs):

  def bark(self):
    pass

Keine Dekoratoren sind erforderlich, um die Methode als „abstrakt“ zu kennzeichnen, und die Methode wird nie aufgerufen, was bedeutet, dass jeder Code, den sie enthält (falls vorhanden), irrelevant ist. Rollen bieten *nur* abstrakte Methoden; konkrete Standardimplementierungen sind anderen, besser geeigneten Mechanismen wie Mixins überlassen.

Sobald Sie eine Rolle definiert und eine Klasse behauptet hat, diese Rolle zu erfüllen, ist es unerlässlich, dass dieser Anspruch überprüft wird. Hier hat der Programmierer eine der von der Rolle geforderten Methoden falsch geschrieben.

@perform_role(FourLegs)
class Horse(Mammal):

  def run_like_teh_wind(self)
    ...

Dies führt dazu, dass das Rollensystem eine Ausnahme auslöst und sich darüber beschwert, dass Ihnen eine Methode run_like_the_wind() fehlt. Das Rollensystem führt diese Prüfungen durch, sobald eine Klasse als Erfüllende einer bestimmten Rolle gekennzeichnet wird.

Konkrete Methoden müssen exakt mit der von der Rolle geforderten Signatur übereinstimmen. Hier haben wir versucht, unsere Rolle durch die Definition einer konkreten Version von bark() zu erfüllen, aber wir haben das Ziel ein wenig verfehlt.

@perform_role(Doglike)
class Coyote(Mammal):

  def bark(self, target=moon):
    pass

Die Signatur dieser Methode stimmt nicht exakt mit dem überein, was die Rolle Doglike erwartete, so dass das Rollensystem ein wenig Aufruhr verursacht.

Mechanismus

Die folgenden sind Strohmänner-Vorschläge, wie Rollen in Python ausgedrückt werden könnten. Die Beispiele hier sind so formuliert, dass der Rollenmechanismus ohne Änderungen am Python-Interpreter implementiert werden kann. (Beispiele adaptiert aus einem Artikel über Perl 6 Rollen von Curtis Poe [2].)

  1. Statische Rollenzuweisung für Klassen
    @perform_role(Thieving)
    class Elf(Character):
      ...
    

    perform_role() akzeptiert mehrere Argumente, so dass auch dies zulässig ist.

    @perform_role(Thieving, Spying, Archer)
    class Elf(Character):
      ...
    

    Die Klasse Elf führt nun sowohl die Rollen Thieving, Spying als auch Archer aus.

  2. Abfrage von Instanzen
    if performs(my_elf, Thieving):
      ...
    

    Das zweite Argument für performs() kann auch alles sein, was eine __contains__()-Methode hat, was bedeutet, dass auch Folgendes zulässig ist.

    if performs(my_elf, set([Thieving, Spying, BoyScout])):
      ...
    

    Ähnlich wie bei isinstance() muss das Objekt nur eine einzige Rolle aus der Menge erfüllen, damit der Ausdruck wahr ist.

Beziehung zu abstrakten Basisklassen

Frühe Entwürfe dieser PEP [5] sahen Rollen als Konkurrenz zu den in PEP 3119 vorgeschlagenen abstrakten Basisklassen vor. Nach weiterer Diskussion und Beratung wurde ein Kompromiss und eine Aufgabenverteilung sowie Anwendungsfälle wie folgt ausgearbeitet:

  • Rollen bieten eine Möglichkeit, die Semantik und abstrakten Fähigkeiten eines Objekts anzugeben. Eine Rolle kann abstrakte Methoden definieren, aber nur als Mittel zur Abgrenzung einer Schnittstelle, über die auf eine bestimmte Menge von Semantiken zugegriffen wird. Eine Ordering-Rolle könnte verlangen, dass eine Menge von Ordnungsoperatoren definiert wird.
    class Ordering(metaclass=Role):
      def __ge__(self, other):
        pass
    
      def __le__(self, other):
        pass
    
      def __ne__(self, other):
        pass
    
      # ...and so on
    

    Auf diese Weise können wir die Rolle oder Funktion eines Objekts innerhalb eines größeren Systems angeben, ohne uns auf eine bestimmte Implementierung einzuschränken oder uns darum zu kümmern.

  • Abstrakte Basisklassen hingegen sind eine Möglichkeit, gemeinsame, diskrete Implementierungseinheiten wiederzuverwenden. Man könnte zum Beispiel einen OrderingMixin definieren, der mehrere Ordnungsoperatoren auf der Grundlage anderer Operatoren implementiert.
    class OrderingMixin:
      def __ge__(self, other):
        return self > other or self == other
    
      def __le__(self, other):
        return self < other or self == other
    
      def __ne__(self, other):
        return not self == other
    
      # ...and so on
    

    Die Verwendung dieser abstrakten Basisklasse – genauer gesagt eines konkreten Mixins – ermöglicht es einem Programmierer, eine begrenzte Menge von Operatoren zu definieren und dem Mixin zu erlauben, die anderen quasi „abzuleiten“.

Durch die Kombination dieser beiden orthogonalen Systeme sind wir in der Lage, a) Funktionalität bereitzustellen und b) Konsumentensysteme auf die Präsenz und Verfügbarkeit dieser Funktionalität aufmerksam zu machen. Da beispielsweise die obige Klasse OrderingMixin die in der Rolle Ordering ausgedrückte Schnittstelle und Semantik erfüllt, sagen wir, dass der Mixin die Rolle ausführt.

@perform_role(Ordering)
class OrderingMixin:
  def __ge__(self, other):
    return self > other or self == other

  def __le__(self, other):
    return self < other or self == other

  def __ne__(self, other):
    return not self == other

  # ...and so on

Nun wird jede Klasse, die den Mixin verwendet, automatisch – d. h. ohne weitere Programmiererleistung – als Ausführende der Rolle Ordering gekennzeichnet.

Die Trennung von Zuständigkeiten in zwei unterschiedliche, orthogonale Systeme ist wünschenswert, da sie es uns ermöglicht, jedes davon separat zu verwenden. Nehmen wir zum Beispiel ein Drittanbieterpaket, das eine Rolle RecursiveHash bereitstellt, die angibt, dass ein Container seine Inhalte bei der Bestimmung seines Hash-Werts berücksichtigt. Da die integrierten Klassen tuple und frozenset von Python dieser Semantik folgen, kann die Rolle RecursiveHash auf sie angewendet werden.

>>> perform_role(RecursiveHash)(tuple)
>>> perform_role(RecursiveHash)(frozenset)

Nun kann jeder Code, der RecursiveHash-Objekte konsumiert, auch Tupel und Frozensets konsumieren.

Offene Fragen

Instanzen andere Rollen als ihre Klasse ausführen lassen

Perl 6 erlaubt es Instanzen, andere Rollen als ihre Klasse auszuführen. Diese Änderungen sind auf die einzelne Instanz beschränkt und wirken sich nicht auf andere Instanzen der Klasse aus. Zum Beispiel:

my_elf = Elf()
my_elf.goes_on_quest()
my_elf.becomes_evil()
now_performs(my_elf, Thieving) # Only this one elf is a thief
my_elf.steals(["purses", "candy", "kisses"])

In Perl 6 geschieht dies durch die Erstellung einer anonymen Klasse, die vom ursprünglichen Elternteil der Instanz erbt und die zusätzlichen Rollen ausführt. Dies ist in Python 3 möglich, ob es wünschenswert ist, ist jedoch eine andere Frage.

Die Aufnahme dieser Funktion würde es natürlich wesentlich einfacher machen, die Werke von Charles Dickens in Python auszudrücken.

>>> from literature import role, BildungsRoman
>>> from dickens import Urchin, Gentleman
>>>
>>> with BildungsRoman() as OliverTwist:
...   mr_brownlow = Gentleman()
...   oliver, artful_dodger = Urchin(), Urchin()
...   now_performs(artful_dodger, [role.Thief, role.Scoundrel])
...
...   oliver.has_adventures_with(ArtfulDodger)
...   mr_brownlow.adopt_orphan(oliver)
...   now_performs(oliver, role.RichWard)

Attribute erfordern

Neal Norwitz hat die Möglichkeit gefordert, Aussagen über die Anwesenheit von Attributen mit demselben Mechanismus zu machen, der zum Erfordern von Methoden verwendet wird. Da Rollen zur Zeit der Klassendefinition wirksam werden und da die überwiegende Mehrheit der Attribute zur Laufzeit durch die __init__()-Methode einer Klasse definiert wird, scheint es keine gute Möglichkeit zu geben, Attribute gleichzeitig mit Methoden zu überprüfen.

Es kann dennoch wünschenswert sein, nicht erzwungene Attribute in die Rollendefinition aufzunehmen, und sei es nur zu Dokumentationszwecken.

Rollen von Rollen

Unter den vorgeschlagenen Semantiken ist es möglich, dass Rollen eigene Rollen haben.

@perform_role(Y)
class X(metaclass=Role):
  ...

Dies ist zwar möglich, aber bedeutungslos, da Rollen im Allgemeinen nicht instanziiert werden. Es gab einige Offline-Diskussionen darüber, diesem Ausdruck Bedeutung zu verleihen, aber bisher sind keine guten Ideen aufgetaucht.

class_performs()

Es ist derzeit nicht möglich, eine Klasse zu fragen, ob ihre Instanzen eine bestimmte Rolle ausführen. Es kann wünschenswert sein, ein Analogon zu performs() bereitzustellen, so dass

>>> isinstance(my_dwarf, Dwarf)
True
>>> performs(my_dwarf, Surly)
True
>>> performs(Dwarf, Surly)
False
>>> class_performs(Dwarf, Surly)
True

Schönere dynamische Rollenzuweisung

Ein früher Entwurf dieser PEP enthielt einen separaten Mechanismus zur dynamischen Zuweisung einer Rolle zu einer Klasse. Dies wurde geschrieben

>>> now_perform(Dwarf, GoldMiner)

Diese Funktionalität existiert bereits durch das Entpacken der durch Dekoratoren bereitgestellten syntaktischen Zuckerung.

>>> perform_role(GoldMiner)(Dwarf)

Die Frage ist, ob dynamische Rollenzuweisung wichtig genug ist, um eine dedizierte Schreibweise zu rechtfertigen.

Syntaxunterstützung

Obwohl die in dieser PEP dargelegten Formulierungen so konzipiert sind, dass das Rollensystem als eigenständiges Paket ausgeliefert werden kann, kann es wünschenswert sein, spezielle Syntax für die Definition, Zuweisung und Abfrage von Rollen hinzuzufügen. Ein Beispiel könnte ein Schlüsselwort role sein, das übersetzt wird.

class MyRole(metaclass=Role):
  ...

in

role MyRole:
  ...

Die Zuweisung einer Rolle könnte die in PEP 3115 vorgeschlagenen Argumente für die Klassendefinition nutzen.

class MyClass(performs=MyRole):
  ...

Implementierung

Eine Referenzimplementierung wird folgen.

Danksagungen

Vielen Dank an Jeffery Yasskin, Talin und Guido van Rossum für mehrere Stunden persönlicher Diskussionen zur Klärung der Unterschiede, Überschneidungen und Feinheiten von Rollen und abstrakten Basisklassen.

Referenzen


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

Zuletzt geändert: 2025-02-01 08:59:27 GMT