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

Python Enhancement Proposals

PEP 465 – Ein dedizierter Infix-Operator für Matrixmultiplikation

Autor:
Nathaniel J. Smith <njs at pobox.com>
Status:
Final
Typ:
Standards Track
Erstellt:
20. Feb 2014
Python-Version:
3.5
Post-History:
13. Mär 2014
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

Diese PEP schlägt einen neuen binären Operator vor, der für die Matrixmultiplikation verwendet werden soll und @ genannt wird. (Eselsbrücke: @ ist * für mATrizen.)

Spezifikation

Ein neuer binärer Operator wird zur Python-Sprache hinzugefügt, zusammen mit der entsprechenden In-Place-Version

Op Rangfolge/Assoziativität Methoden
@ Gleich wie * __matmul__, __rmatmul__
@= n. z. z. __imatmul__

Keine Implementierungen dieser Methoden werden den eingebauten oder Standardbibliotheks-Typen hinzugefügt. Eine Reihe von Projekten hat sich jedoch auf die empfohlenen Semantiken für diese Operationen geeinigt; siehe Details zur beabsichtigten Nutzung unten für Einzelheiten.

Einzelheiten zur Implementierung dieses Operators in CPython finden Sie unter Implementierungsdetails.

Motivation

Zusammenfassung

In numerischem Code konkurrieren zwei wichtige Operationen um die Nutzung des Python-Operators *: elementweise Multiplikation und Matrixmultiplikation. In den fast zwanzig Jahren seit der ersten Einführung der Numeric-Bibliothek gab es viele Versuche, diese Spannung zu lösen [13]; keiner war wirklich zufriedenstellend. Derzeit verwendet der meiste numerische Python-Code * für elementweise Multiplikation und Funktions-/Methodensyntax für Matrixmultiplikation; dies führt jedoch unter normalen Umständen zu unschönem und unleserlichem Code. Das Problem ist so schlimm, dass signifikante Mengen an Code weiterhin die entgegengesetzte Konvention verwenden (was den Vorteil hat, dass es unter *anderen* Umständen zu unschönem und unleserlichem Code führt), und diese API-Fragmentierung über Codebasen hinweg verursacht dann noch mehr Probleme. Es scheint keine *gute* Lösung für das Problem der Gestaltung einer numerischen API innerhalb der aktuellen Python-Syntax zu geben – nur eine Landschaft von Optionen, die auf unterschiedliche Weise schlecht sind. Die minimale Änderung der Python-Syntax, die ausreicht, um diese Probleme zu lösen, ist die Hinzufügung eines einzigen neuen Infix-Operators für die Matrixmultiplikation.

Matrixmultiplikation hat eine einzigartige Kombination von Merkmalen, die sie von anderen binären Operationen unterscheidet und zusammen einen einzigartig überzeugenden Fall für die Hinzufügung eines dedizierten Infix-Operators darstellt

  • Genau wie bei den bestehenden numerischen Operatoren gibt es eine riesige Menge an Vorarbeit, die die Verwendung von Infix-Notation für Matrixmultiplikation in allen Bereichen der Mathematik, Wissenschaft und Technik unterstützt; @ füllt harmonisch eine Lücke im bestehenden Operator-System von Python.
  • @ klärt den realen Code erheblich.
  • @ bietet eine sanftere Einstiegsrampe für weniger erfahrene Benutzer, die besonders unter schwer lesbarem Code und API-Fragmentierung leiden.
  • @ kommt einem erheblichen und wachsenden Teil der Python-Benutzergemeinschaft zugute.
  • @ wird häufig verwendet werden – tatsächlich deuten Beweise darauf hin, dass es häufiger verwendet werden könnte als // oder die bitweisen Operatoren.
  • @ ermöglicht es der Python-Numerik-Community, die Fragmentierung zu reduzieren und endlich einen einzigen Konsens-Duck-Typ für alle numerischen Array-Objekte zu standardisieren.

Hintergrund: Was ist am Status Quo falsch?

Wenn wir Zahlen auf einem Computer verarbeiten, haben wir normalerweise viele, viele Zahlen zu verarbeiten. Der Versuch, sie einzeln zu verarbeiten, ist umständlich und langsam – besonders in einer interpretierten Sprache. Stattdessen wollen wir die Möglichkeit haben, einfache Operationen aufzuschreiben, die auf große Sammlungen von Zahlen gleichzeitig angewendet werden. Das *n-dimensionale Array* ist das grundlegende Objekt, das alle beliebten numerischen Rechenumgebungen verwenden, um dies zu ermöglichen. Python verfügt über mehrere Bibliotheken, die solche Arrays bereitstellen, wobei numpy derzeit die prominenteste ist.

Beim Arbeiten mit n-dimensionalen Arrays gibt es zwei verschiedene Möglichkeiten, wie wir Multiplikation definieren möchten. Eine ist die elementweise Multiplikation

[[1, 2],     [[11, 12],     [[1 * 11, 2 * 12],
 [3, 4]]  x   [13, 14]]  =   [3 * 13, 4 * 14]]

und die andere ist die Matrixmultiplikation

[[1, 2],     [[11, 12],     [[1 * 11 + 2 * 13, 1 * 12 + 2 * 14],
 [3, 4]]  x   [13, 14]]  =   [3 * 11 + 4 * 13, 3 * 12 + 4 * 14]]

Elementweise Multiplikation ist nützlich, weil sie es uns ermöglicht, viele Multiplikationen einfach und schnell auf einer großen Sammlung von Werten durchzuführen, ohne eine langsame und umständliche for Schleife schreiben zu müssen. Und das funktioniert als Teil eines sehr allgemeinen Schemas: Bei der Verwendung der von numpy oder anderen numerischen Bibliotheken bereitgestellten Array-Objekte funktionieren alle Python-Operatoren elementweise auf Arrays aller Dimensionalitäten. Das Ergebnis ist, dass man Funktionen mit einfachem Code wie a * b + c / d schreiben kann, die Variablen so behandelt, als wären sie einfache Werte, und diese Funktion dann sofort verwenden kann, um diese Berechnung effizient auf große Sammlungen von Werten durchzuführen, während sie diese mit jedem beliebigen, komplexen Array-Layout, das für das jeweilige Problem am besten geeignet ist, organisiert hält.

Matrixmultiplikation ist eher ein Sonderfall. Sie ist nur für 2D-Arrays (auch "Matrizen" genannt) definiert, und die Multiplikation ist die einzige Operation, die eine wichtige "Matrix"-Version hat – "Matrixaddition" ist dasselbe wie elementweise Addition; es gibt keine "Matrix-Bitweise-ODER" oder "Matrix-Floor-Division"; "Matrix-Division" und "Matrix-Potenz" können definiert werden, sind aber nicht sehr nützlich usw. Dennoch wird Matrixmultiplikation in allen numerischen Anwendungsbereichen sehr stark genutzt; mathematisch gesehen ist sie eine der grundlegendsten Operationen überhaupt.

Da die Python-Syntax derzeit nur einen einzigen Multiplikationsoperator * zulässt, müssen Bibliotheken, die Array-ähnliche Objekte bereitstellen, entscheiden: entweder * für elementweise Multiplikation verwenden oder * für Matrixmultiplikation verwenden. Und leider stellt sich heraus, dass beim allgemeinen Zahlenrechnen beide Operationen häufig verwendet werden und es erhebliche Vorteile hat, Infix- statt Funktionsaufrufsyntax in beiden Fällen zu verwenden. Daher ist es überhaupt nicht klar, welche Konvention optimal oder auch nur akzeptabel ist; oft variiert dies von Fall zu Fall.

Dennoch bedeutet Netzwerkeffekte, dass es sehr wichtig ist, dass wir *nur eine* Konvention wählen. In numpy ist es beispielsweise technisch möglich, zwischen den Konventionen zu wechseln, da numpy zwei verschiedene Typen mit unterschiedlichen __mul__ Methoden bereitstellt. Für numpy.ndarray Objekte führt * eine elementweise Multiplikation durch, und Matrixmultiplikation muss einen Funktionsaufruf (numpy.dot) verwenden. Für numpy.matrix Objekte führt * eine Matrixmultiplikation durch, und elementweise Multiplikation erfordert Funktionssyntax. Code, der numpy.ndarray verwendet, funktioniert gut. Code, der numpy.matrix verwendet, funktioniert ebenfalls gut. Aber Probleme treten auf, sobald wir versuchen, diese beiden Code-Teile zu integrieren. Code, der einen ndarray erwartet und einen matrix erhält, oder umgekehrt, kann abstürzen oder falsche Ergebnisse liefern. Die Verfolgung, welche Funktionen welche Typen als Eingaben erwarten und welche Typen als Ausgaben zurückgeben, und dann ständiges Hin- und Herkonvertieren ist unglaublich umständlich und unmöglich, um es in irgendeiner Größenordnung richtig zu machen. Funktionen, die defensiv versuchen, beide Typen als Eingabe zu behandeln und DTRT (Do The Right Thing), finden sich in einem Sumpf aus isinstance und if-Anweisungen wieder.

