Что делает макросы Lisp такими особенными?


297

Читая эссе Пола Грэма о языках программирования, можно подумать, что макросы Lisp - единственный путь. Как занятый разработчик, работающий на других платформах, я не имел права использовать макросы Lisp. Как человек, который хочет понять гул, пожалуйста, объясните, что делает эту функцию такой мощной.

Пожалуйста, также свяжите это с тем, что я бы понял из мира разработки на Python, Java, C # или C.


2
Кстати, есть макропроцессор в стиле LISP для C #, который называется LeMP: ecsharp.net/lemp ... В JavaScript также есть такой, который называется Sweet.js: sweetjs.org
Qwertie

@Qwertie Sweetjs вообще работает в эти дни?
fredrik.hjarner

Я не использовал его, но последний коммит был шесть месяцев назад ... достаточно для меня!
Qwertie

Ответы:


308

Чтобы дать краткий ответ, макросы используются для определения языковых синтаксических расширений для Common Lisp или Domain-Specific Languages ​​(DSL). Эти языки встроены прямо в существующий код на Лиспе. Теперь DSL могут иметь синтаксис, подобный Lisp (например, интерпретатор Prolog Питера Норвига для Common Lisp) или совершенно другой (например, Infix Notation Math для Clojure).

Вот более конкретный пример:
Python имеет встроенные в язык списки. Это дает простой синтаксис для общего случая. Линия

divisibleByTwo = [x for x in range(10) if x % 2 == 0]

выдает список, содержащий все четные числа от 0 до 9. В Python 1,5 дня не было такого синтаксиса; вы бы использовали что-то вроде этого:

divisibleByTwo = []
for x in range( 10 ):
   if x % 2 == 0:
      divisibleByTwo.append( x )

Оба они функционально эквивалентны. Давайте вызовем нашу приостановку неверия и притворимся, что у Лиспа очень ограниченный макрос цикла, который просто выполняет итерации, и нет простого способа сделать эквивалентную работу со списком.

В Лиспе вы могли бы написать следующее. Я должен отметить, что этот надуманный пример подобран к коду Python, а не к хорошему примеру кода на Лиспе.

;; the following two functions just make equivalent of Python's range function
;; you can safely ignore them unless you are running this code
(defun range-helper (x)
  (if (= x 0)
      (list x)
      (cons x (range-helper (- x 1)))))

(defun range (x)
  (reverse (range-helper (- x 1))))

;; equivalent to the python example:
;; define a variable
(defvar divisibleByTwo nil)

;; loop from 0 upto and including 9
(loop for x in (range 10)
   ;; test for divisibility by two
   if (= (mod x 2) 0) 
   ;; append to the list
   do (setq divisibleByTwo (append divisibleByTwo (list x))))

Прежде чем идти дальше, я должен лучше объяснить, что такое макрос. Это преобразование, выполняемое для кода за кодом. То есть фрагмент кода, читаемый интерпретатором (или компилятором), который принимает код в качестве аргумента, манипулирует и возвращает результат, который затем запускается на месте.

Конечно, это много печатать, и программисты ленивы. Таким образом, мы могли бы определить DSL для выполнения списка. Фактически, мы уже используем один макрос (макрос цикла).

Лисп определяет пару специальных синтаксических форм. Кавычка ( ') указывает, что следующий токен является литералом. Квазицитат или backtick ( `) указывает, что следующий токен является литералом с escape-символами . Побеги указаны оператором запятой. Литерал '(1 2 3)является эквивалентом Python [1, 2, 3]. Вы можете назначить его другой переменной или использовать на месте. Вы можете думать `(1 2 ,x)как эквивалент Python's, [1, 2, x]где xпеременная определена ранее. Эта запись списка является частью магии, которая входит в макросы. Вторая часть - читатель Lisp, который разумно заменяет макросы на код, но это лучше всего показано ниже:

Таким образом, мы можем определить макрос с именем lcomp(сокращение от понимания списка). Его синтаксис будет точно таким же, как у питона, который мы использовали в примере [x for x in range(10) if x % 2 == 0] -(lcomp x for x in (range 10) if (= (% x 2) 0))

(defmacro lcomp (expression for var in list conditional conditional-test)
  ;; create a unique variable name for the result
  (let ((result (gensym)))
    ;; the arguments are really code so we can substitute them 
    ;; store nil in the unique variable name generated above
    `(let ((,result nil))
       ;; var is a variable name
       ;; list is the list literal we are suppose to iterate over
       (loop for ,var in ,list
            ;; conditional is if or unless
            ;; conditional-test is (= (mod x 2) 0) in our examples
            ,conditional ,conditional-test
            ;; and this is the action from the earlier lisp example
            ;; result = result + [x] in python
            do (setq ,result (append ,result (list ,expression))))
           ;; return the result 
       ,result)))

