Décorateurs Python et macros Lisp

18

Lors de la recherche de décorateurs Python, quelqu'un a déclaré qu'ils étaient aussi puissants que les macros Lisp (en particulier Clojure).

En regardant les exemples donnés dans PEP 318, il me semble que ce ne sont que des moyens sophistiqués d'utiliser de vieilles fonctions simples d'ordre supérieur dans 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):
    ...

Je n'ai vu aucun code se transformer dans aucun des exemples, comme décrit dans Anatomy of a Clojure Macro . De plus, l' homoiconicité manquante de Python pourrait rendre les transformations de code impossibles.

Alors, comment ces deux se comparent-ils et pouvez-vous dire qu'ils sont à peu près égaux dans ce que vous pouvez faire? Les preuves semblent aller contre.

Edit: Sur la base d'un commentaire, je cherche deux choses: une comparaison sur "aussi puissant que" et sur "aussi facile à faire des choses impressionnantes avec".

Profpatsch
la source
12
Bien sûr, les décorateurs ne sont pas de vraies macros. Ils ne peuvent pas traduire un langage arbitraire (avec une syntaxe totalement différente) en python. Ceux qui prétendent le contraire ne comprennent tout simplement pas les macros.
SK-logic
1
Python n'est pas homoiconique, mais il est très très dynamique. L'homoiconicité n'est requise pour les transformations de code puissantes que si vous voulez le faire au moment de la compilation - si vous avez un accès direct à l'AST compilé et aux outils pour le modifier, vous pouvez le faire au moment de l'exécution, quelle que soit la syntaxe du langage. Cela étant dit, "aussi puissant que" et "aussi facile à faire des choses impressionnantes avec" sont des concepts très différents.
Phoshi
Peut-être devrais-je alors changer la question en «aussi facile de faire des choses incroyables». ;)
Profpatsch
Peut-être que quelqu'un peut pirater une fonction d'ordre supérieur Clojure comparable à l'exemple Python ci-dessus. J'ai essayé mais sillonné mon esprit dans le processus. Étant donné que l'exemple Python utilise des attributs d'objet, cela doit être un peu différent.
Profpatsch
@Phoshi La modification de l'AST compilé au moment de l'exécution est connue sous le nom de: code auto-modifiant .
Kaz

Réponses:

16

Un décorateur est fondamentalement juste une fonction .

Exemple en Common Lisp:

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

Ci-dessus, la fonction est un symbole (qui serait retourné par DEFUN) et nous mettons les attributs sur la liste des propriétés du symbole .

Maintenant, nous pouvons l'écrire autour d'une définition de fonction:

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

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

)  

Si nous voulons ajouter une syntaxe sophistiquée comme en Python, nous écrivons une macro de lecture . Une macro de lecture nous permet de programmer au niveau de la syntaxe de l'expression s:

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

On peut alors écrire:

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

Le Lisp lecteur lit ci-dessus:

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

Nous avons maintenant une forme de décorateurs en Common Lisp.

Combinaison de macros et de macros de lecture.

En fait, je ferais la traduction ci-dessus en code réel en utilisant une macro, pas une fonction.

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

L'utilisation est la même que ci-dessus avec la même macro de lecteur. L'avantage est que le compilateur Lisp le considère toujours comme un formulaire dit de niveau supérieur - le compilateur de fichiers * traite spécialement les formulaires de niveau supérieur, par exemple, il ajoute des informations à leur sujet au moment de la compilation environnement de . Dans l'exemple ci-dessus, nous pouvons voir que la macro examine le code source et extrait le nom.

Le lecteur Lisp lit l'exemple ci-dessus dans:

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

Qui obtient ensuite une macro développée en:

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

Les macros sont très différentes des macros de lecture .

Les macros font passer le code source, peuvent faire ce qu'elles veulent, puis renvoyer le code source. La source d'entrée n'a pas besoin d'être un code Lisp valide. Ça peut être n'importe quoi et ça pourrait être écrit totalement différent. Le résultat doit alors être un code Lisp valide. Mais si le code généré utilise également une macro, la syntaxe du code incorporé dans l'appel de macro pourrait à nouveau être une syntaxe différente. Un exemple simple: on pourrait écrire une macro mathématique qui accepterait une sorte de syntaxe mathématique:

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