PEP 238 hat / in zwei Operatoren aufgeteilt: / und //. Stellen Sie sich das Chaos vor, das entstanden wäre, wenn stattdessen int in zwei Typen aufgeteilt worden wäre: classic_int, dessen __div__ die Ganzzahldivision implementierte, und new_int, dessen __div__ die wahre Division implementierte. Dies ist, in begrenzter Weise, die Situation, in der sich Python-Zahlenjongleure derzeit befinden.

In der Praxis hat sich die überwiegende Mehrheit der Projekte auf die Konvention geeinigt, * für elementweise Multiplikation und Funktionsaufrufsyntax für Matrixmultiplikation zu verwenden (z. B. unter Verwendung von numpy.ndarray anstelle von numpy.matrix). Dies reduziert die Probleme, die durch API-Fragmentierung entstehen, beseitigt sie aber nicht. Der starke Wunsch, Infix-Notation für Matrixmultiplikation zu verwenden, hat dazu geführt, dass eine Reihe von spezialisierten Array-Bibliotheken weiterhin die entgegengesetzte Konvention verwenden (z. B. scipy.sparse, pyoperators, pyviennacl), trotz der Probleme, die dies verursacht, und numpy.matrix selbst wird immer noch in Einführungskursen verwendet, erscheint oft in StackOverflow-Antworten und so weiter. Gut geschriebene Bibliotheken müssen daher weiterhin auf beide Arten von Objekten vorbereitet sein und sind natürlich auch an unangenehme Funktionsaufrufsyntax für die Matrixmultiplikation gebunden. Nach fast zwei Jahrzehnten des Versuchens hat die numerische Community immer noch keinen Weg gefunden, diese Probleme im Rahmen der aktuellen Python-Syntax zu lösen (siehe Abgelehnte Alternativen zur Hinzufügung eines neuen Operators unten).

Diese PEP schlägt die minimale effektive Änderung der Python-Syntax vor, die es uns ermöglicht, diesen Sumpf zu entwässern. Sie teilt * in zwei Operatoren auf, genau wie es für / geschehen ist: * für elementweise Multiplikation und @ für Matrixmultiplikation. (Warum nicht umgekehrt? Weil dieser Weg mit dem bestehenden Konsens kompatibel ist und weil er uns eine konsistente Regel gibt, dass alle eingebauten numerischen Operatoren auch elementweise auf Arrays angewendet werden; die umgekehrte Konvention würde zu mehr Sonderfällen führen.)

Das ist also der Grund, warum Matrixmultiplikation nicht einfach * verwendet und auch nicht kann. Nun werden wir im Rest dieses Abschnitts erklären, warum sie dennoch die hohe Hürde für die Hinzufügung eines neuen Operators erfüllt.

Warum sollte Matrixmultiplikation Infix sein?

Derzeit verwendet der meiste numerische Code in Python Syntax wie numpy.dot(a, b) oder a.dot(b) zur Durchführung von Matrixmultiplikation. Das funktioniert offensichtlich, also warum machen die Leute so ein Aufhebens darum, bis hin zur Schaffung von API-Fragmentierung und Kompatibilitätssümpfen?

Matrixmultiplikation teilt zwei Merkmale mit gewöhnlichen arithmetischen Operationen wie Addition und Multiplikation von Zahlen: (a) sie wird sehr häufig in numerischen Programmen verwendet – oft mehrmals pro Zeile Code – und (b) sie hat eine alte und universell anerkannte Tradition, mit Infix-Syntax geschrieben zu werden. Das liegt daran, dass diese Notation für typische Formeln dramatisch lesbarer ist als jede Funktionsaufrufsyntax. Hier ist ein Beispiel zur Veranschaulichung

Eines der nützlichsten Werkzeuge zum Testen einer statistischen Hypothese ist der lineare Hypothesentest für OLS-Regressionsmodelle. Es spielt keine Rolle, was all diese Worte, die ich gerade gesagt habe, bedeuten; wenn wir dies implementieren müssen, schauen wir in einem Lehrbuch oder einer Veröffentlichung nach und stoßen auf viele mathematische Formeln, die so aussehen

S = (Hβ − r)T(HVHT) − 1(Hβ − r)

Hier sind die verschiedenen Variablen Vektoren oder Matrizen (Details für Neugierige: [5]).

Nun müssen wir Code schreiben, um diese Berechnung durchzuführen. Im aktuellen numpy kann Matrixmultiplikation entweder mit der Funktions- oder der Methodenaufrufsyntax durchgeführt werden. Keine davon bietet eine besonders lesbare Übersetzung der Formel

import numpy as np
from numpy.linalg import inv, solve

# Using dot function:
S = np.dot((np.dot(H, beta) - r).T,
           np.dot(inv(np.dot(np.dot(H, V), H.T)), np.dot(H, beta) - r))

# Using dot method:
S = (H.dot(beta) - r).T.dot(inv(H.dot(V).dot(H.T))).dot(H.dot(beta) - r)

Mit dem @-Operator wird die direkte Übersetzung der obigen Formel zu

S = (H @ beta - r).T @ inv(H @ V @ H.T) @ (H @ beta - r)

Beachten Sie, dass es nun eine transparente 1-zu-1-Abbildung zwischen den Symbolen in der ursprünglichen Formel und dem Code gibt, der sie implementiert.

Natürlich wird ein erfahrener Programmierer wahrscheinlich bemerken, dass dies nicht die beste Methode zur Berechnung dieses Ausdrucks ist. Die wiederholte Berechnung von Hβ − r sollte vielleicht ausgelagert werden; und Ausdrücke der Form dot(inv(A), B) sollten fast immer durch das numerisch stabilere solve(A, B) ersetzt werden. Bei der Verwendung von @ erhalten wir bei diesen beiden Refactorings

# Version 1 (as above)
S = (H @ beta - r).T @ inv(H @ V @ H.T) @ (H @ beta - r)

# Version 2
trans_coef = H @ beta - r
S = trans_coef.T @ inv(H @ V @ H.T) @ trans_coef

# Version 3
S = trans_coef.T @ solve(H @ V @ H.T, trans_coef)

Beachten Sie, dass es bei Vergleichen zwischen jedem Paar von Schritten sehr einfach ist, genau zu sehen, was geändert wurde. Wenn wir die äquivalenten Transformationen auf den Code anwenden, der die .dot-Methode verwendet, sind die Änderungen viel schwerer zu lesen oder auf Korrektheit zu überprüfen

# Version 1 (as above)
S = (H.dot(beta) - r).T.dot(inv(H.dot(V).dot(H.T))).dot(H.dot(beta) - r)

# Version 2
trans_coef = H.dot(beta) - r
S = trans_coef.T.dot(inv(H.dot(V).dot(H.T))).dot(trans_coef)

# Version 3
S = trans_coef.T.dot(solve(H.dot(V).dot(H.T)), trans_coef)

Lesbarkeit zählt! Die Aussagen, die @ verwenden, sind kürzer, enthalten mehr Leerzeichen, können direkt und einfach sowohl untereinander als auch mit der Lehrbuchformel verglichen werden und enthalten nur aussagekräftige Klammern. Dieser letzte Punkt ist besonders wichtig für die Lesbarkeit: Bei der Verwendung von Funktionsaufrufsyntax erzeugen die erforderlichen Klammern bei jeder Operation eine visuelle Unordnung, die es sehr schwierig macht, die Gesamtstruktur der Formel mit bloßem Auge zu erfassen, selbst für eine relativ einfache Formel wie diese. Augen sind schlecht darin, nicht-reguläre Sprachen zu parsen. Ich habe viele Fehler gemacht und gefunden, als ich versuchte, die oben genannten 'dot'-Formeln aufzuschreiben. Ich weiß, dass sie immer noch mindestens einen Fehler enthalten, vielleicht mehr. (Übung: Finden Sie ihn. Oder sie.) Die @-Beispiele sind dagegen nicht nur korrekt, sondern auf den ersten Blick offensichtlich korrekt.

