Decoradores Python e macros Lisp

18

Ao olhar para os decoradores de Python, alguém fez a afirmação de que eles são tão poderosos quanto as macros Lisp (principalmente o Clojure).

Olhando para os exemplos dados no PEP 318 , parece-me que eles são apenas uma maneira elegante de usar funções simples de ordem superior antigas no 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):
    ...

Não vi nenhum código se transformar em nenhum dos exemplos, como descrito em Anatomia de uma macro Clojure . Além disso, a falta de homoiconicidade do Python poderia impossibilitar as transformações de código.

Então, como esses dois se comparam e você pode dizer que eles são iguais no que você pode fazer? As evidências parecem apontar contra.

Editar: Com base em um comentário, estou procurando duas coisas: comparação entre "tão poderoso quanto" e "tão fácil de fazer coisas incríveis".

Profpatsch
fonte
12
Claro que os decoradores não são macros reais. Eles não podem traduzir uma linguagem arbitrária (com uma sintaxe totalmente diferente) em python. Quem afirma o contrário simplesmente não entende as macros.
SK-logic
1
Python não é homoicônico, no entanto, é muito, muito dinâmico. A homoiconicidade é necessária apenas para poderosas transformações de código se você quiser fazê-lo em tempo de compilação - se você tiver suporte para acesso direto ao AST compilado e ferramentas para alterá-lo, poderá fazê-lo em tempo de execução, independentemente da sintaxe do idioma. Dito isto, "tão poderoso quanto" e "tão fácil de fazer coisas incríveis" são conceitos muito diferentes.
Phoshi
Talvez eu deva mudar a pergunta para "tão fácil de fazer coisas incríveis" então. ;)
Profpatsch
Talvez alguém possa hackear alguma função comparável de ordem superior do Clojure ao exemplo do Python acima. Eu tentei, mas cruzei minha mente no processo. Como o exemplo do Python usa atributos de objeto, isso deve ser um pouco diferente.
Profpatsch # 9/13
@Phoshi A alteração do AST compilado em tempo de execução é conhecida como: código auto-modificável .
Kaz

Respostas:

16

Um decorador é basicamente apenas uma função .

Exemplo no Lisp comum:

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

Acima, a função é um símbolo (que seria retornado por DEFUN ) e colocamos os atributos na lista de propriedades do símbolo .

Agora podemos escrever em torno de uma definição de função:

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

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

)  

Se queremos adicionar uma sintaxe sofisticada como no Python, escrevemos uma macro de leitor . Uma macro de leitor nos permite programar no nível da sintaxe da expressão s:

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

Podemos então escrever:

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

O leitor Lisp lê acima para:

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

Agora, temos uma forma de decoradores no Common Lisp.

Combinando macros e macros de leitor.

Na verdade, eu faria a tradução acima em código real usando uma macro, não uma função.

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

O uso é como acima com a mesma macro do leitor. A vantagem é que o compilador Lisp ainda o vê como um formulário de nível superior - o compilador de arquivos * trata os formulários de nível superior especialmente, por exemplo, adiciona informações sobre eles no tempo de compilação ambiente de . No exemplo acima, podemos ver que a macro analisa o código-fonte e extrai o nome.

O leitor Lisp lê o exemplo acima em:

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

Que então é expandida para a macro:

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

Macros são muito diferentes das macros do leitor .

As macros passam o código fonte, podem fazer o que quiserem e depois retornar o código fonte. A fonte de entrada não precisa ser um código Lisp válido. Pode ser qualquer coisa e pode ser escrito totalmente diferente. O resultado deve ser um código Lisp válido. Mas se o código gerado também estiver usando uma macro, a sintaxe do código incorporado na chamada de macro poderá ser novamente uma sintaxe diferente. Um exemplo simples: pode-se escrever uma macro matemática que aceite algum tipo de sintaxe matemática:

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

A expressão y = 3 x ^ 2 - 4 x + 3não é um código Lisp válido, mas a macro poderia, por exemplo, analisá-la e retornar um código Lisp válido assim:

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

Existem muitos outros casos de uso de macros no Lisp.

Rainer Joswig
fonte
8

No Python (a linguagem), os decoradores não podem modificar a função, apenas envolvê-la, portanto são definitivamente muito menos poderosos do que as macros lisp.

No CPython (o intérprete), os decoradores podem modificar a função porque têm acesso ao bytecode, mas a função é compilada primeiro e pode ser manipulada pelo decorador, portanto, não é possível alterar a sintaxe, uma coisa lisp-macro -equivalente precisaria fazer.

Observe que os lisps modernos não usam expressões S como bytecode, portanto, as macros que trabalham nas listas de expressões S definitivamente funcionam antes da compilação do bytecode, conforme observado acima, em python o decorador corre atrás dele.

Jan Hudec
fonte
1
Você não precisa modificar a função. Você só precisa ler o código da função de alguma forma (na prática, isso significa código de código). Não que isso o torne mais prático.
2
@ delnan: Tecnicamente, o lisp também não está modificando; está usando-o como fonte para gerar um novo e, portanto, seria python, sim. O problema está na ausência de lista de token ou AST e no fato de o compilador já reclamar de algumas coisas que você poderia permitir na macro.
Jan Hudec
4

É muito difícil usar decoradores Python para introduzir novos mecanismos de fluxo de controle.

É quase trivial usar macros Common Lisp para introduzir novos mecanismos de controle de fluxo.

A partir disso, provavelmente decorre que eles não são igualmente expressivos (eu escolho interpretar "poderoso" como "expressivo", pois acho que eles realmente significam).

Vatine
fonte
Ouso dizers/quite hard/impossible/
@delnan Bem, eu não iria muito mais longe ao dizer "impossível", mas você definitivamente tem que trabalhar para isso.
Vatine 10/10
0

É certamente uma funcionalidade relacionada, mas, para um decorador Python, não é trivial modificar o método que está sendo chamado (esse seria o fparâmetro no seu exemplo). Para modificá-lo, você pode enlouquecer com o módulo ast ), mas precisará de uma programação bastante complicada.

No entanto , algumas coisas foram feitas: verifique o pacote de macropy para obter alguns exemplos realmente alucinantes.

Jacob Oscarson
fonte
3
Mesmo o astmaterial de transformação em python não é igual às macros Lisp. No Python, a linguagem de origem deve ser Python; nas macros Lisp, a linguagem de origem transformada por uma macro pode ser, literalmente, qualquer coisa. Portanto, a metaprogramação do Python é adequada apenas para coisas simples (como AoP), enquanto a metaprogramação do Lisp é útil para implementar poderosos compiladores eDSL.
SK-logic
1
O problema é que a macropia não é implementada usando decoradores. Ele usa a sintaxe do decorador (porque deve usar uma sintaxe python válida), mas é implementada ao seqüestrar o processo de compilação de bytes de um gancho de importação.
Jan Hudec
@ SK-logic: No Lisp, o idioma de origem também precisa ser lisp. A sintaxe Just Lisp é muito simples, mas flexível, enquanto a sintaxe python é muito mais complexa e não tão flexível.
Jan Hudec
1
@JanHudec na linguagem fonte Lisp pode ter qualquer (eu realmente quero dizer, qualquer ) sintaxe - veja macros leitor.
SK-logic