ChatGPT解决这个技术问题 Extra ChatGPT

是什么让 Lisp 宏如此特别?

阅读有关编程语言的 Paul Graham's essays 会认为 Lisp macros 是唯一的出路。作为一个忙碌的开发人员,在其他平台上工作,我没有使用 Lisp 宏的特权。作为一个想了解嗡嗡声的人,请解释一下是什么让这个功能如此强大。

还请将此与我从 Python、Java、C# 或 C 开发领域理解的内容联系起来。

顺便说一句,有一个用于 C# 的 LISP 风格的宏处理器,名为 LeMP:ecsharp.net/lemp ... JavaScript 也有一个名为 Sweet.js:sweetjs.org
@Qwertie这些天sweetjs还能工作吗?
我没有使用它,但最近的提交是六个月前......对我来说已经足够了!

g
gilch

简而言之,宏用于定义 Common Lisp 或领域特定语言 (DSL) 的语言语法扩展。这些语言直接嵌入到现有的 Lisp 代码中。现在,DSL 可以具有类似于 Lisp 的语法(例如 Peter Norvig 的用于 Common Lisp 的 Prolog Interpreter)或完全不同的语法(例如 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 )

它们在功能上是等效的。让我们调用我们的怀疑暂停,并假设 Lisp 有一个非常有限的循环宏,它只进行迭代,而没有简单的方法来完成列表推导的等价。

在 Lisp 中,您可以编写以下内容。我应该注意到这个人为的例子被选为与 Python 代码相同,而不是 Lisp 代码的一个很好的例子。

;; 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 来进行列表推导。事实上,我们已经在使用一个宏(循环宏)。