Wenn wir noch ausgefeiltere Programmierer sind und Code schreiben, von dem wir erwarten, dass er wiederverwendet wird, dann könnten Überlegungen zur Geschwindigkeit oder numerischen Genauigkeit uns dazu veranlassen, eine bestimmte Reihenfolge der Auswertung zu bevorzugen. Da @ es ermöglicht, irrelevante Klammern wegzulassen, können wir sicher sein, dass, wenn wir etwas wie (H @ V) @ H.T schreiben, unsere Leser wissen werden, dass die Klammern absichtlich hinzugefügt wurden, um einen sinnvollen Zweck zu erfüllen. In den dot-Beispielen ist es unmöglich zu wissen, welche Verschachtelungsentscheidungen wichtig sind und welche willkürlich sind.

Infix @ verbessert die Benutzerfreundlichkeit von Matrixcode in allen Phasen der Programmierinteraktion dramatisch.

Transparente Syntax ist besonders wichtig für nicht-fachkundige Programmierer

Ein großer Teil wissenschaftlichen Codes wird von Leuten geschrieben, die Experten in ihrem Fachgebiet sind, aber keine Programmierer sind. Und es gibt viele Universitätskurse, die jedes Jahr mit Titeln wie „Datenanalyse für Sozialwissenschaftler“ laufen, die keine Programmierkenntnisse voraussetzen und eine Kombination aus mathematischen Techniken, Einführung in die Programmierung und der Nutzung von Programmierung zur Implementierung dieser mathematischen Techniken vermitteln, alles innerhalb von 10-15 Wochen. Diese Kurse werden immer häufiger in Python statt in spezialisierten Sprachen wie R oder Matlab unterrichtet.

Für diese Art von Benutzern, deren Programmierkenntnisse fragil sind, bedeutet die Existenz einer transparenten Abbildung zwischen Formeln und Code oft den Unterschied zwischen Erfolg und Misserfolg beim Schreiben dieses Codes. Dies ist so wichtig, dass solche Kurse oft den numpy.matrix-Typ verwenden, der * als Matrixmultiplikation definiert, obwohl dieser Typ fehlerhaft ist und vom Rest der numpy-Community wegen der verursachten Fragmentierung stark abgeraten wird. Dieser pädagogische Anwendungsfall ist tatsächlich der *einzige* Grund, warum numpy.matrix ein unterstützter Teil von numpy bleibt. Die Hinzufügung von @ wird sowohl Anfängern als auch fortgeschrittenen Benutzern mit besserer Syntax zugute kommen; und darüber hinaus wird sie beiden Gruppen ermöglichen, von Anfang an die gleiche Notation zu standardisieren, was eine sanftere Einstiegsrampe zur Expertise bietet.

Aber ist Matrixmultiplikation nicht eine ziemlich Nischenanforderung?

Die Welt ist voller kontinuierlicher Daten, und Computer werden zunehmend aufgerufen, auf ausgefeilte Weise damit zu arbeiten. Arrays sind die Lingua Franca der Finanzwelt, des maschinellen Lernens, der 3D-Grafik, der Computer Vision, der Robotik, der Operations Research, der Ökonometrie, der Meteorologie, der Computerlinguistik, der Empfehlungssysteme, der Neurowissenschaften, der Astronomie, der Bioinformatik (einschließlich Genetik, Krebsforschung, Medikamentenentwicklung usw.), Physik-Engines, Quantenmechanik, Geophysik, Netzwerkanalyse und vieler anderer Anwendungsbereiche. In den meisten oder allen diesen Bereichen wird Python schnell zu einem dominierenden Akteur, zu einem großen Teil aufgrund seiner Fähigkeit, traditionelle diskrete Datenstrukturen (Hash-Tabellen, Strings usw.) auf gleicher Ebene mit modernen numerischen Datentypen und Algorithmen elegant zu vermischen.

Wir leben alle in unseren kleinen Sub-Communitys, daher sind einige Python-Benutzer vielleicht überrascht, das Ausmaß zu erkennen, in dem Python für Zahlenjonglage verwendet wird – insbesondere, da ein Großteil der Aktivitäten dieser speziellen Sub-Community außerhalb traditioneller Python/FOSS-Kanäle stattfindet. Um also eine grobe Vorstellung davon zu geben, wie viele numerische Python-Programmierer es tatsächlich gibt, hier sind zwei Zahlen: Im Jahr 2013 fanden 7 internationale Konferenzen speziell zum Thema numerisches Python statt [3] [4]. Auf der PyCon 2014 schienen etwa 20% der Tutorials die Verwendung von Matrizen zu beinhalten [6].

Um dies weiter zu quantifizieren, haben wir die „Suche“-Funktion von Github verwendet, um zu sehen, welche Module tatsächlich in einer Vielzahl von realem Code (d. h. dem gesamten Code auf Github) importiert werden. Wir haben auf den Import mehrerer beliebter Standardbibliotheksmodule, einer Vielzahl numerisch orientierter Module und verschiedener anderer äußerst profilierter Module wie django und lxml geprüft (letzteres ist das meist heruntergeladene Paket auf PyPI). Sterne (*) zeigen Pakete an, die Array- oder Matrix-ähnliche Objekte exportieren, die @ übernehmen werden, wenn diese PEP genehmigt wird.

Count of Python source files on Github matching given search terms
                 (as of 2014-04-10, ~21:00 UTC)
================ ==========  ===============  =======  ===========
module           "import X"  "from X import"    total  total/numpy
================ ==========  ===============  =======  ===========
sys                 2374638            63301  2437939         5.85
os                  1971515            37571  2009086         4.82
re                  1294651             8358  1303009         3.12
numpy ************** 337916 ********** 79065 * 416981 ******* 1.00
warnings             298195            73150   371345         0.89
subprocess           281290            63644   344934         0.83
django                62795           219302   282097         0.68
math                 200084            81903   281987         0.68
threading            212302            45423   257725         0.62
pickle+cPickle       215349            22672   238021         0.57
matplotlib           119054            27859   146913         0.35
sqlalchemy            29842            82850   112692         0.27
pylab *************** 36754 ********** 41063 ** 77817 ******* 0.19
scipy *************** 40829 ********** 28263 ** 69092 ******* 0.17
lxml                  19026            38061    57087         0.14
zlib                  40486             6623    47109         0.11
multiprocessing       25247            19850    45097         0.11
requests              30896              560    31456         0.08
jinja2                 8057            24047    32104         0.08
twisted               13858             6404    20262         0.05
gevent                11309             8529    19838         0.05
pandas ************** 14923 *********** 4005 ** 18928 ******* 0.05
sympy                  2779             9537    12316         0.03
theano *************** 3654 *********** 1828 *** 5482 ******* 0.01
================ ==========  ===============  =======  ===========

Diese Zahlen sollten mit Vorsicht genossen werden (siehe Fußnote für eine Diskussion: [12]), aber in dem Umfang, in dem sie vertrauenswürdig sind, deuten sie darauf hin, dass numpy möglicherweise das am häufigsten importierte Nicht-Standardbibliotheksmodul im gesamten Pythonverse ist; es wird sogar häufiger importiert als etablierte Standardbibliotheksmodule wie subprocess, math, pickle und threading. Und numpy-Benutzer stellen nur einen Teil der breiteren numerischen Community dar, die von dem @-Operator profitieren wird. Matrizen mögen einst ein Nischen-Datentyp gewesen sein, der auf Fortran-Programme in Universitätslaboren und Militärclustern beschränkt war, aber diese Zeiten sind längst vorbei. Zahlenjonglage ist ein Mainstream-Teil der modernen Python-Nutzung.

Darüber hinaus gibt es einige Präzedenzfälle für die Hinzufügung eines Infix-Operators zur Handhabung einer spezialisierteren arithmetischen Operation: der Ganzzahldivisionsoperator // ist, wie die bitweisen Operatoren, unter bestimmten Umständen nützlich, wenn exakte Berechnungen mit diskreten Werten durchgeführt werden. Aber es ist wahrscheinlich, dass es viele Python-Programmierer gibt, die nie Grund hatten, // (oder auch die bitweisen Operatoren) zu verwenden. @ ist nicht mehr Nische als //.

Also ist @ gut für Matrixformeln, aber wie üblich sind diese wirklich?

Wir haben gesehen, dass @ Matrixformeln sowohl für Experten als auch für Nicht-Experten dramatisch einfacher zu handhaben macht, dass Matrixformeln in vielen wichtigen Anwendungen vorkommen und dass numerische Bibliotheken wie numpy von einem erheblichen Teil der Python-Benutzerbasis verwendet werden. Numerische Bibliotheken handeln aber nicht nur von Matrixformeln, und wichtig zu sein bedeutet nicht unbedingt, viel Code zu beanspruchen: Wenn Matrixformeln nur an ein oder zwei Stellen im durchschnittlichen numerisch orientierten Projekt vorkämen, dann wäre es immer noch nicht wert, einen neuen Operator hinzuzufügen. Wie häufig ist also Matrixmultiplikation wirklich?

