Dekoratory Python i makra Lisp

18

Patrząc na dekoratorów Pythona, ktoś stwierdził, że są tak potężni jak makra Lisp (szczególnie Clojure).

Patrząc na przykłady podane w PEP 318 , wydaje mi się, że są one po prostu fantazyjnym sposobem użycia zwykłych starych funkcji wyższego rzędu w Lisp:

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):
    ...

Nie widziałem żadnego kodu przekształcającego się w żadnym z przykładów, jak opisano w Anatomy of a Clojure Macro . Ponadto brak homoikoniczności Pythona może uniemożliwić transformację kodu.

Jak więc porównać te dwa elementy i czy możesz powiedzieć, że są one równe pod względem tego, co możesz zrobić? Dowody wydają się przemawiać przeciwko temu.

Edycja: Na podstawie komentarza szukam dwóch rzeczy: porównania „tak potężnego jak” i „tak łatwego do robienia niesamowitych rzeczy”.

Profpatsch
źródło
12
Oczywiście dekoratorzy nie są prawdziwymi makrami. Nie potrafią tłumaczyć dowolnego języka (z zupełnie inną składnią) na python. Ci, którzy twierdzą inaczej, po prostu nie rozumieją makr.
SK-logic
1
Python nie jest homoiczny, jednak jest bardzo dynamiczny. Homoiconicity jest wymagana tylko w przypadku potężnych transformacji kodu, jeśli chcesz to zrobić w czasie kompilacji - jeśli masz wsparcie dla bezpośredniego dostępu do skompilowanego AST i narzędzia do jego zmiany, możesz to zrobić w czasie wykonywania, niezależnie od składni języka. To powiedziawszy, „tak potężny jak” i „tak łatwo robić niesamowite rzeczy” to bardzo różne pojęcia.
Phoshi
Może powinienem zmienić pytanie na „tak łatwo robić niesamowite rzeczy”. ;)
Profpatsch
Być może ktoś może zhakować porównywalną funkcję wyższego rzędu Clojure do powyższego przykładu w Pythonie. Próbowałem, ale w tym momencie przeszło mi przez myśl. Ponieważ przykład w Pythonie używa atrybutów obiektowych, musi to być nieco inne.
Profpatsch,
@ Phoshi Zmiana skompilowanego AST w czasie wykonywania jest znana jako: kod samodmodyfikujący .
Kaz.

Odpowiedzi:

16

Dekorator jest w zasadzie tylko funkcja .

Przykład w Common Lisp:

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

Powyżej funkcja jest symbolem (który zostałby zwrócony przez DEFUN) i umieszczamy atrybuty na liście właściwości symbolu .

Teraz możemy napisać to wokół definicji funkcji:

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

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

)  

Jeśli chcemy dodać fantazyjną składnię, jak w Pythonie, piszemy makro czytnika . Makro czytnika pozwala nam programować na poziomie składni s-expression:

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

Następnie możemy napisać:

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

Czytnik Lisp czyta powyżej, aby:

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

Teraz mamy formę dekoratorów w Common Lisp.

Łączenie makr i makr czytelników.

Właściwie zrobiłbym powyższe tłumaczenie w prawdziwym kodzie przy użyciu makra, a nie funkcji.

(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))))

Zastosowanie jest takie samo jak powyżej w przypadku tego samego makra czytnika. Zaletą jest to, że kompilator Lisp nadal postrzega go jako tak zwaną formę najwyższego poziomu - kompilator plików * traktuje formularze najwyższego poziomu specjalnie, na przykład dodaje informacje o nich do środowiska czasu kompilacji . W powyższym przykładzie widzimy, że makro przegląda kod źródłowy i wyodrębnia nazwę.

Czytnik Lisp wczytuje powyższy przykład na:

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

Który następnie zostaje makro rozszerzony do:

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

Makra bardzo różnią się od makr czytników .

Makra otrzymują kod źródłowy, mogą robić, co chcą, a następnie zwracają kod źródłowy. Źródło wejściowe nie musi być poprawnym kodem Lisp. Może to być wszystko i może być napisane zupełnie inaczej. Wynik musi wtedy być poprawnym kodem Lisp. Ale jeśli wygenerowany kod również korzysta z makra, wówczas składnia kodu osadzonego w wywołaniu makra może znów mieć inną składnię. Prosty przykład: można napisać makro matematyczne, które akceptuje pewną składnię matematyczną:

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