Теперь мы можем выполнить в командной строке:

CL-USER> (lcomp x for x in (range 10) if (= (mod x 2) 0))
(0 2 4 6 8)

Довольно аккуратно, а? Теперь это не останавливается там. У вас есть механизм или кисть, если хотите. Вы можете иметь любой синтаксис, какой только захотите. Как и withсинтаксис Python или C # . Или .NET синтаксис LINQ. В конце концов, это то, что привлекает людей в Lisp - максимальная гибкость.


51
+1 для реализации понимания списка в Лиспе, потому что почему бы и нет?
ckb

9
@ckb На самом деле LISP уже есть список понимание макроса в стандартной библиотеке: (loop for x from 0 below 10 when (evenp x) collect x), больше примеров здесь . Но на самом деле, цикл - это «просто макрос» (я фактически заново реализовал его с нуля )
Сюзанна Дюперон,

8
Я знаю, что это довольно не связано, но мне интересно узнать синтаксис и как на самом деле работает синтаксический анализ ... Скажем, я называю lcomp таким образом (меняя элемент thirs с "for" на "azertyuiop"): (lcomp x azertyuiop x in ( диапазон 10) если (= (% x 2) 0)) будет ли макрос работать должным образом? Или параметр «for» используется в цикле, так что при вызове он должен быть строкой «for»?
Дадер

2
@dader Итак, цикл - это на самом деле другой макрос, а for - это специальный символ, используемый в этом макросе. Я мог бы легко определить макрос без макроса цикла. Однако, если бы я это сделал, пост был бы намного длиннее. Макрос цикла и весь его синтаксис определен в CLtL .
gte525u

3
Одна вещь, в которой я путаюсь с макросами другого языка, это то, что их макросы ограничены синтаксисом основного языка. Могут ли макросы Lispy интерпретировать не-Lispy синтаксис. Я имею в виду, как представить себе создание синтаксиса, похожего на haskell (без парантезы), и интерпретировать его с помощью макросов Lisp. Возможно ли это, и каковы плюсы / минусы использования макросов по сравнению с использованием лексера и парсера напрямую?
CMCDragonkai

105

Вы найдете исчерпывающую дискуссию о макросе LISP здесь .

Интересное подмножество этой статьи:

В большинстве языков программирования синтаксис сложен. Макросы должны разбирать синтаксис программы, анализировать и собирать его. У них нет доступа к анализатору программы, поэтому они должны зависеть от эвристики и лучших предположений. Иногда их анализ снижения ставки ошибочен, и тогда они ломаются.

Но Лисп другой. Lisp макросов сделать доступ к синтаксическому анализатору, и это очень простой синтаксический анализатор. Макрос на Лиспе - это не строка, а подготовленный фрагмент исходного кода в виде списка, потому что источником программы на Лиспе не является строка; это список. И программы на Лиспе действительно хороши для разделения списков и их объединения. Они делают это надежно, каждый день.

Вот расширенный пример. В Лиспе есть макрос, называемый "setf", который выполняет присваивание. Простейшая форма setf

  (setf x whatever)

который устанавливает значение символа «x» на значение выражения «что угодно».

Лисп также имеет списки; Вы можете использовать функции "car" и "cdr", чтобы получить первый элемент списка или остальную часть списка соответственно.