Wenn es schwierig wird, werden die Harten empirisch. Um eine grobe Schätzung zu erhalten, wie nützlich der @-Operator sein wird, zeigt die folgende Tabelle die Häufigkeit, mit der verschiedene Python-Operatoren tatsächlich in der Standardbibliothek und auch in zwei hochkarätigen numerischen Paketen – der Machine-Learning-Bibliothek scikit-learn und der Neuroimaging-Bibliothek nipy – normalisiert nach Quellzeilen Code (SLOC) verwendet werden. Die Zeilen sind nach der Spalte „Kombiniert“ sortiert, die alle drei Codebasen zusammenfasst. Die kombinierte Spalte ist daher stark auf die Standardbibliothek ausgerichtet, die viel größer ist als beide Projekte zusammen (Standardbibliothek: 411575 SLOC, scikit-learn: 50924 SLOC, nipy: 37078 SLOC). [7]

Die Zeile dot (markiert mit ******) zählt, wie häufig Matrixmultiplikationsoperationen in jeder Codebasis sind.

====  ======  ============  ====  ========
  op  stdlib  scikit-learn  nipy  combined
====  ======  ============  ====  ========
   =    2969          5536  4932      3376 / 10,000 SLOC
   -     218           444   496       261
   +     224           201   348       231
  ==     177           248   334       196
   *     156           284   465       192
   %     121           114   107       119
  **      59           111   118        68
  !=      40            56    74        44
   /      18           121   183        41
   >      29            70   110        39
  +=      34            61    67        39
   <      32            62    76        38
  >=      19            17    17        18
  <=      18            27    12        18
 dot ***** 0 ********** 99 ** 74 ****** 16
   |      18             1     2        15
   &      14             0     6        12
  <<      10             1     1         8
  //       9             9     1         8
  -=       5            21    14         8
  *=       2            19    22         5
  /=       0            23    16         4
  >>       4             0     0         3
   ^       3             0     0         3
   ~       2             4     5         2
  |=       3             0     0         2
  &=       1             0     0         1
 //=       1             0     0         1
  ^=       1             0     0         0
 **=       0             2     0         0
  %=       0             0     0         0
 <<=       0             0     0         0
 >>=       0             0     0         0
====  ======  ============  ====  ========

Diese beiden numerischen Pakete allein enthalten ~780 Verwendungen von Matrixmultiplikation. Innerhalb dieser Pakete wird Matrixmultiplikation häufiger verwendet als die meisten Vergleichsoperatoren (< != <= >=). Selbst wenn wir diese Zählungen durch Einbeziehung der Standardbibliothek in unsere Vergleiche verdünnen, wird Matrixmultiplikation insgesamt immer noch häufiger verwendet als jeder der bitweisen Operatoren und doppelt so häufig wie //. Dies gilt auch dann, wenn die Standardbibliothek, die eine beträchtliche Menge an Ganzzahl-Arithmetik und keine Matrixoperationen enthält, mehr als 80% der kombinierten Codebasis ausmacht.

Zufälligerweise machen die numerischen Bibliotheken ungefähr denselben Anteil an der „kombinierten“ Codebasis aus wie numerische Tutorials am Tutorial-Plan der PyCon 2014, was darauf hindeutet, dass die „kombinierte“ Spalte möglicherweise nicht *wild* unrepräsentativ für neuen Python-Code im Allgemeinen ist. Obwohl es unmöglich ist, dies mit Sicherheit zu wissen, scheint es anhand dieser Daten durchaus möglich, dass Matrixmultiplikation in allen derzeit geschriebenen Python-Codes häufiger verwendet wird als // und die bitweisen Operationen.

Aber ist es nicht seltsam, einen Operator hinzuzufügen, der keine Standardbibliotheksverwendung hat?

Es ist sicherlich ungewöhnlich (obwohl erweitertes Slicing existierte lange bevor eingebaute Typen Unterstützung dafür erhielten, Ellipsis wird immer noch nicht in der Standardbibliothek verwendet usw.). Aber das Wichtigste ist, ob eine Änderung den Benutzern nützt, nicht wo die Software heruntergeladen wird. Aus dem oben Genannten geht klar hervor, dass @ verwendet wird und zwar häufig. Und diese PEP liefert das kritische Stück, das es der Python-Numerik-Community ermöglichen wird, endlich einen Konsens über einen Standard-Duck-Typ für alle Array-ähnlichen Objekte zu erzielen, was eine notwendige Voraussetzung dafür ist, jemals einen numerischen Array-Typ in die Standardbibliothek aufzunehmen.

Kompatibilitätsüberlegungen

Derzeit ist die einzige legale Verwendung des Tokens @ in Python-Code am Anfang einer Anweisung in Dekoratoren. Die neuen Operatoren sind beide Infix; die einzige Stelle, an der sie niemals auftreten können, ist am Anfang einer Anweisung. Daher wird kein bestehender Code durch die Hinzufügung dieser Operatoren gebrochen, und es gibt keine mögliche Parsing-Mehrdeutigkeit zwischen Decorator-@ und den neuen Operatoren.

Eine weitere wichtige Art der Kompatibilität ist die mentale Belastung, die Benutzer auf sich nehmen, um ihr Verständnis der Python-Sprache nach dieser Änderung zu aktualisieren, insbesondere für Benutzer, die nicht mit Matrizen arbeiten und daher keinen Nutzen daraus ziehen. Auch hier hat @ nur minimale Auswirkungen: Selbst umfassende Tutorials und Referenzen müssen nur ein oder zwei Sätze hinzufügen, um die Änderungen dieser PEP für ein nicht-numerisches Publikum vollständig zu dokumentieren.

Details zur beabsichtigten Nutzung

Dieser Abschnitt ist informativ und nicht normativ – er dokumentiert den Konsens einer Reihe von Bibliotheken, die Array- oder Matrix-ähnliche Objekte bereitstellen, wie @ implementiert wird.

Dieser Abschnitt verwendet die Numpy-Terminologie zur Beschreibung beliebiger mehrdimensionaler Datenarrays, da sie eine Obermenge aller anderen gängigen Modelle darstellt. In diesem Modell wird die *Form* jedes Arrays durch ein Tupel von Ganzzahlen dargestellt. Da Matrizen zweidimensional sind, haben sie len(shape) == 2, während 1D-Vektoren len(shape) == 1 haben und Skalare shape == () haben, d. h. sie sind „0-dimensional“. Jedes Array enthält insgesamt prod(shape) Einträge. Beachten Sie, dass prod(()) == 1 ist (aus demselben Grund, warum sum(()) == 0 ist); Skalare sind nur eine gewöhnliche Art von Array, kein Sonderfall. Beachten Sie auch, dass wir zwischen einem einzelnen Skalarwert (shape == (), analog zu 1), einem Vektor mit nur einem Eintrag (shape == (1,), analog zu [1]), einer Matrix mit nur einem Eintrag (shape == (1, 1,), analog zu [[1]]) usw. unterscheiden, sodass die Dimensionalität jedes Arrays immer gut definiert ist. Andere Bibliotheken mit eingeschränkteren Darstellungen (z. B. solche, die nur 2D-Arrays unterstützen) implementieren möglicherweise nur eine Teilmenge der hier beschriebenen Funktionalität.

Semantik