Lisp 定义了一些特殊的语法形式。引号 (') 表示下一个标记是文字。准引号或反引号 (`) 表示下一个标记是带有转义的文字。转义符由逗号运算符指示。文字 '(1 2 3) 相当于 Python 的 [1, 2, 3]。您可以将其分配给另一个变量或就地使用它。您可以将 `(1 2 ,x) 视为 Python 的 [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)) 中使用的 python 完全相同

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

很整洁吧?现在它不止于此。如果你愿意,你有一个机制,或者一个画笔。你可以有任何你可能想要的语法。类似于 Python 或 C# 的 with 语法。或 .NET 的 LINQ 语法。最后,这就是 Lisp 吸引人们的地方——终极的灵活性。


+1 用于在 Lisp 中实现列表理解,因为为什么不呢?
@ckb 实际上 LISP 在标准库中已经有一个列表理解宏:(loop for x from 0 below 10 when (evenp x) collect x)more examples here。但实际上,循环是“只是一个宏”(我实际上是 re-implemented it from scratch some time ago
我知道这很不相关,但是我想知道语法以及解析的实际工作原理...假设我以这种方式调用 lcomp (将 thirs 项目从“for”更改为“azertyuiop”):(lcomp x azertyuiop x in ( range 10) if (= (% x 2) 0)) 宏还会按预期工作吗?还是在循环中使用了“for”参数,以便在调用时它必须是字符串“for”?
我对其他语言的宏感到困惑的一件事是,它们的宏受到宿主语言语法的限制。 Lispy 宏可以解释非 Lispy 语法吗?我的意思是想象创建一个类似 haskell 的语法(没有括号)并使用 Lisp 宏解释它。这可能吗,与直接使用词法分析器和解析器相比,使用宏的优缺点是什么?
@CMCDragonkai 简单的回答,是的,lisp 宏通常用于创建领域特定语言。宿主语言总是对可以在宏中使用的语法施加一些限制。例如,您显然不能将注释语法用作宏中的活动组件。
C
Community

您会发现围绕 lisp macro here 的全面辩论。

那篇文章的一个有趣的子集:

在大多数编程语言中,语法很复杂。宏必须分解程序语法,分析它,然后重新组合它。他们无法访问程序的解析器,因此他们必须依赖启发式和最佳猜测。有时他们的降价分析是错误的,然后他们就破产了。但 Lisp 不同。 Lisp 宏确实可以访问解析器,它是一个非常简单的解析器。一个 Lisp 宏不是交给一个字符串,而是一个预先解析的以列表形式存在的源代码,因为 Lisp 程序的源不是一个字符串;这是一个列表。 Lisp 程序非常擅长分解列表并将它们重新组合在一起。他们每天都可靠地做到这一点。这是一个扩展示例。 Lisp 有一个宏,叫做“setf”,它执行赋值。 setf 的最简单形式是 (setf x whatever),它将符号“x”的值设置为表达式“whatever”的值。 Lisp 也有列表。您可以使用“car”和“cdr”函数分别获取列表的第一个元素或列表的其余部分。现在,如果您想用新值替换列表的第一个元素怎么办?有一个标准函数可以做到这一点,令人难以置信的是,它的名字甚至比“汽车”还要糟糕。它是“rplaca”。但是您不必记住“rplaca”,因为您可以编写 (setf (car somelist) 不管) 来设置 somelist 的汽车。这里真正发生的是“setf”是一个宏。在编译时,它检查它的参数,并发现第一个参数的形式为 (car SOMETHING)。它对自己说:“哦,程序员正在尝试设置汽车。为此使用的函数是'rplaca'。”它悄悄地将代码重写为:(rplaca somelist 不管)


setf 很好地说明了宏的力量,感谢您将其包含在内。
我喜欢高亮 ..因为 Lisp 程序的源代码不是字符串;这是一个清单。!这是否是 LISP 宏因其括号而优于大多数其他宏的主要原因?
@Student 我想是的:books.google.fr/… 表明你是对的。
V
Vatine

Common Lisp 宏本质上扩展了代码的“句法原语”。

例如,在 C 中,switch/case 构造仅适用于整数类型,如果您想将其用于浮点数或字符串,则剩下嵌套的 if 语句和显式比较。您也无法编写 C 宏来为您完成这项工作。

但是,由于 lisp 宏(本质上)是一个 lisp 程序,它将代码片段作为输入并返回代码以替换宏的“调用”,因此您可以根据需要扩展您的“原始”曲目,通常结束具有更易读的程序。

要在 C 中做同样的事情,您必须编写一个自定义预处理器,它会吃掉您的初始(不是完全 C)源代码并吐出 C 编译器可以理解的东西。这不是一个错误的方法,但它不一定是最简单的。


+!对于以“但是,因为 lisp 宏是……”开头的段落,因为这比其他任何事情都更清楚地说明了整个主题!
d
dsm

Lisp 宏允许您决定何时(如果有的话)评估任何部分或表达式。举一个简单的例子,想想C:

expr1 && expr2 && expr3 ...

这就是说:评估 expr1,如果它是真的,评估 expr2,等等。

现在试着把这个 && 变成一个函数……没错,你不能。调用类似的东西:

and(expr1, expr2, expr3)

无论 expr1 是否为假,都会在得出答案之前评估所有三个 exprs

使用 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 macros

希望这可以帮助。


我认为应该是:“(apply && ,@exprs);这可能是错误的!”
@svante - 有两个方面:首先, && 是一个宏,而不是一个函数; apply 仅适用于函数。其次,应用要传递的参数列表,所以你想要“(funcall fn,@exprs)”,“(apply fn(list,@exprs)”或“(apply fn,@exprs nil)”之一,而不是“(应用 fn,@exprs)”。
(and ... 将评估表达式,直到一个评估为假,请注意,错误评估产生的副作用将会发生,只有后续的表达式将被跳过。
R
Rayne

我想我从来没有见过比这个人解释得更好的 Lisp 宏:http://www.defmacro.org/ramblings/lisp.html


特别是如果您有 Java/XML 背景。
周六下午躺在我的沙发上读到这篇文章真是太高兴了!写得非常清楚和有条理。
上帝保佑你和作者。
这是一篇很长的文章,但非常值得一读——其中很多是序言,可以归结为——1)Lisp S 表达式可以像 XML 一样表示代码或数据,2)宏不会急切地评估它们的输入像函数一样,因此可以将输入作为代码或数据的 s 表达式结构进行操作。令人震惊的时刻是,像“todo list”表示这样平凡的东西可以通过实现一个宏来武器化为代码,该宏可以将 todo 数据结构视为项目宏的代码输入。这不是您在大多数语言中会考虑的事情,而且很酷。
B
Brooks Moses

lisp 宏将程序片段作为输入。这个程序片段表示一个数据结构,可以按照您喜欢的任何方式进行操作和转换。最后宏输出另一个程序片段,这个片段是在运行时执行的。

C# 没有宏工具,但是如果编译器将代码解析为 CodeDOM 树,并将其传递给一个方法,该方法将其转换为另一个 CodeDOM,然后将其编译为 IL,则相当于 C# 工具。

这可用于实现“糖”语法,如 for each-statement using-clause、linq select-expressions 等,作为转换为底层代码的宏。

如果 Java 有宏,您可以在 Java 中实现 Linq 语法,而无需 Sun 更改基础语言。

以下是 C# 中用于实现 using 的 lisp 样式宏的伪代码:

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

现在实际上一个用于 C# 的 Lisp 风格的宏处理器,我要指出 using 的宏将是 look like this ;)
C
Community

由于现有的答案提供了很好的具体示例来解释宏实现什么以及如何实现,也许它有助于收集一些关于为什么宏工具相对于其他语言具有显着优势的一些想法;首先来自这些答案,然后是来自其他地方的一个很好的答案:

...在 C 中,您必须编写一个自定义预处理器 [这可能符合足够复杂的 C 程序] ...

Vatine

与任何精通 C++ 的人交谈,并询问他们花了多长时间学习所有模板元编程所需的模板捏造(这仍然没有那么强大)。

Matt Curtis

...在Java中,您必须使用字节码编织来破解自己的方式,尽管像AspectJ这样的一些框架允许您使用不同的方法来做到这一点,但它从根本上是一种破解。

Miguel Ping

DOLIST 类似于 Perl 的 foreach 或 Python 的 for。作为 JSR-201 的一部分,Java 在 Java 1.5 中使用“增强的”for 循环添加了一种类似的循环结构。注意宏有什么不同。 Lisp 程序员在他们的代码中注意到一个共同的模式,可以编写一个宏来为自己提供该模式的源代码级抽象。注意到相同模式的 Java 程序员必须让 Sun 相信这种特殊的抽象值得添加到语言中。然后 Sun 必须发布 JSR 并召集一个全行业的“专家组”来解决所有问题。根据 Sun 的说法,这个过程平均需要 18 个月。之后,编译器编写者都必须升级他们的编译器以支持新功能。甚至一旦 Java 程序员最喜欢的编译器支持新版本的 Java,他们可能“仍然”不能使用新功能,直到他们被允许破坏与旧版本 Java 的源代码兼容性。因此,Common Lisp 程序员可以在五分钟内自行解决的烦恼困扰了 Java 程序员多年。

Peter Seibel, in "Practical Common Lisp"


M
Matt Curtis

想想你可以在 C 或 C++ 中使用宏和模板做什么。它们是管理重复代码的非常有用的工具,但它们的局限性非常严重。

有限的宏/模板语法限制了它们的使用。例如,您不能编写扩展为类或函数以外的其他内容的模板。宏和模板无法轻松维护内部数据。

C 和 C++ 的复杂、非常不规则的语法使得编写非常通用的宏变得困难。

Lisp 和 Lisp 宏解决了这些问题。

Lisp 宏是用 Lisp 编写的。您拥有 Lisp 的全部功能来编写宏。

Lisp 有一个非常规则的语法。

与任何精通 C++ 的人交谈,并询问他们花了多长时间学习进行模板元编程所需的所有模板捏造。或者像 Modern C++ Design 这样的(优秀的)书籍中的所有疯狂技巧,即使该语言已经标准化了十年,它们仍然难以调试并且(在实践中)在现实世界的编译器之间不可移植。如果您用于元编程的语言与您用于编程的语言相同,那么所有这些都会消失!


好吧,说句公道话,C++ 模板元编程的问题不在于元编程语言不同,而在于它的可怕之处——它的设计并没有像发现的那样简单得多的模板功能。
@布鲁克斯当然。新兴特征并不总是坏事。不幸的是,在一种缓慢发展的委员会驱动语言中,很难在它们出现时修复它们。遗憾的是,C++ 的许多现代有用的新特性是用一种很少有人能读懂的语言编写的,普通程序员和“大祭司”之间存在巨大差距。
@downvoter:如果我的回答有问题,请发表评论,以便我们大家分享知识。
M
Miguel Ping

我不确定我是否可以为每个人的(优秀)帖子添加一些见解,但是......

由于 Lisp 语法性质,Lisp 宏工作得很好。

Lisp 是一种非常规则的语言(认为一切都是一个列表);宏使您能够将数据和代码视为相同(修改 lisp 表达式不需要字符串解析或其他技巧)。您将这两个功能结合起来,您就有了一种非常简洁的方式来修改代码。

编辑:我想说的是 Lisp 是 homoiconic,这意味着 lisp 程序的数据结构是用 lisp 本身编写的。

因此,您最终会得到一种在语言之上创建自己的代码生成器的方法,使用语言本身的所有功能(例如,在 Java 中,您必须通过字节码编织来破解自己的方式,尽管 AspectJ 等一些框架允许您使用不同的方法来做到这一点,这基本上是一种黑客攻击)。

在实践中,使用宏,您最终会在 lisp 之上构建自己的迷你语言,而无需学习其他语言或工具,并使用语言本身的全部功能。


这是有见地的评论,然而,“一切都是列表”的想法可能会吓到新手。要了解列表,您需要了解 cons、cars、cdrs、cells。更准确地说,Lisp 是由 S-expressions 组成的,而不是列表。
d
dnolen

Lisp 宏代表了一种几乎出现在任何大型编程项目中的模式。最终,在一个大型程序中,您有一段代码,您意识到编写一个将源代码输出为文本的程序会更简单,更不容易出错,然后您可以将其粘贴进去。

在 Python 中,对象有两个方法 __repr____str____str__ 只是人类可读的表示。 __repr__ 返回有效 Python 代码的表示形式,也就是说,可以作为有效 Python 输入解释器的内容。通过这种方式,您可以创建生成有效代码的 Python 小片段,这些代码可以粘贴到您的实际源代码中。

在 Lisp 中,这整个过程已经被宏系统形式化了。当然,它使您能够创建语法扩展并做各种花哨的事情,但上面总结了它的实际用处。当然,Lisp 宏系统允许您使用整个语言的全部功能来操作这些“片段”,这很有帮助。


您的第一段对于 Lisp 局外人来说非常清楚,这很重要。
你漏掉太多了。在第一段中粘贴代码的整个方面实际上是问题的一部分。每个人实际上都在分叉代码!现在,当您想要修复或增强您粘贴的代码时,现在您必须亲自维护无数副本,这些副本不断分叉,因为现在这是您的标准做法。 Lisp 宏保持干净,你可以一次性修复它们,一开始这更难,但变得容易得多。使用复制和粘贴,前期很容易,但会越来越难,直到代码在其自身的复杂性权重下崩溃。
另外,不要低估简单性带给您的好处。 Lisp 并没有太多的语法,当你使用宏来自动生成代码时,这有助于它不妨碍你——你不必跳过花括号(C/C++/C#/ Java)或完美缩进(Python)。当您进行多层生成时,很难确保每一行都以分号结尾,并且使用更轻的语法负载会变得更容易(它不是那么繁重)。
此外,同型性是一个巨大的胜利——一切都是相同形状的想法是一个好处。 C 和 C++ 的模板语言最终成为一种完全独立的语言,并且看起来非常不同且非常神秘。甚至不要以这种方式尝试多级代码生成。并不是说模板宏不强大——它们有自己的位置——但它们感觉就像是被固定住了,是事后的想法,没有很好的集成,笨重,现在是向前发展的必要邪恶。一旦你的 Lisp 宏就位,一切都会变得更容易,因为你可以使用一种新的原子语言。
d
dmitry_vk

简而言之,宏是代码的转换。它们允许引入许多新的语法结构。例如,考虑 C# 中的 LINQ。在 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 上循环、正确关闭它们。


J
Joerg Schmuecker

虽然以上都解释了宏是什么,甚至还有很酷的例子,但我认为宏和普通函数之间的主要区别在于 LISP 在调用函数之前首先评估所有参数。使用宏则相反,LISP 将未计算的参数传递给宏。例如,如果您将 (+ 1 2) 传递给函数,该函数将收到值 3。如果您将其传递给宏,它将收到 List(+ 1 2)。这可以用来做各种非常有用的事情。

添加新的控制结构,例如循环或列表的解构

测量执行传入函数所需的时间。对于函数,将在将控制权传递给函数之前评估参数。使用宏,您可以在秒表的开始和停止之间拼接代码。以下在宏和函数中具有完全相同的代码,并且输出非常不同。注意:这是一个人为的示例,选择实现是为了更好地突出差异。 (defmacro working-timer (b) (let ( (start (get-universal-time)) (result (eval b))) ;; 不在这里拼接以保持简单 ((- (get-universal-time) start) ))) (defun my-broken-timer (b) (let ( (start (get-universal-time)) (result (eval b))) ;; 甚至不需要 eval ((- (get-universal- time) start)))) (working-timer (sleep 10)) => 10 (broken-timer (sleep 10)) => 0


顺便说一句,Scala 在语言中添加了宏。虽然它们缺乏 Lisp 宏的美感,因为该语言不是同音的,但它们绝对值得研究,它们提供的抽象语法树最终可能更容易使用。现在说我更喜欢哪种宏系统还为时过早。
“LISP 将未计算的参数传递给宏”最终得到了一个明确的答案。但是您忘记了后半部分:宏的结果是转换后的代码,系统将代替原始代码对整个代码进行评估,就好像它最初就在那里一样(除非它本身再次调用一个宏,这次也将被该宏转换)。
S
Student

单线答案:

最小语法 => 宏优于表达式 => 简洁 => 抽象 => 强大

Lisp 宏只是以编程方式编写代码。也就是说,在扩展宏之后,你得到的只是没有宏的 Lisp 代码。因此,原则上,他们没有取得任何新成果。

但是,它们与其他编程语言中的宏的不同之处在于它们在表达式级别编写代码,而其他宏在字符串级别编写代码。由于括号,这是 lisp 独有的;或者更准确地说,他们的 minimal syntax 是可能的,这要归功于他们的括号。

正如该线程中的许多示例以及 Paul Graham 的 On Lisp 中所示,lisp 宏可以成为使您的代码更加简洁的工具。当简洁达到一定程度时,它提供了新的抽象级别,使代码更加简洁。再次回到第一点,原则上它们不提供任何新东西,但这就像说因为纸和铅笔(几乎)形成了图灵机,我们不需要实际的计算机。

如果有人知道一些数学,想想为什么函子和自然变换是有用的想法。原则上,他们不提供任何新东西。但是,通过将它们扩展到较低级别的数学,您会发现一些简单的想法(就范畴论而言)的组合可能需要 10 页才能写下来。你更倾向哪个?


n
nilsi

我从 common lisp 食谱中得到了这个,我认为它解释了为什么 lisp 宏很有用。

“宏是一段普通的 Lisp 代码,它对另一段假定的 Lisp 代码进行操作,将其转换为(更接近)可执行 Lisp 的版本。这听起来可能有点复杂,所以让我们举一个简单的例子。假设你想要一个setq 的版本,它将两个变量设置为相同的值。所以如果你写

(setq2 x y (+ z 3))

z=8 x 和 y 都设置为 11 时。(我想不出这有什么用,但这只是一个例子。)

很明显,我们不能将 setq2 定义为函数。如果是 x=50y=-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


如果 xy 通过引用传递,则可以将 setq2 定义为函数。但是,我不知道在 CL 中是否有可能。因此,对于特别不了解 Lisps 或 CL 的人来说,这不是 IMO 的说明性示例
@neoascetic CL 参数仅按值传递(这就是它首先需要宏的原因)。有些值是指针(如列表)。
s
stonemetal

在 python 中,你有装饰器,你基本上有一个函数,它接受另一个函数作为输入。你可以做任何你想做的事情:调用函数,做其他事情,将函数调用包装在资源获取释放中,等等,但你不能窥视该函数的内部。假设我们想让它更强大,假设你的装饰器将函数的代码作为列表接收,那么你不仅可以按原样执行函数,而且现在可以执行它的一部分,重新排序函数的行等。


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