Wyrażenie y = 3 x ^ 2 - 4 x + 3nie jest poprawnym kodem Lisp, ale makro może na przykład parsować je i zwrócić prawidłowy kod Lisp w następujący sposób:

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

Istnieje wiele innych przypadków użycia makr w Lisp.

Rainer Joswig
źródło
8

W Pythonie (języku) dekoratorzy nie mogą modyfikować funkcji, tylko ją zawijają, więc są zdecydowanie mniej wydajni niż makra lisp.

W CPython (interpreter) dekoratorzy mogą modyfikować funkcję, ponieważ mają dostęp do kodu bajtowego, ale funkcja jest najpierw kompilowana, a następnie dekorator może nią manipulować, więc nie można zmienić składni, co jest makropoleceniem - odpowiednik będzie musiał zrobić.

Zauważ, że współczesne lisps nie używają wyrażeń S jako kodu bajtowego, więc makra działające na listach wyrażeń S zdecydowanie działają przed kompilacją kodu bajtowego, jak wspomniano powyżej, w Pythonie dekorator działa po nim.

Jan Hudec
źródło
1
Nie musisz modyfikować funkcji. Musisz tylko odczytać kod funkcji w jakiejś formie (w praktyce oznacza to kod bajtowy). Nie dlatego, że jest to bardziej praktyczne.
2
@delnan: Technicznie lisp też go nie modyfikuje; używa go jako źródła do generowania nowego, więc pytałby tak. Problem leży w braku listy tokenów lub AST oraz w tym, że kompilator już narzekał na pewne rzeczy, które w innym przypadku można by dopuścić w makrze.
Jan Hudec
4

Trudno jest użyć dekoratorów Python do wprowadzenia nowych mechanizmów kontroli przepływu.

Użycie makr Common Lisp do wprowadzenia nowych mechanizmów sterowania przepływem jest banalne.

Z tego prawdopodobnie wynika, że ​​nie są one tak samo ekspresyjne (wybieram interpretowanie słowa „potężny” jako „ekspresyjny”, ponieważ uważam, że to, co faktycznie oznaczają).

Vatine
źródło
s/quite hard/impossible/
@delnan Cóż, nie posunąłbym się tak daleko, by powiedzieć „niemożliwe”, ale na pewno musiałbyś nad tym popracować.
Vatine
0

Jest to z pewnością związana funkcjonalność, ale modyfikator wywoływanej metody w Pythonie nie jest trywialny (byłby to fparametr w twoim przykładzie). Aby zmodyfikować go pan mógł zwariować z ast modułu), ale chcesz być na jakimś dość skomplikowanego programowania.

Sprawy w tym zakresie zostały jednak zrobione: sprawdź pakiet makropy , aby zobaczyć naprawdę zadziwiające przykłady.

Jacob Oscarson
źródło
3
Nawet astrzeczy -transformujące w pythonie nie są równe makrom Lisp. W Pythonie językiem źródłowym powinien być Python, w przypadku makr Lisp językiem źródłowym przekształconym przez makro może być dosłownie wszystko. Dlatego metaprogramowanie w języku Python nadaje się tylko do prostych rzeczy (takich jak AoP), podczas gdy metaprogramowanie Lisp jest przydatne do implementowania wydajnych kompilatorów eDSL.
SK-logic
1
Chodzi o to, że makropy nie są implementowane przy użyciu dekoratorów. Używa składni dekoratora (ponieważ musi używać prawidłowej składni python), ale jest implementowana poprzez przejęcie procesu kompilacji bajtów z haka importu.
Jan Hudec
@ SK-logic: W Lisp językiem źródłowym musi być również lisp. Składnia Just Lisp jest bardzo prosta, ale elastyczna, podczas gdy składnia w języku Python jest znacznie bardziej złożona i mało elastyczna.
Jan Hudec
1
@ JanHudec, w języku źródłowym Lisp może mieć dowolną (tak naprawdę dowolną ) składnię - patrz makra czytelników.
SK-logic