Die empfohlenen Semantiken für @ für verschiedene Eingaben sind:

  • 2D-Eingaben sind konventionelle Matrizen, und daher sind die Semantiken offensichtlich: wir wenden die konventionelle Matrixmultiplikation an. Wenn wir arr(2, 3) zur Darstellung eines beliebigen 2x3-Arrays schreiben, dann gibt arr(2, 3) @ arr(3, 4) ein Array mit der Form (2, 4) zurück.
  • 1D-Vektor-Eingaben werden zu 2D befördert, indem eine '1' vorangestellt oder angehängt wird, die Operation wird durchgeführt und dann wird die hinzugefügte Dimension aus der Ausgabe entfernt. Die 1 wird immer am "äußeren" Rand der Form angebracht: vorangestellt für linke Argumente und angehängt für rechte Argumente. Das Ergebnis ist, dass Matrix @ Vektor und Vektor @ Matrix beide legal sind (bei kompatiblen Formen) und beide 1D-Vektoren zurückgeben; Vektor @ Vektor gibt einen Skalar zurück. Dies ist mit Beispielen klarer.
    • arr(2, 3) @ arr(3, 1) ist ein reguläres Matrixprodukt und gibt ein Array mit der Form (2, 1) zurück, d. h. einen Spaltenvektor.
    • arr(2, 3) @ arr(3) führt die gleiche Berechnung wie die vorherige durch (d. h. behandelt den 1D-Vektor als eine Matrix, die eine einzelne *Spalte* enthält, Form = (3, 1)), gibt aber das Ergebnis mit der Form (2,) zurück, d. h. einen 1D-Vektor.
    • arr(1, 3) @ arr(3, 2) ist ein reguläres Matrixprodukt und gibt ein Array mit der Form (1, 2) zurück, d. h. einen Zeilenvektor.
    • arr(3) @ arr(3, 2) führt die gleiche Berechnung durch wie zuvor (d. h. behandelt den 1D-Vektor als Matrix mit einer einzelnen *Zeile*, Form = (1, 3)), gibt das Ergebnis aber mit der Form (2,) zurück, d. h. als 1D-Vektor.
    • arr(1, 3) @ arr(3, 1) ist ein reguläres Matrixprodukt und gibt ein Array mit der Form (1, 1) zurück, d. h. einen einzelnen Wert in Matrixform.
    • arr(3) @ arr(3) führt die gleiche Berechnung wie zuvor durch, gibt das Ergebnis aber mit der Form () zurück, d. h. einen einzelnen Skalarwert, nicht in Matrixform. Dies ist also das Standard-Skalarprodukt für Vektoren.

    Eine Unannehmlichkeit dieser Definition für 1D-Vektoren besteht darin, dass @ in einigen Fällen nicht-assoziativ ist ((Mat1 @ vec) @ Mat2 != Mat1 @ (vec @ Mat2)). Aber das scheint ein Fall zu sein, in dem Praktikabilität Reinheit übertrifft: Nicht-Assoziativität tritt nur bei seltsamen Ausdrücken auf, die nie in der Praxis geschrieben würden; wenn sie dennoch geschrieben werden, gibt es eine konsistente Regel, um zu verstehen, was passieren wird (Mat1 @ vec @ Mat2 wird als (Mat1 @ vec) @ Mat2 analysiert, genau wie a - b - c); und die Nicht-Unterstützung von 1D-Vektoren würde viele wichtige Anwendungsfälle ausschließen, die in der Praxis sehr häufig vorkommen. Niemand möchte neuen Benutzern erklären, dass sie zum Lösen des einfachsten linearen Gleichungssystems auf die naheliegende Weise (inv(A) @ b[:, np.newaxis]).flatten() statt inv(A) @ b eingeben müssen, oder eine gewöhnliche Kleinste-Quadrate-Regression durch Eingabe von solve(X.T @ X, X @ y[:, np.newaxis]).flatten() statt solve(X.T @ X, X @ y) durchführen müssen. Niemand möchte jedes Mal, wenn er ein Skalarprodukt berechnet, (a[np.newaxis, :] @ b[:, np.newaxis])[0, 0] statt a @ b eingeben, oder (a[np.newaxis, :] @ Mat @ b[:, np.newaxis])[0, 0] für allgemeine quadratische Formen statt a @ Mat @ b. Außerdem verwenden Sage und Sympy (siehe unten) diese nicht-assoziativen Semantiken mit einem Infix-Matrixmultiplikationsoperator (sie verwenden *) und berichten, dass sie dadurch keine Probleme hatten.

  • Für Eingaben mit mehr als 2 Dimensionen behandeln wir die letzten beiden Dimensionen als die Dimensionen der zu multiplizierenden Matrizen und "broadcasten" über die anderen Dimensionen. Dies bietet eine bequeme Möglichkeit, viele Matrixprodukte schnell in einer einzigen Operation zu berechnen. Zum Beispiel führt arr(10, 2, 3) @ arr(10, 3, 4) 10 separate Matrixmultiplikationen durch, von denen jede eine 2x3- und eine 3x4-Matrix multipliziert, um eine 2x4-Matrix zu erzeugen, und gibt dann die 10 resultierenden Matrizen zusammen in einem Array mit der Form (10, 2, 4) zurück. Die Intuition hier ist, dass wir diese 3D-Arrays von Zahlen so behandeln, als wären sie 1D-Arrays *von Matrizen*, und dann die Matrixmultiplikation elementweise anwenden, wobei jedes "Element" nun eine ganze Matrix ist. Beachten Sie, dass Broadcasting nicht auf perfekt ausgerichtete Arrays beschränkt ist; in komplizierteren Fällen ermöglicht es mehrere einfache, aber leistungsstarke Tricks zur Steuerung der Ausrichtung von Arrays; siehe [10] für Details. (Insbesondere stellt sich heraus, dass bei Berücksichtigung des Broadcastings das Standardprodukt Skalar * Matrix ein Spezialfall des elementweisen Multiplikationsoperators * ist.)

    Wenn ein Operand >2D ist und ein anderer Operand 1D ist, dann gelten die obigen Regeln unverändert, wobei die 1D->2D-Promotion vor dem Broadcasting erfolgt. Z.B. erzeugt arr(10, 2, 3) @ arr(3) zuerst eine Promotion zu arr(10, 2, 3) @ arr(3, 1), broadcastet dann das rechte Argument, um die ausgerichtete Operation arr(10, 2, 3) @ arr(10, 3, 1) zu erstellen, multipliziert, um ein Array mit der Form (10, 2, 1) zu erhalten, und entfernt schließlich die hinzugefügte Dimension, wodurch ein Array mit der Form (10, 2) zurückgegeben wird. Ebenso erzeugt arr(2) @ arr(10, 2, 3) ein Zwischenarray mit der Form (10, 1, 3) und ein finales Array mit der Form (10, 3).

  • 0D- (Skalar-)Eingaben führen zu einem Fehler. Skalar * Matrix-Multiplikation ist eine mathematisch und algorithmisch getrennte Operation von Matrix @ Matrix-Multiplikation und wird bereits durch den elementweisen *-Operator abgedeckt. Die Zulassung von Skalar @ Matrix würde daher sowohl einen unnötigen Sonderfall darstellen als auch TOOWTDI verletzen.

Übernahme

Wir gruppieren bestehende Python-Projekte, die Array- oder Matrix-ähnliche Typen bereitstellen, basierend auf der API, die sie derzeit für elementweise und Matrixmultiplikation verwenden.

Projekte, die derzeit * für elementweise Multiplikation und Funktions-/Methodenaufrufe für Matrixmultiplikation verwenden

Die Entwickler der folgenden Projekte haben die Absicht bekundet, @ auf ihren Array-ähnlichen Typen mit der oben genannten Semantik zu implementieren.

  • numpy
  • pandas
  • blaze
  • theano

Die folgenden Projekte wurden auf die Existenz des PEP hingewiesen, aber es ist noch nicht bekannt, was sie tun werden, falls er akzeptiert wird. Wir erwarten jedoch keine Einwände, da alles hier Vorgeschlagene mit dem übereinstimmt, wie sie bereits Dinge tun.

  • pycuda
  • panda3d

Projekte, die derzeit * für Matrixmultiplikation und Funktions-/Methodenaufrufe für elementweise Multiplikation verwenden

Die folgenden Projekte haben die Absicht geäußert, falls dieser PEP angenommen wird, von ihrer aktuellen API zur Konvention elementweise-*, matmul-@ zu migrieren (d. h. dies ist eine Liste von Projekten, deren API-Fragmentierung wahrscheinlich beseitigt wird, wenn dieser PEP angenommen wird).

  • numpy (numpy.matrix)
  • scipy.sparse
  • pyoperators
  • pyviennacl

Die folgenden Projekte wurden auf die Existenz des PEP hingewiesen, aber es ist nicht bekannt, was sie tun werden, falls er angenommen wird (d. h. dies ist eine Liste von Projekten, deren API-Fragmentierung möglicherweise oder möglicherweise nicht beseitigt wird, wenn dieser PEP angenommen wird).

  • cvxopt

Projekte, die derzeit * für Matrixmultiplikation verwenden und denen die elementweise Multiplikation von Matrizen nicht wirklich wichtig ist

