Loading... # 0x01 今天在 Go 语言学习群里有同学提出这样一个奇怪的问题,在 Go 里面使用匿名函数的方式这样定义一个计算 Fibonacci 数的函数编译时会直接报错。 ```go var fib = func (x int) int { if x < 2 { return x } return fib(x-1) + fib(x-2) } // ↓错误信息 # hello-go/test .\test.go:8:5: initialization loop: E:\code\go\src\hello-go\test\test.go:8:5: fib refers to E:\code\go\src\hello-go\test\test.go:8:5: fib ``` 而在 Python 中,使用类似的方法却能成功定义 ```python >>> fib = lambda x: x if x < 2 else fib(x-2) + fib(x-1) >>> fib(10) 55 ``` 虽然从感情上能很想锤写出这种代码的人,但是锤完我们还是得思考为啥会有这样的区别呢? # Python 部分分析 要了解为啥这条语句能正常执行,还是得先分析 Python 解释器到底是如何执行这条语句的。而我们都知道Python 执行 .py 代码文件,需要将代码文件解析成 AST 抽象语法树,然后编译成字节码,最后由 Python 虚拟机执行字节码。(如果你不清楚这个过程,可以看看我的另一篇文章~)<div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="http://gvoidy.cn/index.php/archives/260/" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://image.gvoidy.cn/assets/assets/img/sj/218.jpg);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">人肉编译器!Python 中 print("Hello World") 是怎么执行的</p> <div class="inster-summary text-muted"> 0x01print("Hello world!")几乎是所有人学 Python 时做的第一件事... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> ## 生成 ast ```python >>> import ast >>> text = open('1.py').read() >>> res = ast.parse(text, '1.py', 'exec') >>> ast.dump(res) "Module(body=[Assign(targets=[Name(id='fib', ctx=Store())], value=Lambda(args=arguments(posonlyargs=[], args=[arg(arg='x')], kwonlyargs=[], kw_defaults=[], defaults=[]), body=IfExp(test=Compare(left=Name(id='x', ctx=Load()), ops=[Lt()], comparators=[Constant(value=2)]), body=Name(id='x', ctx=Load()), orelse=BinOp(left=Call(func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='x', ctx=Load()), op=Sub(), right=Constant(value=2))], keywords=[]), op=Add(), right=Call(func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='x', ctx=Load()), op=Sub(), right=Constant(value=1))], keywords=[])))))], type_ignores=[])" ``` ```python Module( body=[Assign(targets=[Name(id='fib', ctx=Store())], value=Lambda( args=arguments( posonlyargs=[], args=[arg(arg='x')], kwonlyargs=[], kw_defaults=[], defaults=[] ), body=IfExp( test=Compare(left=Name(id='x', ctx=Load()), ops=[Lt()], comparators=[Constant(value=2)]), body=Name(id='x', ctx=Load()), orelse=BinOp(left=Call( func=Name(id='fib', ctx=Load()), args=[BinOp( left=Name(id='x', ctx=Load()), op=Sub(), right=Constant(value=2))], keywords=[] ), op=Add(), right=Call( func=Name(id='fib', ctx=Load()), args=[BinOp(left=Name(id='x', ctx=Load()), op=Sub(), right=Constant(value=1))], keywords=[] )))))], type_ignores=[] ) ``` 通过这棵 ast 可以初步得出一个猜测:首先定义了一个 fib 变量并将一个 lambda 对象赋值给了这个变量。然后在 lambda 对象使用(或者说调用)fib 方法时,会在各级名字空间内寻找这个方法,最终在全局名字空间中找到了这个名字对应的方法,于是开始执行。 > 什么是 Python 名字空间?Python 因为是动态语言,所有变量名与对象的绑定都是在程序运行时完成的,Python 会将作用域(即代码逻辑层面的变量生效作用域)中的名字绑定关系保存在一个 Dict 字典中。而名字空间也会根据作用域的不同划分为 **全局名字空间 ( Globals )**、**局部名字空间 ( Locals )**、**闭包名字空间 ( Enclosings )**、**内建名字空间 ( Builtin )**。 我们再反编译一下这段代码的字节码,证实一下我们的猜测:  可以得出我们的结论应该没有问题 # Go 部分分析 Go 是一门静态的编译型语言,所有变量名需要在使用前声明完毕。而 `var` 关键字正是声明变量名的方法,因为下面这段代码无法编译,我们通过经验人肉编译一下。 ```go var fib = func (x int) int { if x < 2 { return x } return fib(x-1) + fib(x-2) } ``` 编译的逻辑应该为,声明(或称初始化)一个 fib 变量名,并将后续函数对象赋值给 fib,**同时**该函数内部会使用 fib 函数,因为此时该变量仍未定义所以会出现问题。 我们稍微改一下这个程序,下面这种方式即可让他运行: ```go package main func main() { var fib func(x int) int fib = func (x int) int { if x < 2 { return x } return fib(x-1) + fib(x-2) } } ``` # 结论 所以这应该就是动态语言和静态语言的一个区别,动态语言(如 Python、JavaScript等)可以不需要先声明变量就直接使用,所以能使用这种类似 trick 的方法。而静态语言 (如 Go)就不行。 贴一个同学提供的 JavaScript 的例子: ```java > fib = function(x){if(x<2){return x}else{return fib(x-1)+fib(x-2)}} < ƒ (x){if(x<2){return x}else{return fib(x-1)+fib(x-2)}} > fib(10) < 55 ``` 最后修改:2021 年 07 月 09 日 05 : 12 PM © 允许规范转载 赞赏 如果觉得我的文章对你有用,请随意赞赏 赞赏作者 支付宝微信