Python-декораторы и макросы Lisp

18

При поиске декораторов Python кто-то утверждал, что они такие же мощные, как макросы Lisp (особенно Clojure).

Глядя на примеры, приведенные в PEP 318, мне кажется, что это всего лишь причудливый способ использования простых старых функций высшего порядка в Лиспе:

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

Я не видел никакого преобразования кода ни в одном из примеров, как описано в « Анатомии Clojure Macro» . Кроме того, отсутствие однородности Python может сделать невозможным преобразование кода.

Итак, как эти два сравниваются и можете ли вы сказать, что они примерно равны в том, что вы можете сделать? Доказательства, кажется, указывают на это.

Изменить: на основе комментария, я ищу две вещи: сравнение «так мощно, как» и «так легко делать удивительные вещи с».

Profpatsch
источник
12
Конечно, декораторы - это не настоящие макросы. Они не могут перевести произвольный язык (с совершенно другим синтаксисом) в Python. Те, кто утверждает обратное, просто не понимают макросы.
SK-logic
1
Python не гомоичен, однако очень динамичен. Гомоиконичность требуется только для мощных преобразований кода, если вы хотите сделать это во время компиляции - если у вас есть поддержка прямого доступа к скомпилированному AST и инструментам для его изменения, вы можете сделать это во время выполнения независимо от синтаксиса языка. При этом «столь же мощный, как» и «с которым легко делать потрясающие вещи» - это совершенно разные понятия.
Фоши
Может быть, я должен изменить вопрос на «как легко делать удивительные вещи» тогда. ;)
Profpatsch
Может быть, кто-то может взломать какую-то сопоставимую функцию высшего порядка Clojure с примером Python выше. Я пытался, но опрокидывал мой разум в процессе. Поскольку пример Python использует атрибуты объекта, это должно быть немного по-другому.
Profpatsch
@Phoshi Изменение скомпилированного AST во время выполнения называется: самоизменяющийся код .
Kaz

Ответы:

16

Декоратор в основном только функция .

Пример в Common Lisp:

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

Выше функция является символом (который будет возвращен DEFUN), и мы помещаем атрибуты в список свойств символа .

Теперь мы можем написать это вокруг определения функции:

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

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

)  

Если мы хотим добавить необычный синтаксис, как в Python, мы напишем макрос для чтения . Макрос читателя позволяет нам программировать на уровне синтаксиса s-выражения:

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

Затем мы можем написать:

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

Читатель Lisp читает выше, чтобы:

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

Теперь у нас есть форма декораторов в Common Lisp.

Сочетание макросов и читательских макросов.

На самом деле я бы сделал перевод выше в реальном коде, используя макрос, а не функцию.

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

Использование, как указано выше, с тем же макросом читателя. Преимущество заключается в том, что компилятор Lisp по-прежнему рассматривает ее как так называемую форму верхнего уровня - файловый компилятор * обрабатывает формы верхнего уровня специально, например, добавляет информацию о них в среду времени компиляции . В приведенном выше примере мы видим, что макрос просматривает исходный код и извлекает имя.

Читатель Lisp читает приведенный выше пример в:

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

Который затем получает макрос, расширенный в:

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

Макросы очень отличаются от макросов читателя .

Макросы передают исходный код, могут делать что хотят, а затем возвращают исходный код. Входной источник не должен быть действительным кодом Lisp. Это может быть что угодно, и это может быть написано совершенно по-другому. Результат должен быть действительным кодом Lisp тогда. Но если сгенерированный код также использует макрос, то синтаксис кода, встроенного в вызов макроса, снова может быть другим синтаксисом. Простой пример: можно написать математический макрос, который будет принимать некоторый математический синтаксис:

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

Выражение y = 3 x ^ 2 - 4 x + 3не является допустимым кодом Lisp, но макрос может, например, проанализировать его и вернуть действительный код Lisp следующим образом:

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

Есть много других случаев использования макросов в Лиспе.

Райнер Йосвиг
источник
8

В Python (языке) декораторы не могут модифицировать функцию, только обернуть ее, поэтому они определенно гораздо менее мощны, чем макросы lisp.

В CPython (интерпретаторе) декораторы могут модифицировать функцию, потому что у них есть доступ к байт-коду, но функция компилируется в первую очередь, а затем может быть изменена декоратором, так что невозможно изменить синтаксис, вещь lisp-macro -эквивалент нужно было бы сделать.

Обратите внимание, что современные списки не используют S-выражения в качестве байт-кода, поэтому макросы, работающие со списками S-выражений, определенно работают до компиляции байт-кода, как отмечалось выше, в python декоратор запускается после него.

Ян Худек
источник
1
Вам не нужно изменять функцию. Вам нужно только прочитать код функции в некоторой форме (на практике это означает байт-код). Не то чтобы это делало его более практичным.
2
@delnan: Технически, lisp тоже не модифицирует его; он использует его в качестве источника для генерации нового и Python, да. Проблема заключается в отсутствии списка токенов или AST, а также в том, что компилятор уже жаловался на некоторые вещи, которые вы могли бы иначе разрешить в макросе.
Ян Худек
4

Довольно сложно использовать Python-декораторы для внедрения новых механизмов управления потоком.

Использование макросов Common Lisp для внедрения новых механизмов управления потоком является тривиальным.

Из этого, вероятно, следует, что они не одинаково выразительны (я предпочитаю интерпретировать «мощный» как «выразительный», поскольку я думаю, что они на самом деле имеют в виду).

Vatine
источник
Смею сказатьs/quite hard/impossible/
@delnan Ну, я не пошел бы совсем так далеко, чтобы сказать «невозможно», но вы определенно должны работать на него.
Vatine
0

Это, безусловно, связано с функциональностью, но из декоратора Python нетрудно изменить вызываемый метод (это будет fпараметр в вашем примере). Чтобы изменить его, вы можете сойти с ума с модулем ast ), но вас может заинтересовать довольно сложное программирование.

Тем не менее, все сделано в этом направлении: посмотрите на пакет macropy , чтобы найти несколько действительно изумительных примеров.

Джейкоб Оскарсон
источник
3
Даже astвещи -трансформирующие в python не равны макросам Lisp. В Python исходным языком должен быть Python, а в макросах Lisp исходным языком, преобразованным макросом, может быть буквально что угодно. Поэтому метапрограммирование Python подходит только для простых вещей (например, AoP), в то время как метапрограммирование Lisp полезно для реализации мощных компиляторов eDSL.
SK-logic
1
Дело в том, что макропия не реализована с помощью декораторов. Он использует синтаксис декоратора (потому что он должен использовать правильный синтаксис Python), но он реализуется путем перехвата процесса компиляции байтов из ловушки импорта.
Ян Худек
@ SK-logic: В Лиспе исходный язык также должен быть лиспом. Синтаксис Just Lisp очень простой, но гибкий, в то время как синтаксис Python гораздо более сложный и не такой гибкий.
Ян Худек
1
@JanHudec, в исходном языке Lisp может быть любой (я имею в виду, любой ) синтаксис - см. Макросы читателя.
SK-logic