Es gibt mehrere Projekte, die Matrixtypen implementieren, aber aus einer ganz anderen Perspektive als die oben genannten numerischen Bibliotheken. Diese Projekte konzentrieren sich auf rechnerische Methoden zur Analyse von Matrizen im Sinne abstrakter mathematischer Objekte (d. h. lineare Abbildungen über freie Moduln über Ringen) und nicht als große Beutel mit Zahlen, die verarbeitet werden müssen. Und es stellt sich heraus, dass aus der abstrakten mathematischen Sicht elementweise Operationen kaum von Nutzen sind; wie im Hintergrundkapitel oben erläutert, sind elementweise Operationen durch den Bag-of-Numbers-Ansatz motiviert. Diese Projekte stoßen also nicht auf das grundlegende Problem, das dieser PEP zu lösen versucht, was sie für sie weitgehend irrelevant macht; obwohl sie oberflächlich den Projekten wie numpy ähneln, tun sie tatsächlich etwas ganz anderes. Sie verwenden * für Matrixmultiplikation (und für Gruppenaktionen usw.) und falls dieser PEP angenommen wird, ist ihre geäußerte Absicht, dies fortzusetzen, vielleicht @ als Alias hinzuzufügen. Zu diesen Projekten gehören

  • sympy
  • sage

Implementierungsdetails

Neue Funktionen operator.matmul und operator.__matmul__ werden zur Standardbibliothek hinzugefügt, mit der üblichen Semantik.

Eine entsprechende Funktion PyObject* PyObject_MatrixMultiply(PyObject *o1, PyObject *o2) wird zur C-API hinzugefügt.

Ein neuer AST-Knoten namens MatMult wird zusammen mit einem neuen Token ATEQUAL und neuen Bytecode-Opcodes BINARY_MATRIX_MULTIPLY und INPLACE_MATRIX_MULTIPLY hinzugefügt.

Zwei neue Typslots werden hinzugefügt; ob dies zu PyNumberMethods oder einer neuen PyMatrixMethods Struktur gehört, muss noch festgelegt werden.

Begründung für Spezifikationsdetails

Wahl des Operators

Warum @ und nicht eine andere Schreibweise? Es gibt keinen Konsens über andere Programmiersprachen hinweg, wie dieser Operator benannt werden sollte [11]; hier diskutieren wir die verschiedenen Optionen.

Beschränken wir uns nur auf Symbole, die auf US-englischen Tastaturen vorhanden sind, sind die Satzzeichen, die in Python noch keine Bedeutung im Ausdruckskontext haben: @, Backtick, $, ! und ?. Von diesen Optionen ist @ eindeutig die beste; ! und ? sind im Programmierkontext bereits stark mit unzutreffenden Bedeutungen belegt, der Backtick wurde von BDFL per Dekret aus Python verbannt (siehe PEP 3099), und $ ist hässlicher, noch unähnlicher zu * und , und hat Perl/PHP-Altlasten. $ ist wahrscheinlich die zweitbeste Option von diesen.

Symbole, die nicht auf US-englischen Tastaturen vorhanden sind, haben einen erheblichen Nachteil (5 Minuten am Anfang jedes numerischen Python-Tutorials nur für die Tastaturbelegung aufzuwenden, ist keine Belastung, die jemand wirklich will). Außerdem ist es selbst wenn wir das Tippproblem irgendwie überwinden, nicht klar, ob es überhaupt welche gibt, die besser als @ sind. Einige vorgeschlagene Optionen sind:

  • U+00D7 MULTIPLIKATIONSZEICHEN: A × B
  • U+22C5 PUNKTOPERATOR: A B
  • U+2297 GEKREISTER MAL: A B
  • U+00B0 GRAD: A ° B

Was wir brauchen, ist jedoch ein Operator, der "Matrixmultiplikation im Gegensatz zu Skalar-/elementweiser Multiplikation" bedeutet. Es gibt kein konventionelles Symbol mit dieser Bedeutung weder in der Programmierung noch in der Mathematik, wo diese Operationen normalerweise durch Kontext unterschieden werden. (Und U+2297 GEKREISTER MAL wird tatsächlich konventionell für genau das Falsche verwendet: elementweise Multiplikation – das "Hadamard-Produkt" – oder äußeres Produkt, anstatt Matrix-/inneres Produkt wie unser Operator). @ hat zumindest den Vorteil, dass es *wie* ein seltsamer nicht-kommutativer Operator aussieht; ein naiver Benutzer, der Mathematik, aber keine Programmierung kennt, könnte A * B im Gegensatz zu A × B, oder A * B im Gegensatz zu A B, oder A * B im Gegensatz zu A ° B nicht unterscheiden und raten, welcher die übliche Multiplikation und welcher der Sonderfall ist.

Schließlich gibt es die Option, Mehrzeichen-Tokens zu verwenden. Einige Optionen

  • Matlab und Julia verwenden einen .*-Operator. Abgesehen davon, dass er visuell mit * verwechselt werden kann, wäre dies für uns eine schreckliche Wahl, denn in Matlab und Julia bedeutet * Matrixmultiplikation und .* elementweise Multiplikation. Die Verwendung von .* für Matrixmultiplikation würde uns also genau umgekehrt zu dem machen, was Matlab- und Julia-Benutzer erwarten.
  • APL verwendete anscheinend +.×, was durch die Kombination eines Mehrzeichen-Tokens, verwirrender Attributzugriffs-ähnlicher . Syntax und eines Unicode-Zeichens irgendwo unter U+2603 SCHNEEMANN auf unserer Kandidatenliste rangiert. Wenn uns die Idee gefällt, Additions- und Multiplikationsoperatoren zu kombinieren, um zu evozieren, wie Matrixmultiplikation tatsächlich funktioniert, dann könnte etwas wie +* verwendet werden – obwohl dies zu leicht mit *+ verwechselt werden könnte, was einfach die Multiplikation kombiniert mit dem unären +-Operator ist.
  • PEP 211 schlug ~* vor. Dies hat den Nachteil, dass es irgendwie vorschlägt, dass es einen unären *-Operator gibt, der mit unärem ~ kombiniert wird, aber es könnte funktionieren.
  • R verwendet %*% für Matrixmultiplikation. In R bildet dies Teil eines allgemeinen erweiterbaren Infixsystems, in dem alle Tokens der Form %foo% benutzerdefinierte Binäroperatoren sind. Wir könnten das Token stehlen, ohne das System zu stehlen.
  • Einige andere plausible Kandidaten, die vorgeschlagen wurden: >< (= ASCII-Zeichnung des Multiplikationszeichens ×); der Fußnotenoperator [*] oder |*| (aber im Kontext verwendet, die Verwendung von vertikalen Gruppierungssymbolen neigt dazu, die visuelle Unübersichtlichkeit von verschachtelten Klammern wiederherzustellen, die als einer der Hauptnachteile der Funktionssyntax, von der wir wegkommen wollen, festgestellt wurde); ^*.

Also, es spielt keine große Rolle, aber @ scheint besser oder gleich gut zu sein wie jede der Alternativen

  • Es ist ein freundliches Zeichen, das Pythoneers bereits von der Verwendung in Decorators kennen, aber die Verwendung als Decorator und die Verwendung im mathematischen Ausdruck sind so unterschiedlich, dass es schwierig wäre, sie in der Praxis zu verwechseln.
  • Es ist auf Tastaturlayouts weit verbreitet (und dank seiner Verwendung in E-Mail-Adressen gilt dies auch für seltsame Tastaturen wie die auf Mobiltelefonen).
  • Es ist rund wie * und .
  • Die mATrices-Eselsbrücke ist niedlich.
  • Die schwungvolle Form erinnert an die gleichzeitigen Durchläufe über Zeilen und Spalten, die die Matrixmultiplikation definieren.
  • Seine Asymmetrie ist bezeichnend für seine Nicht-Kommutativität.
  • Was auch immer, wir müssen uns für etwas entscheiden.

Rangfolge und Assoziativität

Es gab eine lange Diskussion [15] darüber, ob @ rechts- oder linksassoziativ sein sollte (oder sogar etwas Exotischeres [18]). Fast alle Python-Operatoren sind linksassoziativ, so dass die Befolgung dieser Konvention der einfachste Ansatz wäre, aber es gab zwei Argumente, die nahelegten, dass Matrixmultiplikation als Sonderfall rechtsassoziativ gemacht werden könnte.

Erstens hat Matrixmultiplikation eine enge konzeptionelle Verbindung zur Funktionsanwendung/-komposition, sodass viele mathematisch versierte Benutzer die Intuition haben, dass ein Ausdruck wie RSx von rechts nach links verläuft, wobei zuerst S den Vektor x transformiert und dann R das Ergebnis transformiert. Dies ist nicht allgemein anerkannt (und nicht alle Zahlenknacker sind in dem reinen mathematischen konzeptionellen Rahmen verwurzelt, der diese Intuition motiviert [16]), aber zumindest ist diese Intuition weiter verbreitet als bei anderen Operationen wie 2⋅3⋅4, die jeder von links nach rechts liest.

Zweitens, wenn Ausdrücke wie Mat @ Mat @ vec oft im Code vorkommen, werden Programme schneller laufen (und effizienzorientierte Programmierer können weniger Klammern verwenden), wenn dies als Mat @ (Mat @ vec) ausgewertet wird, als wenn es wie (Mat @ Mat) @ vec ausgewertet wird.