L'expression y = 3 x ^ 2 - 4 x + 3n'est pas du code Lisp valide, mais la macro pourrait par exemple l'analyser et retourner du code Lisp valide comme ceci:

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

Il existe de nombreux autres cas d'utilisation des macros en Lisp.

Rainer Joswig
la source
8

En Python (le langage), les décorateurs ne peuvent pas modifier la fonction, ils ne font que l'envelopper, ils sont donc nettement moins puissants que les macros lisp.

Dans CPython (l'interpréteur), les décorateurs peuvent modifier la fonction car ils ont accès au bytecode, mais la fonction est compilée en premier et peut être manipulée par le décorateur, il n'est donc pas possible de modifier la syntaxe, une chose lisp-macro -quivalent devrait faire.

Notez que les lisps modernes n'utilisent pas les expressions S comme bytecode, donc les macros travaillant sur les listes d'expressions S fonctionnent définitivement avant la compilation de bytecode comme indiqué ci-dessus, en python, le décorateur s'exécute après.

Jan Hudec
la source
1
Vous n'avez pas besoin de modifier la fonction. Il vous suffit de lire le code de la fonction sous une forme (en pratique, cela signifie bytecode). Non pas que cela le rende plus pratique.
2
@delnan: Techniquement, lisp ne le modifie pas non plus; il l'utilise comme source pour en générer un nouveau, tout comme python, oui. Le problème réside dans l'absence de liste de jetons ou d'AST et le fait que le compilateur s'est déjà plaint de certaines choses que vous pourriez autrement autoriser dans la macro.
Jan Hudec
4

Il est assez difficile d'utiliser des décorateurs Python pour introduire de nouveaux mécanismes de flux de contrôle.

Utiliser les macros Common Lisp pour introduire de nouveaux mécanismes de flux de contrôle est à la limite de la simplicité.

De cela, il s'ensuit probablement qu'ils ne sont pas également expressifs (je choisis d'interpréter "puissant" comme "expressif", car je pense que ce qu'ils signifient réellement).

Vatine
la source
J'ose dires/quite hard/impossible/
@delnan Eh bien, je n'irais pas assez jusqu'à dire « impossible », mais vous auriez certainement devoir travailler.
Vatine du
0

C'est certainement une fonctionnalité associée, mais à partir d'un décorateur Python, il n'est pas trivial de modifier la méthode appelée (ce serait le fparamètre dans votre exemple). Pour le modifier, vous pourriez devenir fou avec le module ast ), mais vous seriez dans une programmation assez compliquée.

Cependant, des choses ont été faites dans ce sens: consultez le package macropy pour des exemples vraiment hallucinants .

Jacob Oscarson
la source
3
Même le asttruc de transformation en python n'est pas égal aux macros Lisp. Avec Python, le langage source doit être Python, avec les macros Lisp, le langage source transformé par une macro peut être, littéralement, n'importe quoi. Par conséquent, la métaprogrammation Python ne convient que pour des choses simples (comme l'AoP), tandis que la métaprogrammation Lisp est utile pour implémenter de puissants compilateurs eDSL.
SK-logic
1
Le fait est que la macropie n'est pas implémentée à l'aide de décorateurs. Il utilise la syntaxe du décorateur (car il doit utiliser une syntaxe python valide), mais il est implémenté en détournant le processus de compilation d'octets à partir d'un hook d'importation.
Jan Hudec
@ SK-logic: En Lisp, la langue source doit également être lisp. La syntaxe Just Lisp est très simple mais flexible, tandis que la syntaxe python est beaucoup plus complexe et moins flexible.
Jan Hudec
1
@JanHudec, en langage source Lisp peut avoir n'importe quelle (je veux dire vraiment, n'importe quelle ) syntaxe - voir macros de lecture.
SK-logic