А что если вы хотите заменить первый элемент списка новым значением? Для этого есть стандартная функция, и невероятно, ее название даже хуже, чем «машина». Это «rplaca». Но вам не нужно помнить «rplaca», потому что вы можете написать

  (setf (car somelist) whatever)

установить машину из какого-то списка.

Что действительно происходит здесь, так это то, что setf - это макрос. Во время компиляции он проверяет свои аргументы и видит, что первый имеет форму (автомобиль НЕЧТО). Он говорит сам себе: «О, программист пытается что-то настроить. Для этого используется функция« rplaca »». И он спокойно переписывает код на месте, чтобы:

  (rplaca somelist whatever)

5
setf - хорошая иллюстрация силы макросов, спасибо за включение этого.
Джоэл

Мне нравится выделение .. потому что источником программы на Лиспе не является строка; это список. ! Является ли это основной причиной, по которой макрос LISP превосходит большинство других из-за своих скобок?
Студент

@ Студент Полагаю, что так: books.google.fr/… предполагает, что вы правы.
VonC

55

Общие макросы Lisp существенно расширяют «синтаксические примитивы» вашего кода.

Например, в C конструкция switch / case работает только с целочисленными типами, и если вы хотите использовать ее для чисел с плавающей запятой или строк, у вас останутся вложенные операторы if и явные сравнения. Также нет способа написать макрос на C, который сделает эту работу за вас.

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

Чтобы сделать то же самое в C, вы должны написать собственный препроцессор, который съест ваш исходный (не совсем C) источник и выдаст то, что может понять компилятор C. Это не неправильный способ сделать это, но это не обязательно самый простой.


41

Макросы Lisp позволяют вам решить, когда (если вообще будет) любая часть или выражение будет оцениваться. Чтобы привести простой пример, подумайте о C:

expr1 && expr2 && expr3 ...

Что это говорит: оценивать expr1, и, если это правда, оценивать expr2и т. Д.

Теперь попробуйте превратить это &&в функцию ... верно, вы не можете. Вызов что-то вроде:

and(expr1, expr2, expr3)

Оценим все три, exprsпрежде чем дать ответ, независимо от того, expr1был ли ложным!

С помощью макросов LISP вы можете написать что-то вроде:

(defmacro && (expr1 &rest exprs)
    `(if ,expr1                     ;` Warning: I have not tested
         (&& ,@exprs)               ;   this and might be wrong!
         nil))

теперь у вас есть &&функция, которую вы можете вызвать точно так же, как функция, и она не будет оценивать любые формы, которые вы ей передадите, если они все не верны.

Чтобы увидеть, как это полезно, контрастируйте:

(&& (very-cheap-operation)
    (very-expensive-operation)
    (operation-with-serious-side-effects))

и:

and(very_cheap_operation(),
    very_expensive_operation(),
    operation_with_serious_side_effects());

Другие вещи, которые вы можете делать с макросами, это создание новых ключевых слов и / или мини-языков (посмотрите (loop ...)макрос для примера), интеграция других языков в lisp, например, вы можете написать макрос, который позволит вам сказать что-то вроде:

(setvar *rows* (sql select count(*)
                      from some-table
                     where column1 = "Yes"
                       and column2 like "some%string%")

И это даже не входит в макросы Reader .

Надеюсь это поможет.


Я думаю, что это должно быть: "(применить &&, @ exprs); это и может быть неправильно!"
Сванте

1
@svante - по двум причинам: во-первых, && - это макрос, а не функция; применять только работает на функции. во-вторых, применить принять список аргументов для передачи, поэтому вам нужен один из «(funcall fn, @ exprs)», «(apply fn (list, @ exprs)» или «(apply fn, @ exprs nil)», а не «(применить fn, @ exprs)».
Аарон,

(and ...будет оценивать выражения до тех пор, пока одно из них не станет ложным, обратите внимание, что побочные эффекты, вызванные ложной оценкой, будут иметь место, только последующие выражения будут пропущены.
ocodo

31

Я не думаю, что когда-либо видел макросы Lisp, объясненные лучше, чем этот парень: http://www.defmacro.org/ramblings/lisp.html


3
Особенно, если у вас есть фон Java / XML.
sunsations

1
Какая радость читать это, лежа на моем диване в субботу днем! Очень четко написано и организовано.
Колин

10

Подумайте, что вы можете сделать в C или C ++ с помощью макросов и шаблонов. Это очень полезные инструменты для управления повторяющимся кодом, но они ограничены довольно серьезными способами.

  • Ограниченный синтаксис макросов / шаблонов ограничивает их использование. Например, вы не можете написать шаблон, который расширяется до чего-то другого, кроме класса или функции. Макросы и шаблоны не могут легко поддерживать внутренние данные.
  • Сложный, очень нерегулярный синтаксис C и C ++ затрудняет написание очень общих макросов.

Макросы Lisp и Lisp решают эти проблемы.

  • Макросы Lisp написаны на Lisp. У вас есть все возможности Lisp для написания макроса.
  • Лисп имеет очень регулярный синтаксис.

Поговорите с кем-нибудь, кто освоил C ++, и спросите его, сколько времени они потратили на изучение всех шаблонных фальшивок, необходимых для метапрограммирования шаблонов. Или все сумасшедшие уловки в (превосходных) книгах, таких как Modern C ++ Design , которые все еще трудно отлаживать и (на практике) не переносимы между реальными компиляторами, даже если язык был стандартизирован в течение десятилетия. Все это исчезает, если язык, который вы используете для метапрограммирования, - это тот же язык, который вы используете для программирования!


11
Ну, честно говоря, проблема с метапрограммированием шаблонов в C ++ состоит не в том, что язык метапрограммирования отличается , а в том, что он ужасен - он был не столько разработан, сколько обнаружен в том, что предполагалось сделать намного более простой функциональностью шаблона.
Брукс Моисей

@ Брукс Конечно. Новые функции не всегда плохи. К сожалению, на медленном языке, управляемом комитетом, их трудно исправить, когда они есть. Обидно, что многие современные полезные новые функции С ++ написаны на языке, который мало кто может прочитать, и существует огромный разрыв между обычным программистом и «первосвященником».
Мэтт Кертис

2
@ downvoter: если что-то не так с моим ответом, пожалуйста, оставьте комментарий, чтобы мы все могли поделиться своими знаниями.
Мэтт Кертис

10

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

C # не имеет возможности макросов, однако эквивалент был бы, если бы компилятор анализировал код в дереве CodeDOM и передавал его методу, который преобразовывал это в другой CodeDOM, который затем компилировался в IL.

Это может быть использовано для реализации синтаксиса "сахара", такого как for each -statement using-clause, linq select-expressions и т. Д., В качестве макросов, преобразующихся в базовый код.

Если бы в Java были макросы, вы могли бы реализовать синтаксис Linq в Java без необходимости изменения базового языка Sun.

Вот псевдокод того, как usingможет выглядеть макрос в стиле Lisp для реализации :

define macro "using":
    using ($type $varname = $expression) $block
into:
    $type $varname;
    try {
       $varname = $expression;
       $block;
    } finally {
       $varname.Dispose();
    }

Теперь, когда на самом деле есть макропроцессор в стиле Lisp для C #, я хотел бы отметить, что макрос for usingбудет выглядеть так ;)
Qwertie

9

Поскольку существующие ответы дают хорошие конкретные примеры, объясняющие, что макросы достигают и как, возможно, это помогло бы собрать воедино некоторые мысли о том, почему средство макросов является значительным преимуществом по сравнению с другими языками ; сначала из этих ответов, потом из другого:

... в C вам придется написать собственный препроцессор [который, вероятно, будет квалифицирован как достаточно сложная программа на C ] ...

- ватин

Поговорите с кем-нибудь, кто освоил C ++, и спросите его, сколько времени они потратили на изучение всей шаблонной фудгерики, необходимой для выполнения метапрограммирования шаблонов [которое все еще не так эффективно].

- Мэтт Кертис

... в Java вам придется взломать свой путь с помощью переплетения байт-кода, хотя некоторые фреймворки, такие как AspectJ, позволяют делать это с использованием другого подхода, это принципиально хак.

- Мигель Пинг

DOLIST похож на foreach Perl или для Python. Java добавила подобный вид конструкции цикла с «улучшенным» циклом for в Java 1.5, как часть JSR-201. Обратите внимание на разницу в макросах. Программист на Лиспе, который замечает общий шаблон в своем коде, может написать макрос, чтобы получить абстракцию этого шаблона на уровне исходного кода. Java-программист, который замечает тот же шаблон, должен убедить Sun, что эта конкретная абстракция стоит добавить в язык. Затем Sun должна опубликовать JSR и создать отраслевую «экспертную группу», чтобы все прояснить. Этот процесс, согласно Sun, занимает в среднем 18 месяцев. После этого все авторы компиляторов должны обновить свои компиляторы для поддержки новой функции. И даже когда любимый компилятор Java-программиста поддерживает новую версию Java, они, вероятно, «по-прежнему» не могут использовать новую функцию, пока им не разрешат нарушить совместимость исходного кода со старыми версиями Java. Таким образом, раздражение, которое программисты Common Lisp могут решить для себя в течение пяти минут, мучает программистов на Java годами.

- Питер Сейбел, в "Практическом Обыкновенном Лиспе"


8

Я не уверен, что могу добавить некоторое понимание к всем (превосходным) сообщениям, но ...

Макросы Lisp прекрасно работают из-за синтаксической природы Lisp.

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

Редактировать: я пытался сказать, что Лисп гомоичен , что означает, что структура данных для программы LISP написана в самом LISP.

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

На практике, используя макросы, вы в конечном итоге создаете свой собственный мини-язык поверх lisp, без необходимости изучать дополнительные языки или инструменты, а также используя все возможности самого языка.


1
Проницательный комментарий, однако, идея о том, что «все есть список», может напугать новичков. Чтобы понять список, нужно понимать минусы, машины, CDR, ячейки. Точнее, Лисп составлен из S-expressions, а не списков.
Рибамар

6

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

В Python объекты имеют два метода __repr__и __str__. __str__это просто читабельное представление человека. __repr__возвращает представление, которое является допустимым кодом Python, то есть что-то, что может быть введено в интерпретатор как действительный Python. Таким образом, вы можете создавать небольшие фрагменты Python, которые генерируют действительный код, который можно вставить в ваш исходный код.

В Лиспе весь этот процесс формализован системой макросов. Конечно, это позволяет вам создавать расширения для синтаксиса и делать всевозможные причудливые вещи, но его фактическая полезность суммируется вышеизложенным. Конечно, помогает то, что макросистема Lisp позволяет вам манипулировать этими «фрагментами» с полной мощью всего языка.


1
Ваш первый абзац очень ясен для постороннего Лиспа, что важно.
Подстановочный

5

Короче говоря, макросы - это преобразования кода. Они позволяют вводить много новых синтаксических конструкций. Например, рассмотрим LINQ в C #. В lisp существуют похожие языковые расширения, которые реализуются макросами (например, встроенная конструкция цикла, итерация). Макросы значительно уменьшают дублирование кода. Макросы позволяют встраивать «маленькие языки» (например, когда в c # / java для настройки используется xml, в lisp то же самое может быть достигнуто с помощью макросов). Макросы могут скрыть трудности использования библиотек.

Например, в LISP вы можете написать

(iter (for (id name) in-clsql-query "select id, name from users" on-database *users-database*)
      (format t "User with ID of ~A has name ~A.~%" id name))

и это скрывает все содержимое базы данных (транзакции, правильное закрытие соединения, выборку данных и т. д.), тогда как в C # это требует создания SqlConnections, SqlCommands, добавления SqlParameters к SqlCommands, зацикливания на SqlDataReaders, правильного их закрытия.


3

Хотя все вышесказанное объясняет, что такое макросы и даже имеют классные примеры, я думаю, что ключевое отличие между макросом и нормальной функцией заключается в том, что LISP сначала оценивает все параметры перед вызовом функции. С макросом все наоборот, LISP передает параметры, не оцененные, в макрос. Например, если вы передадите (+ 1 2) функции, функция получит значение 3. Если вы передадите это макросу, она получит список (+ 1 2). Это может быть использовано для создания невероятно полезных вещей.

  • Добавление новой структуры управления, например, цикл или деконструкция списка
  • Измерьте время, необходимое для выполнения переданной функции. С функцией параметр будет оцениваться до того, как управление будет передано функции. С помощью макроса вы можете объединить ваш код между началом и остановкой вашего секундомера. Ниже приведен точно такой же код в макросе и функции, и результат очень отличается. Примечание: это надуманный пример, и реализация была выбрана так, чтобы она была идентична, чтобы лучше подчеркнуть разницу.

    (defmacro working-timer (b) 
      (let (
            (start (get-universal-time))
            (result (eval b))) ;; not splicing here to keep stuff simple
        ((- (get-universal-time) start))))
    
    (defun my-broken-timer (b)
      (let (
            (start (get-universal-time))
            (result (eval b)))    ;; doesn't even need eval
        ((- (get-universal-time) start))))
    
    (working-timer (sleep 10)) => 10
    
    (broken-timer (sleep 10)) => 0

Кстати, Scala добавил макросы в язык. Хотя им не хватает красоты макросов Lisp, потому что язык не гомоичен, им определенно стоит взглянуть на него, и абстрактные синтаксические деревья, которые они предоставляют, могут быть проще использовать в конце. Мне пока рано говорить, какую макросистему я предпочитаю.
Йорг Шмюкер

2
«LISP передает параметры, недооцененные макросу», наконец, ответ, который говорит об этом прямо. но вы забыли вторую половину этого: и результатом макроса является преобразованный код, который будет оцениваться системой целиком вместо оригинала, как если бы он был там в первую очередь (если только он сам по себе снова не вызовет макрос, который также будет преобразован этим макросом на этот раз).
Уилл Несс

0

Я получил это из обычной кулинарной книги LISP, но я думаю, что она объяснила, почему макросы LISP хороши в хорошем смысле.

«Макрос - это обычный фрагмент кода на Лиспе, который работает с другим фрагментом предполагаемого кода на Лиспе, переводя его в исполняемый Лисп (версия ближе к нему). Это может показаться немного сложным, поэтому давайте приведем простой пример. Предположим, вы хотите версия setq, которая устанавливает две переменные в одно и то же значение. Так что если вы напишите

(setq2 x y (+ z 3))

когда z=8и x, и y установлены на 11. (Я не могу думать об этом, но это всего лишь пример.)

Должно быть очевидно, что мы не можем определить setq2 как функцию. Если x=50и y=-5, эта функция получит значения 50, -5 и 11; он не знал бы, какие переменные должны быть установлены. Что мы действительно хотим сказать, так это то, что когда вы (система Lisp) видите (setq2 v1 v2 e), относитесь к нему как к эквивалентному (progn (setq v1 e) (setq v2 e)). На самом деле, это не совсем правильно, но пока подойдет. Макрос позволяет нам сделать именно это, указав программу для преобразования входного шаблона (setq2 v1 v2 e)«в выходной шаблон (progn ...)».

Если вы подумали, что это хорошо, вы можете прочитать здесь: http://cl-cookbook.sourceforge.net/macros.html


1
Можно определить setq2как функцию, если xи yпередаются по ссылке. Однако я не знаю, возможно ли это в CL. Так что для тех, кто не знает Lisps или CL, в частности, это не очень показательный пример IMO
неоаскет

Передача аргументов @neoascetic CL осуществляется только по значению (поэтому в первую очередь нужны макросы). хотя некоторые значения являются указателями (например, списки).
Уилл Несс

-5

В Python у вас есть декораторы, у вас есть функция, которая принимает другую функцию в качестве входных данных. Вы можете делать все, что захотите: вызывать функцию, делать что-то еще, переносить вызов функции в выпуске получения ресурсов и т. Д., Но вы не можете заглянуть внутрь этой функции. Скажем, мы хотели сделать его более мощным, скажем, ваш декоратор получил код функции в виде списка, тогда вы могли не только выполнять функцию как есть, но теперь вы можете выполнять ее части, переупорядочивать строки функции и т. Д.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.