Gegen diese Argumente sprechen jedoch folgende Punkte:

Was das Effizienzargument betrifft, so konnten wir empirisch keine Beweise dafür finden, dass Ausdrücke vom Typ Mat @ Mat @ vec in realem Code tatsächlich dominieren. Bei der Analyse einer Reihe von großen Projekten, die numpy verwenden, stellten wir fest, dass die Leute, wenn sie durch die aktuelle Funktionsaufruf-Syntax von numpy gezwungen wurden, eine Reihenfolge der Operationen für verschachtelte Aufrufe von dot zu wählen, tatsächlich etwas *häufiger* linksassoziative Verschachtelungen als rechtsassoziative Verschachtelungen verwendeten [17]. Und sowieso ist das Schreiben von Klammern nicht so schlimm – wenn ein effizienzorientierter Programmierer die Mühe auf sich nimmt, über den besten Weg zur Auswertung eines Ausdrucks nachzudenken, sollte er die Klammern wahrscheinlich trotzdem schreiben, nur um dem nächsten Leser klarzumachen, dass die Reihenfolge der Operationen wichtig ist.

Darüber hinaus stellt sich heraus, dass andere Sprachen, darunter solche mit einem stärkeren Fokus auf lineare Algebra, ihre Matmul-Operatoren überwiegend linksassoziativ machen. Insbesondere ist das @-Äquivalent in R, Matlab, Julia, IDL und Gauss linksassoziativ. Die einzigen Ausnahmen, die wir fanden, sind Mathematica, in dem a @ b @ c nicht-assoziativ als dot(a, b, c) analysiert würde, und APL, in dem alle Operatoren rechtsassoziativ sind. Es scheinen keine Sprachen zu existieren, die @ rechtsassoziativ und * linksassoziativ machen. Und diese Entscheidungen scheinen nicht kontrovers zu sein – ich habe noch nie jemanden über diesen speziellen Aspekt einer dieser anderen Sprachen klagen hören, und die Linksassoziativität von * scheint Benutzer der bestehenden Python-Bibliotheken, die * für Matrixmultiplikation verwenden, nicht zu stören. So können wir zumindest daraus schließen, dass die Linksassoziativität von @ sicherlich keine Katastrophen verursachen wird. Die Rechtsassoziativität von @ hingegen würde neues und unsicheres Terrain erkunden.

Und ein weiterer Vorteil der Linksassoziativität ist, dass es viel einfacher zu lernen und zu merken ist, dass @ sich wie * verhält, als sich zuerst daran zu erinnern, dass @ sich von anderen Python-Operatoren unterscheidet, indem es rechtsassoziativ ist, und dann zusätzlich noch daran denken zu müssen, ob es enger oder lockerer bindet als *. (Rechtsassoziativität zwingt uns, eine Präzedenz zu wählen, und die Intuitionen waren etwa gleichmäßig aufgeteilt, welche Präzedenz sinnvoller wäre. Dies deutet also darauf hin, dass niemand, egal welche Wahl wir treffen, sie erraten oder sich daran erinnern kann.)

Insgesamt ist der allgemeine Konsens der numerischen Gemeinschaft daher, dass Matrixmultiplikation zwar ein Sonderfall ist, aber nicht so besonders, dass die Regeln gebrochen werden sollten, und @ sollte sich wie * verhalten.

(Nicht-)Definitionen für eingebaute Typen

Für eingebaute numerische Typen (float, int usw.) oder für die numbers.Number-Hierarchie sind keine __matmul__ oder __matpow__ definiert, da diese Typen Skalare darstellen und die konsensfähige Semantik für @ ist, dass sie bei Skalaren einen Fehler auslösen sollte.

Wir definieren – vorerst – keine __matmul__-Methode für die Standard-memoryview- oder array.array-Objekte, aus mehreren Gründen. Natürlich könnte dies hinzugefügt werden, wenn jemand es wünscht, aber diese Typen würden einiges an zusätzlicher Arbeit über __matmul__ hinaus erfordern, bevor sie für numerische Arbeiten verwendet werden könnten – z. B. haben sie auch keine Möglichkeit, Addition oder Skalarmultiplikation durchzuführen! – und das Hinzufügen einer solchen Funktionalität liegt außerhalb des Rahmens dieses PEP. Darüber hinaus ist die Bereitstellung einer qualitativ hochwertigen Implementierung der Matrixmultiplikation sehr nicht trivial. Naive Implementierungen mit verschachtelten Schleifen sind sehr langsam, und das Bereitstellen einer solchen Implementierung in CPython würde nur eine Falle für Benutzer schaffen. Die Alternative – eine moderne, konkurrenzfähige Matrixmultiplikation bereitzustellen – würde erfordern, dass CPython eine BLAS-Bibliothek linkt, was eine Reihe neuer Komplikationen mit sich bringt. Insbesondere brechen mehrere beliebte BLAS-Bibliotheken (einschließlich derjenigen, die standardmäßig auf OS X ausgeliefert wird) die Verwendung von multiprocessing [8]. Zusammen bedeuten diese Überlegungen, dass der Kosten/Nutzen des Hinzufügens von __matmul__ zu diesen Typen einfach nicht vorhanden ist, so dass wir für den Moment weiterhin diese Probleme an numpy und Freunde delegieren und eine systematischere Lösung auf einen zukünftigen Vorschlag verschieben.

Es gibt auch nicht-numerische Python-Builtins, die __mul__ definieren (str, list, …). Wir definieren auch für diese Typen keine __matmul__, weil wir das überhaupt nicht tun würden.

Nicht-Definition von Matrixpotenz

Frühere Versionen dieses PEP schlugen auch einen Matrixpotenzoperator, @@, analog zu **, vor. Aber nach weiterer Überlegung wurde entschieden, dass der Nutzen hiervon ausreichend unklar war, dass es besser wäre, ihn vorerst wegzulassen und das Problem nur dann wieder aufzugreifen, wenn – sobald wir mehr Erfahrung mit @ haben – sich herausstellt, dass @@ wirklich fehlt. [14]

Abgelehnte Alternativen zur Hinzufügung eines neuen Operators

In den letzten Jahrzehnten hat die Python-Community eine Vielzahl von Wegen erforscht, um die Spannung zwischen Matrix- und elementweiser Multiplikation zu lösen. PEP 211 und PEP 225, beide im Jahr 2000 vorgeschlagen und zuletzt 2008 ernsthaft diskutiert [9], waren frühe Versuche, neue Operatoren hinzuzufügen, um dieses Problem zu lösen, litten aber unter ernsthaften Mängeln; insbesondere hatte die Python-numerische Gemeinschaft zu dieser Zeit noch keinen Konsens über die richtige API für Array-Objekte oder darüber, welche Operatoren benötigt oder nützlich sein könnten (z. B. schlägt PEP 225 6 neue Operatoren mit unbestimmter Semantik vor). Die Erfahrungen seitdem haben nun zu dem Konsens geführt, dass die beste Lösung sowohl für das numerische Python als auch für das Kern-Python darin besteht, einen einzigen Infixoperator für Matrixmultiplikation hinzuzufügen (zusammen mit den anderen neuen Operatoren, die dies impliziert, wie @=).

Wir überprüfen hier einige der verworfenen Alternativen.

Verwendung eines zweiten Typs, der __mul__ als Matrixmultiplikation definiert: Wie oben diskutiert (Hintergrund: Was ist falsch am Status Quo?), wurde dies viele Jahre lang über den numpy.matrix-Typ (und seine Vorgänger in Numeric und numarray) versucht. Das Ergebnis ist ein starker Konsens sowohl unter den numpy-Entwicklern als auch unter den Entwicklern von nachgelagerten Paketen, dass numpy.matrix aufgrund der Probleme, die durch widersprüchliche Duck-Typen für Arrays verursacht werden, im Wesentlichen nie verwendet werden sollte. (Natürlich könnte man dann argumentieren, dass wir __mul__ *nur* als Matrixmultiplikation definieren sollten, aber dann hätten wir das gleiche Problem mit elementweiser Multiplikation.) Es gab mehrere Bestrebungen, numpy.matrix vollständig zu entfernen; die einzigen Gegenargumente kamen von Pädagogen, die feststellen, dass seine Probleme durch die Notwendigkeit, eine einfache und klare Abbildung zwischen mathematischer Notation und Code für Novizen bereitzustellen, überwiegt werden (siehe Transparente Syntax ist besonders wichtig für nicht-fachkundige Programmierer). Aber natürlich verursacht es eigene Probleme, Anfänger mit einer verpönten Syntax zu beginnen und sie dann zu erwarten, dass sie später wechseln. Die Zwei-Typen-Lösung ist schlimmer als die Krankheit.

Hinzufügen vieler neuer Operatoren oder Hinzufügen einer neuen generischen Syntax zum Definieren von Infixoperatoren: Abgesehen davon, dass dies generell un-Pythonisch ist und wiederholt vom BDFL-Dekret abgelehnt wurde, wäre dies die Verwendung eines Vorschlaghammers, um eine Fliege zu erschlagen. Die wissenschaftliche Python-Gemeinschaft ist sich einig, dass das Hinzufügen eines Operators für Matrixmultiplikation ausreicht, um den einzigen sonst nicht behebaren Schwachpunkt zu beheben. (Im Rückblick denken wir alle, dass PEP 225 auch eine schlechte Idee war – oder zumindest viel komplexer als nötig.)

Hinzufügen eines neuen @ (oder was auch immer) Operators, der in allgemeinem Python eine andere Bedeutung hat, und ihn dann in numerischem Code überladen: Dies war der Ansatz, der von PEP 211 verfolgt wurde, der vorschlug, @ als Äquivalent zu itertools.product zu definieren. Das Problem dabei ist, dass itertools.product für sich genommen klar nicht einen dedizierten Operator benötigt. Es wurde nicht einmal als eingebaut erachtet. (Während der Diskussionen über diesen PEP wurde ein ähnlicher Vorschlag gemacht, @ als generellen Funktionskompositionsoperator zu definieren, und dies leidet unter dem gleichen Problem; functools.compose ist nicht einmal nützlich genug, um zu existieren.) Matrixmultiplikation hat eine einzigartig starke Begründung für die Aufnahme als Infixoperator. Es gibt mit ziemlicher Sicherheit keine anderen binären Operationen, die jemals die Hinzufügung weiterer Infixoperatoren zu Python rechtfertigen würden.

Hinzufügen einer .dot-Methode zu Array-Typen, um die „Pseudo-Infix“-Syntax A.dot(B) zu ermöglichen: Dies gibt es in numpy seit einigen Jahren, und in vielen Fällen ist es besser als dot(A, B). Aber es ist immer noch viel weniger lesbar als echte Infixnotation, und leidet insbesondere immer noch unter einer extremen Überfülle an Klammern. Siehe Warum sollte Matrixmultiplikation Infix sein? oben.

Verwenden eines ‚with‘-Blocks, um die Bedeutung von * innerhalb eines einzelnen Codeblocks umzuschalten: Z.B. könnte numpy ein spezielles Kontextobjekt definieren, sodass wir hätten:

c = a * b   # element-wise multiplication
with numpy.mul_as_dot:
    c = a * b  # matrix multiplication

Dies hat jedoch zwei ernsthafte Probleme: Erstens erfordert es, dass die __mul__-Methode jedes Array-ähnlichen Typs weiß, wie ein globaler Zustand überprüft wird (numpy.mul_is_currently_dot oder was auch immer). Das ist in Ordnung, wenn a und b Numpy-Objekte sind, aber die Welt enthält viele Nicht-Numpy-Array-ähnliche Objekte. Dies erfordert entweder nicht-lokale Kopplung – jede numpy-Konkurrenzbibliothek muss numpy importieren und dann numpy.mul_is_currently_dot bei jeder Operation überprüfen – oder es bricht das Duck-Typing, wobei der obige Code radikal unterschiedliche Dinge tut, je nachdem, ob a und b Numpy-Objekte oder eine andere Art von Objekt sind. Zweitens und schlimmer ist, dass with-Blöcke dynamisch und nicht lexikalisch gekapselt sind; d. h. jede Funktion, die innerhalb des with-Blocks aufgerufen wird, findet sich plötzlich im mul_as_dot-Bereich wieder und geht schlimmstenfalls vor die Hunde – wenn man Glück hat. Dies ist also ein Konstrukt, das nur in ziemlich begrenzten Fällen (keine Funktionsaufrufe) sicher verwendet werden könnte und das es sehr einfach machen würde, sich unabsichtlich ins eigene Knie zu schießen.

Verwenden Sie einen Sprachpräprozessor, der zusätzliche numerisch orientierte Operatoren und möglicherweise andere Syntax hinzufügt: (Gemäß dem neuesten Vorschlag des BDFL: [1]) Dieser Vorschlag scheint auf der Idee zu basieren, dass numerischer Code eine breite Palette von Syntaxerweiterungen benötigt. Tatsächlich benötigen die meisten numerischen Anwender angesichts von @ keine zusätzlichen Operatoren oder Syntax; er löst das einzige wirklich schmerzhafte Problem, das auf andere Weise nicht gelöst werden kann und das schmerzhafte Nachwirkungen im größeren Ökosystem verursacht. Die Definition einer neuen Sprache (vermutlich mit eigenem Parser, der mit dem von Python synchron gehalten werden müsste usw.) nur zur Unterstützung eines einzigen binären Operators ist weder praktikabel noch wünschenswert. Im numerischen Kontext ist Pythons Konkurrenz spezialisierte numerische Sprachen (Matlab, R, IDL usw.). Im Vergleich dazu ist Pythons Killer-Feature genau die Möglichkeit, spezialisierten numerischen Code mit Code für XML-Parsing, Webseiten-Generierung, Datenbankzugriff, Netzwerkprogrammierung, GUI-Bibliotheken usw. zu mischen, und wir profitieren auch von der riesigen Vielfalt an Tutorials, Referenzmaterialien, Einführungskursen usw., die Python verwenden. Die Fragmentierung von "numerischem Python" von "echtem Python" wäre eine große Quelle der Verwirrung. Eine Hauptmotivation für dieses PEP ist es, die Fragmentierung zu *reduzieren*. Die Einrichtung eines Präprozessors wäre eine besonders prohibitive Komplikation für unerfahrene Benutzer. Und wir verwenden Python, weil wir Python mögen! Wir wollen kein fast-aber-nicht-ganz-Python.

Verwenden Sie Overloading-Hacks, um einen "neuen Infix-Operator" wie *dot* zu definieren, wie in einem bekannten Python-Rezept: (Siehe: [2]) Schönheit ist besser als Hässlichkeit. Das ist... nicht schön. Und nicht Pythonisch. Und besonders unfreundlich für Anfänger, die gerade erst versuchen, die Idee zu begreifen, dass es ein kohärentes zugrundeliegendes System hinter diesen magischen Beschwörungen gibt, die sie lernen, wenn dann ein böser Hack wie dieser auftaucht, der dieses System verletzt, bizarre Fehlermeldungen erzeugt, wenn er versehentlich missbraucht wird, und dessen zugrundeliegende Mechanismen ohne tiefes Wissen über objektorientierte Systeme nicht verstanden werden können.

Verwenden Sie einen speziellen "Fassaden"-Typ, um Syntax wie arr.M * arr zu unterstützen: Dies ähnelt stark dem vorherigen Vorschlag, da das Attribut .M im Grunde dasselbe Objekt zurückgibt wie arr *dot, und leidet daher unter denselben Einwänden hinsichtlich "magischer Eigenschaften". Dieser Ansatz hat auch einige nicht offensichtliche Komplexitäten: Zum Beispiel muss arr.M * arr ein Array zurückgeben, aber arr.M * arr.M und arr * arr.M müssen Fassadenobjekte zurückgeben, sonst funktionieren arr.M * arr.M * arr und arr * arr.M * arr nicht. Das bedeutet aber, dass Fassadenobjekte sowohl andere Array-Objekte als auch andere Fassadenobjekte erkennen müssen (was zusätzliche Komplexität für das Schreiben von interoperablen Array-Typen aus verschiedenen Bibliotheken mit sich bringt, die nun sowohl die Array-Typen des anderen als auch deren Fassadentypen erkennen müssen). Es schafft auch Fallstricke für Benutzer, die leicht arr * arr.M oder arr.M * arr.M tippen und erwarten könnten, ein Array-Objekt zurückzubekommen; stattdessen erhalten sie ein mysteriöses Objekt, das Fehler wirft, wenn sie versuchen, es zu verwenden. Grundsätzlich müssen Benutzer bei diesem Ansatz vorsichtig sein, .M* als unteilbare Einheit zu betrachten, die als Infix-Operator fungiert – und was Infix-Operator-ähnliche Token-Strings angeht, ist zumindest *dot* schöner anzusehen (sehen Sie sich seine süßen kleinen Ohren an!).

Diskussionen dieser PEP

Hier zur Referenz gesammelt

Referenzen


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

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