Python-Dekorateure und Lisp-Makros

18

Bei der Suche nach Python-Dekorateuren gab jemand die Aussage ab, dass sie genauso mächtig sind wie Lisp-Makros (insbesondere Clojure).

Wenn ich mir die Beispiele in PEP 318 anschaue, sehe ich so aus, als wären sie nur eine ausgefallene Möglichkeit, einfache alte Funktionen höherer Ordnung in Lisp zu verwenden:

def attrs(**kwds):
    def decorate(f):
        for k in kwds:
            setattr(f, k, kwds[k])
        return f
    return decorate

@attrs(versionadded="2.2",
       author="Guido van Rossum")
def mymethod(f):
    ...

Ich habe in keinem der Beispiele eine Code-Transformation gesehen, wie in Anatomy of a Clojure Macro beschrieben . Außerdem könnte die fehlende Homoikonizität von Python Code-Transformationen unmöglich machen.

Also, wie vergleichen sich diese beiden und können Sie sagen, dass sie in dem, was Sie tun können, ungefähr gleich sind? Beweise scheinen dagegen zu sprechen.

Bearbeiten: Basierend auf einem Kommentar suche ich nach zwei Dingen: Vergleich auf "so mächtig wie" und "so einfach, um tolle Dinge zu tun".

Profpatsch
quelle
12
Natürlich sind Dekorateure keine echten Makros. Sie können keine beliebige Sprache (mit einer völlig anderen Syntax) in Python übersetzen. Wer das Gegenteil behauptet, versteht die Makros einfach nicht.
SK-logic
1
Python ist nicht homoikonisch, aber es ist sehr, sehr dynamisch. Homoiconicity ist nur für leistungsstarke Code-Transformationen erforderlich, wenn Sie dies zur Kompilierungszeit tun möchten. Wenn Sie direkten Zugriff auf das kompilierte AST und Tools zum Ändern des AST unterstützen, können Sie dies unabhängig von der Sprachsyntax zur Laufzeit tun. Davon abgesehen sind "so mächtig wie" und "so einfach, tolle Dinge zu tun" sehr unterschiedliche Konzepte.
Phoshi
Vielleicht sollte ich dann die Frage auf „so einfach, tolle Dinge zu machen“ ändern. ;)
Profpatsch
Vielleicht kann jemand eine vergleichbare Clojure-Funktion höherer Ordnung wie im obigen Python-Beispiel hacken. Ich habe es versucht, bin mir dabei aber durch den Kopf gegangen. Da das Python-Beispiel Objektattribute verwendet, muss dies etwas anders sein.
Profpatsch
@Phoshi Das Ändern des kompilierten AST zur Laufzeit ist bekannt als: selbstmodifizierender Code .
Kaz

Antworten:

16

Ein Dekorateur ist im Grunde nur eine Funktion .

Beispiel in Common Lisp:

(defun attributes (keywords function)
  (loop for (key value) in keywords
        do (setf (get function key) value))
  function)

Oben ist die Funktion ein Symbol (das von zurückgegeben würde DEFUN) und wir fügen die Attribute in die Eigenschaftsliste des Symbols ein .

Jetzt können wir es um eine Funktionsdefinition schreiben:

(attributes
  '((version-added "2.2")
    (author "Rainer Joswig"))

  (defun foo (a b)
    (+ a b))

)  

Wenn wir eine ausgefallene Syntax wie in Python hinzufügen möchten, schreiben wir ein Reader-Makro . Mit einem Reader-Makro können wir auf der Ebene der S-Ausdruckssyntax programmieren:

(set-macro-character
 #\@
 (lambda (stream char)
   (let ((decorator (read stream))
         (arg       (read stream))
         (form      (read stream)))
     `(,decorator ,arg ,form))))

Wir können dann schreiben:

@attributes'((version-added "2.2")
             (author "Rainer Joswig"))
(defun foo (a b)
  (+ a b))

Der Lisp- Leser liest oben:

(ATTRIBUTES (QUOTE ((VERSION-ADDED "2.2")
                    (AUTHOR "Rainer Joswig")))
            (DEFUN FOO (A B) (+ A B)))

Jetzt haben wir eine Form von Dekorateuren in Common Lisp.

Makros und Reader-Makros kombinieren.

Eigentlich würde ich oben die Übersetzung in echtem Code mit einem Makro machen, nicht mit einer Funktion.

(defmacro defdecorator (decorator arg form)
  `(progn
     ,form
     (,decorator ,arg ',(second form))))

(set-macro-character
 #\@
 (lambda (stream char)
   (declare (ignore char))
   (let* ((decorator (read stream))
          (arg       (read stream))
          (form      (read stream)))
     `(defdecorator ,decorator ,arg ,form))))

Die Verwendung erfolgt wie oben mit demselben Reader-Makro. Der Vorteil ist , dass der Lisp - Compiler sieht es immer noch als sogenannte Top-Level - Form - die * Datei Compiler behandelt Top-Level - Formulare speziell zum Beispiel fügt sie Informationen über sie in die Kompilierung- Umgebung . Im obigen Beispiel können wir sehen, dass das Makro in den Quellcode schaut und den Namen extrahiert.

Der Lisp- Leser liest das obige Beispiel in:

(DEFDECORATOR ATTRIBUTES
  (QUOTE ((VERSION-ADDED "2.2")
           (AUTHOR "Rainer Joswig")))
  (DEFUN FOO (A B) (+ A B)))

Welches wird dann Makro erweitert in:

(PROGN (DEFUN FOO (A B) (+ A B))
       (ATTRIBUTES (QUOTE ((VERSION-ADDED "2.2")
                           (AUTHOR "Rainer Joswig")))
                   (QUOTE FOO)))

Makros unterscheiden sich stark von Reader-Makros .

Makros bekommen den Quellcode übergeben, können machen was sie wollen und dann den Quellcode zurückgeben. Die Eingabequelle muss kein gültiger Lisp-Code sein. Es kann alles sein und es könnte ganz anders geschrieben sein. Das Ergebnis muss dann ein gültiger Lisp-Code sein. Wenn der generierte Code jedoch auch ein Makro verwendet, könnte die Syntax des in den Makroaufruf eingebetteten Codes wiederum eine andere sein. Ein einfaches Beispiel: Man könnte ein mathematisches Makro schreiben, das eine Art mathematische Syntax akzeptiert:

(math y = 3 x ^ 2 - 4 x + 3)

Der Ausdruck y = 3 x ^ 2 - 4 x + 3ist kein gültiger Lisp-Code, aber das Makro könnte ihn zum Beispiel analysieren und einen gültigen Lisp-Code wie diesen zurückgeben:

(setq y (+ (* 3 (expt x 2))
           (- (* 4 x))
           3))

Es gibt viele andere Anwendungsfälle von Makros in Lisp.

Rainer Joswig
quelle
8

In Python (der Sprache) können die Dekorateure die Funktion nicht ändern, sondern nur umbrechen, sodass sie definitiv weniger leistungsfähig sind als Lisp-Makros.

In CPython (dem Interpreter) können die Dekoratoren die Funktion ändern, weil sie Zugriff auf den Bytecode haben, aber die Funktion wird zuerst kompiliert und kann dann vom Dekorator manipuliert werden, sodass es nicht möglich ist, die Syntax zu ändern -äquivalent müsste man tun.

Beachten Sie, dass moderne LISPS keine S-Ausdrücke als Bytecode verwenden. Daher funktionieren Makros, die mit S-Ausdruckslisten arbeiten, definitiv vor der Bytecode-Kompilierung, wie oben erwähnt. In Python wird der Decorator danach ausgeführt.

Jan Hudec
quelle
1
Sie müssen die Funktion nicht ändern. Sie müssen nur den Code der Funktion in irgendeiner Form lesen (in der Praxis bedeutet dies Bytecode). Nicht, dass das praktischer wäre.
2
@delnan: Technisch gesehen ändert lisp es auch nicht; es benutzt es als Quelle, um ein neues zu generieren, und das würde Python auch tun, ja. Das Problem liegt im Fehlen einer Token-Liste oder eines AST und in der Tatsache, dass sich der Compiler bereits über einige Dinge beschwert hat, die Sie andernfalls im Makro zulassen könnten.
Jan Hudec
4

Es ist ziemlich schwierig, Python-Dekoratoren zu verwenden, um neue Kontrollflussmechanismen einzuführen.

Die Verwendung von Common-Lisp-Makros zur Einführung neuer Kontrollflussmechanismen ist nahezu trivial.

Daraus folgt wahrscheinlich, dass sie nicht gleichermaßen ausdrucksstark sind (ich interpretiere "kraftvoll" als "ausdrucksstark", da ich denke, dass das, was sie tatsächlich bedeuten).

Vatine
quelle
Ich wage zu sagens/quite hard/impossible/
@delnan Nun, würde ich mich nicht ganz so weit als „unmöglich“ zu sagen, aber Sie würden auf jeden Fall an der Arbeit an ihn haben.
Vatine
0

Es ist sicherlich eine verwandte Funktionalität, aber von einem Python-Dekorator aus ist es nicht trivial, die aufgerufene Methode zu ändern (das wäre der fParameter in Ihrem Beispiel). Um es zu modifizieren, könnten Sie mit dem ast- Modul ( verrückt werden), aber Sie würden sich auf eine ziemlich komplizierte Programmierung einlassen.

Dinge in dieser Richtung wurden jedoch getan: Schauen Sie sich das Macropy- Paket an, um einige wirklich umwerfende Beispiele zu finden.

Jacob Oscarson
quelle
3
Sogar das ast-transformierende Zeug in Python entspricht nicht den Lisp-Makros. Bei Python sollte die Ausgangssprache Python sein, bei Lisp-Makros kann die von einem Makro transformierte Ausgangssprache buchstäblich alles sein. Daher eignet sich die Python-Metaprogrammierung nur für einfache Dinge (wie AoP), während die Lisp-Metaprogrammierung für die Implementierung leistungsfähiger eDSL-Compiler hilfreich ist.
SK-logic
1
Die Sache ist, dass Makropie nicht mit Dekorateuren implementiert wird. Es verwendet die Decorator-Syntax (da eine gültige Python-Syntax verwendet werden muss), wird jedoch durch Hijacken des Byte-Kompilierungsprozesses von einem Import-Hook implementiert.
Jan Hudec
@ SK-logic: In Lisp muss die Ausgangssprache auch lisp sein. Nur die Lisp-Syntax ist sehr einfach, aber flexibel, während die Python-Syntax viel komplexer und weniger flexibel ist.
Jan Hudec
1
@JanHudec, in der Lisp-Ausgangssprache kann jede (ich meine wirklich jede ) Syntax verwendet werden - siehe Reader-Makros.
SK-logic