PEP 335 – Überladbare boolesche Operatoren
- Autor:
- Gregory Ewing <greg.ewing at canterbury.ac.nz>
- Status:
- Abgelehnt
- Typ:
- Standards Track
- Erstellt:
- 29-Aug-2004
- Python-Version:
- 3.3
- Post-History:
- 05-Sep-2004, 30-Sep-2011, 25-Oct-2011
Ablehnungsbescheid
Diese PEP wurde abgelehnt. Siehe https://mail.python.org/pipermail/python-dev/2012-March/117510.html
Zusammenfassung
Diese PEP schlägt eine Erweiterung vor, um Objekten zu erlauben, ihre eigene Bedeutung für die booleschen Operatoren „and“, „or“ und „not“ zu definieren, und schlägt eine effiziente Strategie für die Implementierung vor. Ein Prototyp dieser Implementierung steht zum Download bereit.
Hintergrund
Python bietet derzeit keine speziellen Methoden vom Typ „__xxx__“ an, die den booleschen Operatoren „and“, „or“ und „not“ entsprechen. Im Fall von „and“ und „or“ liegt der wahrscheinlichste Grund darin, dass diese Operatoren Short-Circuiting-Semantiken haben, d. h. der zweite Operand wird nicht ausgewertet, wenn das Ergebnis aus dem ersten Operanden bestimmt werden kann. Die übliche Technik, spezielle Methoden für diese Operatoren bereitzustellen, würde daher nicht funktionieren.
Im Fall von „not“ gibt es jedoch keine solche Schwierigkeit, und es wäre einfach, eine spezielle Methode für diesen Operator bereitzustellen. Der Rest dieses Vorschlags wird sich daher hauptsächlich auf die Bereitstellung einer Möglichkeit zur Überladung von „and“ und „or“ konzentrieren.
Motivation
Es gibt viele Anwendungen, in denen es natürlich ist, benutzerdefinierte Bedeutungen für Python-Operatoren bereitzustellen, und in einigen davon kann es unbequem sein, dass boolesche Operatoren von den anpassbaren ausgeschlossen sind. Beispiele hierfür sind:
- NumPy, bei dem fast alle Operatoren für Arrays definiert sind, um die entsprechende Operation zwischen den einzelnen Elementen durchzuführen und ein Array der Ergebnisse zurückzugeben. Aus Konsistenzgründen würde man erwarten, dass eine boolesche Operation zwischen zwei Arrays ein Array von Booleans zurückgibt, was derzeit jedoch nicht möglich ist.
Es gibt einen Präzedenzfall für eine Erweiterung dieser Art: Vergleichsoperatoren waren ursprünglich auf die Rückgabe boolescher Ergebnisse beschränkt, und „rich comparisons“ wurden hinzugefügt, damit Vergleiche von NumPy-Arrays Arrays von Booleans zurückgeben konnten.
- Ein symbolisches algebraisches System, bei dem ein Python-Ausdruck in einer Umgebung ausgewertet wird, die zur Konstruktion eines Objektausschnitts führt, der der Struktur des Ausdrucks entspricht.
- Eine relationale Datenbank-Schnittstelle, bei der ein Python-Ausdruck verwendet wird, um eine SQL-Abfrage zu konstruieren.
Eine häufig vorgeschlagene Workaround ist die Verwendung der bitweisen Operatoren „&“, „|“ und „~“ anstelle von „and“, „or“ und „not“, dies hat jedoch einige Nachteile:
- Die Priorität dieser Operatoren ist im Verhältnis zu anderen Operatoren unterschiedlich, und sie können bereits für andere Zwecke verwendet werden (wie in Beispiel 1).
- Es ist ästhetisch unbefriedigend, Benutzer zu zwingen, etwas anderes als die offensichtlichste Syntax für das zu verwenden, was sie ausdrücken wollen. Dies wäre besonders akut im Fall von Beispiel 3, da boolesche Operationen ein Grundnahrungsmittel von SQL-Abfragen sind.
- Bitweise Operatoren bieten keine Lösung für das Problem verketteter Vergleiche wie „a < b < c“, die eine implizite „and“-Operation beinhalten. Solche Ausdrücke können derzeit bei Datentypen wie NumPy-Arrays überhaupt nicht verwendet werden, bei denen das Ergebnis eines Vergleichs nicht als normale boolesche Semantik behandelt werden kann; sie müssen in etwas wie (a < b) & (b < c) erweitert werden, was erheblich an Klarheit verliert.
Begründung
Die Anforderungen an eine erfolgreiche Lösung für das Problem der Anpassbarkeit boolescher Operatoren sind:
- Im Standardfall (wo keine Anpassung erfolgt) müssen die bestehenden Short-Circuiting-Semantiken beibehalten werden.
- Es darf keine nennenswerte Geschwindigkeitsverringerung im Standardfall geben.
- Idealerweise sollte der Anpassungsmechanismus dem Objekt erlauben, nach eigenem Ermessen entweder Short-Circuiting- oder Non-Short-Circuiting-Semantiken bereitzustellen.
Eine offensichtliche, bereits vorgeschlagene Strategie ist, der speziellen Methode das erste Argument und eine Funktion zur Auswertung des zweiten Arguments zu übergeben. Dies würde Anforderung 1 und 3 erfüllen, aber nicht Anforderung 2, da dies den Overhead der Erstellung eines Funktions-Objekts und möglicherweise eines Python-Funktionsaufrufs bei jeder booleschen Operation verursachen würde. Daher wird dies hier nicht weiter betrachtet.
Der folgende Abschnitt schlägt eine Strategie vor, die alle drei Anforderungen erfüllt. Ein Prototyp-Implementierung dieser Strategie steht zum Download bereit.
Spezifikation
Spezielle Methoden
Auf Python-Ebene können Objekte die folgenden speziellen Methoden definieren.
| Unary | Binär, Phase 1 | Binär, Phase 2 |
|---|---|---|
|
|
|
Die Methode __not__, falls definiert, implementiert den Operator 'not'. Wenn sie nicht definiert ist oder NotImplemented zurückgibt, werden die bestehenden Semantiken verwendet.
Um Short-Circuiting zu ermöglichen, wird die Verarbeitung der Operatoren 'and' und 'or' in zwei Phasen unterteilt. Phase 1 tritt nach der Auswertung des ersten Operanden, aber vor dem zweiten, auf. Wenn der erste Operand die relevante Phase-1-Methode definiert, wird diese mit dem ersten Operanden als Argument aufgerufen. Wenn diese Methode das Ergebnis bestimmen kann, ohne den zweiten Operanden zu benötigen, gibt sie das Ergebnis zurück und die weitere Verarbeitung wird übersprungen.
Wenn die Phase-1-Methode feststellt, dass der zweite Operand benötigt wird, gibt sie den speziellen Wert NeedOtherOperand zurück. Dies löst die Auswertung des zweiten Operanden und den Aufruf einer relevanten Phase-2-Methode aus. Während Phase 2 funktionieren die Methodenpaare __and2__/__rand2__ und __or2__/__ror2__ wie bei anderen binären Operatoren.
Die Verarbeitung greift auf bestehende Semantiken zurück, wenn zu irgendeinem Zeitpunkt eine relevante spezielle Methode nicht gefunden wird oder NotImplemented zurückgibt.
Als Sonderfall: Wenn der erste Operand eine Phase-2-Methode definiert, aber keine entsprechende Phase-1-Methode, wird der zweite Operand immer ausgewertet und die Phase-2-Methode aufgerufen. Dies ermöglicht es einem Objekt, das keine Short-Circuiting-Semantiken wünscht, einfach die Phase-2-Methoden zu implementieren und Phase 1 zu ignorieren.
Bytecodes
Der Patch fügt vier neue Bytecodes hinzu: LOGICAL_AND_1, LOGICAL_AND_2, LOGICAL_OR_1 und LOGICAL_OR_2. Als Beispiel für ihre Verwendung sieht der für einen 'and'-Ausdruck generierte Bytecode wie folgt aus:
.
.
.
evaluate first operand
LOGICAL_AND_1 L
evaluate second operand
LOGICAL_AND_2
L: .
.
.
Der LOGICAL_AND_1-Bytecode führt die Verarbeitung der Phase 1 durch. Wenn er feststellt, dass der zweite Operand benötigt wird, belässt er den ersten Operanden auf dem Stack und fährt mit dem folgenden Code fort. Andernfalls entfernt er den ersten Operanden vom Stack, pusht das Ergebnis und springt zu L.
Der LOGICAL_AND_2-Bytecode führt die Verarbeitung der Phase 2 durch, entfernt beide Operanden vom Stack und pusht das Ergebnis.
Typ-Slots
Auf C-Ebene manifestieren sich die neuen speziellen Methoden als fünf neue Slots im Typobjekt. Im Patch werden sie der tp_as_number-Teilstruktur hinzugefügt, da dies die Verwendung einiger bestehender Code zur Behandlung von unären und binären Operatoren ermöglicht. Ihre Existenz wird durch ein neues Typflag, Py_TPFLAGS_HAVE_BOOLEAN_OVERLOAD, signalisiert.
Die neuen Typ-Slots sind:
unaryfunc nb_logical_not;
unaryfunc nb_logical_and_1;
unaryfunc nb_logical_or_1;
binaryfunc nb_logical_and_2;
binaryfunc nb_logical_or_2;
Python/C API-Funktionen
Es gibt auch fünf neue Python/C API-Funktionen, die den neuen Operationen entsprechen.
PyObject *PyObject_LogicalNot(PyObject *);
PyObject *PyObject_LogicalAnd1(PyObject *);
PyObject *PyObject_LogicalOr1(PyObject *);
PyObject *PyObject_LogicalAnd2(PyObject *, PyObject *);
PyObject *PyObject_LogicalOr2(PyObject *, PyObject *);
Alternativen und Optimierungen
Dieser Abschnitt erörtert einige mögliche Variationen des Vorschlags und Wege, wie die für boolesche Ausdrücke generierten Bytecode-Sequenzen optimiert werden könnten.
Reduzierter Satz spezieller Methoden
Der Vollständigkeit halber enthält die vollständige Version dieses Vorschlags einen Mechanismus, mit dem Typen ihr eigenes angepasstes Short-Circuiting-Verhalten definieren können. Der vollständige Mechanismus ist jedoch nicht erforderlich, um die hier dargelegten Hauptanwendungsfälle zu behandeln, und es wäre möglich, eine vereinfachte Version zu definieren, die nur die Phase-2-Methoden enthält. Es gäbe dann nur 5 neue spezielle Methoden (__and2__, __rand2__, __or2__, __ror2__, __not__) mit 3 zugehörigen Typ-Slots und 3 API-Funktionen.
Diese vereinfachte Version könnte bei Bedarf später zur vollständigen Version erweitert werden.
Zusätzliche Bytecodes
Wie hier definiert, wäre die Bytecode-Sequenz für Code, der auf dem Ergebnis eines booleschen Ausdrucks verzweigt, etwas länger als derzeit. Zum Beispiel in Python 2.7:
if a and b:
statement1
else:
statement2
generiert
LOAD_GLOBAL a
POP_JUMP_IF_FALSE false_branch
LOAD_GLOBAL b
POP_JUMP_IF_FALSE false_branch
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
Unter diesem Vorschlag, wie bisher beschrieben, würde dies etwa so aussehen:
LOAD_GLOBAL a
LOGICAL_AND_1 test
LOAD_GLOBAL b
LOGICAL_AND_2
test:
POP_JUMP_IF_FALSE false_branch
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
Dies beinhaltet die Ausführung eines zusätzlichen Bytecodes im Short-Circuiting-Fall und zwei zusätzlichen Bytecodes im Non-Short-Circuiting-Fall.
Durch die Einführung zusätzlicher Bytecodes, die die logischen Operationen mit dem Testen und Verzweigen des Ergebnisses kombinieren, kann dies jedoch auf die gleiche Anzahl von Bytecodes wie das Original reduziert werden.
LOAD_GLOBAL a
AND1_JUMP true_branch, false_branch
LOAD_GLOBAL b
AND2_JUMP_IF_FALSE false_branch
true_branch:
<code for statement1>
JUMP_FORWARD end_branch
false_branch:
<code for statement2>
end_branch:
Hier führt AND1_JUMP die Verarbeitung der Phase 1 wie oben durch und prüft dann das Ergebnis. Wenn ein Ergebnis vorhanden ist, wird es vom Stack entfernt, sein Wahrheitswert getestet und zu einer von zwei Stellen verzweigt.
Andernfalls bleibt der erste Operand auf dem Stack und die Ausführung wird mit dem nächsten Bytecode fortgesetzt. Der AND2_JUMP_IF_FALSE-Bytecode führt die Verarbeitung der Phase 2 durch, entfernt das Ergebnis vom Stack und verzweigt, wenn es falsch ist.
Für den Operator 'or' gäbe es entsprechende OR1_JUMP- und OR2_JUMP_IF_TRUE-Bytecodes.
Wenn die vereinfachte Version ohne Phase-1-Methoden verwendet wird, kann ein früher Ausstieg nur erfolgen, wenn der erste Operand für 'and' falsch und für 'or' wahr ist. Folglich können die Zwei-Ziel-Bytecodes AND1_JUMP und OR1_JUMP durch AND1_JUMP_IF_FALSE und OR1_JUMP_IF_TRUE ersetzt werden, die gewöhnliche Verzweigungsanweisungen mit nur einem Ziel sind.
Optimierung von „not“
Neuere Versionen von Python implementieren eine einfache Optimierung, bei der das Verzweigen auf einem negierten booleschen Ausdruck durch Umkehrung der Verzweigungsrichtung implementiert wird, wodurch ein UNARY_NOT-Opcode eingespart wird.
Streng genommen sollte diese Optimierung nicht mehr durchgeführt werden, da der Operator 'not' überschrieben werden kann, um ganz andere Ergebnisse als üblich zu erzielen. In typischen Anwendungsfällen wird jedoch nicht erwartet, dass Ausdrücke mit angepassten booleschen Operationen zum Verzweigen verwendet werden – es ist viel wahrscheinlicher, dass das Ergebnis auf andere Weise verwendet wird.
Daher würde es wahrscheinlich wenig Schaden anrichten, zu spezifizieren, dass der Compiler die Gesetze der Booleschen Algebra verwenden darf, um jeden Ausdruck zu vereinfachen, der direkt in einem booleschen Kontext erscheint. Wenn dies umständlich ist, kann das Ergebnis immer zuerst einer temporären Variablen zugewiesen werden.
Dies würde es ermöglichen, die bestehende 'not'-Optimierung beizubehalten, und zukünftige Erweiterungen wie die Verwendung der De Morganschen Gesetze zur tieferen Einbettung in den Ausdruck ermöglichen.
Anwendungsbeispiele
Beispiel 1: NumPy-Arrays
#-----------------------------------------------------------------
#
# This example creates a subclass of numpy array to which
# 'and', 'or' and 'not' can be applied, producing an array
# of booleans.
#
#-----------------------------------------------------------------
from numpy import array, ndarray
class BArray(ndarray):
def __str__(self):
return "barray(%s)" % ndarray.__str__(self)
def __and2__(self, other):
return (self & other)
def __or2__(self, other):
return (self & other)
def __not__(self):
return (self == 0)
def barray(*args, **kwds):
return array(*args, **kwds).view(type = BArray)
a0 = barray([0, 1, 2, 4])
a1 = barray([1, 2, 3, 4])
a2 = barray([5, 6, 3, 4])
a3 = barray([5, 1, 2, 4])
print "a0:", a0
print "a1:", a1
print "a2:", a2
print "a3:", a3
print "not a0:", not a0
print "a0 == a1 and a2 == a3:", a0 == a1 and a2 == a3
print "a0 == a1 or a2 == a3:", a0 == a1 or a2 == a3
Ausgabe von Beispiel 1
a0: barray([0 1 2 4])
a1: barray([1 2 3 4])
a2: barray([5 6 3 4])
a3: barray([5 1 2 4])
not a0: barray([ True False False False])
a0 == a1 and a2 == a3: barray([False False False True])
a0 == a1 or a2 == a3: barray([False False False True])
Beispiel 2: Datenbankabfragen
#-----------------------------------------------------------------
#
# This example demonstrates the creation of a DSL for database
# queries allowing 'and' and 'or' operators to be used to
# formulate the query.
#
#-----------------------------------------------------------------
class SQLNode(object):
def __and2__(self, other):
return SQLBinop("and", self, other)
def __rand2__(self, other):
return SQLBinop("and", other, self)
def __eq__(self, other):
return SQLBinop("=", self, other)
class Table(SQLNode):
def __init__(self, name):
self.__tablename__ = name
def __getattr__(self, name):
return SQLAttr(self, name)
def __sql__(self):
return self.__tablename__
class SQLBinop(SQLNode):
def __init__(self, op, opnd1, opnd2):
self.op = op.upper()
self.opnd1 = opnd1
self.opnd2 = opnd2
def __sql__(self):
return "(%s %s %s)" % (sql(self.opnd1), self.op, sql(self.opnd2))
class SQLAttr(SQLNode):
def __init__(self, table, name):
self.table = table
self.name = name
def __sql__(self):
return "%s.%s" % (sql(self.table), self.name)
class SQLSelect(SQLNode):
def __init__(self, targets):
self.targets = targets
self.where_clause = None
def where(self, expr):
self.where_clause = expr
return self
def __sql__(self):
result = "SELECT %s" % ", ".join([sql(target) for target in self.targets])
if self.where_clause:
result = "%s WHERE %s" % (result, sql(self.where_clause))
return result
def sql(expr):
if isinstance(expr, SQLNode):
return expr.__sql__()
elif isinstance(expr, str):
return "'%s'" % expr.replace("'", "''")
else:
return str(expr)
def select(*targets):
return SQLSelect(targets)
#-----------------------------------------------------------------
dishes = Table("dishes")
customers = Table("customers")
orders = Table("orders")
query = select(customers.name, dishes.price, orders.amount).where(
customers.cust_id == orders.cust_id and orders.dish_id == dishes.dish_id
and dishes.name == "Spam, Eggs, Sausages and Spam")
print repr(query)
print sql(query)
Ausgabe von Beispiel 2
<__main__.SQLSelect object at 0x1cc830>
SELECT customers.name, dishes.price, orders.amount WHERE
(((customers.cust_id = orders.cust_id) AND (orders.dish_id =
dishes.dish_id)) AND (dishes.name = 'Spam, Eggs, Sausages and Spam'))
Urheberrecht
Dieses Dokument wurde gemeinfrei erklärt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0335.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT