6.3 我的地盘听我的
视频讲解
这里谈的其实是变量的作用域,作用域是程序运行时变量可被访问的范围,这个知识点在Python中是一个很容易“掉坑”的地方,大家一定要认真学习。
6.3.1 局部变量
定义在函数内部的变量是局部变量,局部变量的作用范围只能在函数的内部生效,它不能在函数外被引用。请分析下面代码,判断哪些变量是局部变量:
程序执行效果如下:
>>> 请输入原价:80 请输入折扣率:0.75 打折后价格是:60.00
分析:在函数discounts(price, rate)中,两个参数是price和rate,还有一个是final_price,它们都是discounts()函数中的局部变量。
为什么把它们称为局部变量呢?不妨修改一下代码:
程序运行,像刚才一样输入之后程序便报错了:
错误分析:Python提示没有找到'final_price'的定义,也就是说,Python找不到final_price这个变量。这是因为final_price只是一个局部变量,它的作用范围只在它的地盘上(discount()函数的定义范围内)有效,超出这个范围,就不再属于它的地盘了,它将什么都不是。
6.3.2 全局变量
与局部变量相对的是全局变量,上面代码中old_price、new_price、rate都是在函数外面定义的,它们都是全局变量,全局变量拥有更大的作用域,因此在函数中可以访问到它们:
程序执行效果如下:
>>> 请输入原价:80 请输入折扣率:0.75 试图在函数内部访问全局变量old_price的值:80.00 打折后价格是:60.00
在Python中,可以在函数中“肆无忌惮”地访问一个全局变量,但如果试图去修改它,就会有奇怪的事情会发生了。
请看下面例子:
程序执行效果如下:
>>> 请输入原价:80 请输入折扣率:0.75 在局部变量中修改后old_price的值是:50.00 全局变量old_price现在的值是:80.00 打折后价格是:60.00
分析:如果在函数内部试图修改全局变量的值,那么Python会创建一个新的局部变量替代(名字与全局变量相同),但真正的全局变量是“不为所动”的,所以才有了上面的实现结果。
6.3.3 global关键字
全局变量的作用域是整个模块,也就是代码段内所有的函数内部都可以访问到全局变量。但要注意的一点是,在函数内部仅仅去访问全局变量就好,不要试图去修改它。如果随意修改全局变量的值,很容易牵一发而动全身。
因此,Python使用屏蔽(shadowing)的手段对全局变量进行“保护”:一旦函数内部试图直接修改全局变量,Python就会在函数内部创建一个名字一模一样的局部变量代替,这样修改的结果只会影响到局部变量,而全局变量则丝毫不变。
请看下面例子:
代码是死的,但人是活的!假设你已经完全了解在函数中修改全局变量可能会导致程序可读性变差、出现莫名其妙的BUG、代码的维护成本成倍提高,但还是坚持“虚心接受,死不悔改”这八字原则,仍然觉得有必要在函数内部去修改这个全局变量,那么可以使用global关键字来达到目的。
代码修改如下:
6.3.4 内嵌函数
视频讲解
Python的函数定义是支持嵌套的,也就是允许在函数内部定义另一个函数,这种函数称为内嵌函数或者内部函数。
举个例子:
这是函数嵌套的经典例子,虽然看起来很简单,不过麻雀虽小,五脏俱全。
关于内部函数的使用,有一个比较值得注意的地方,就是内部函数整个作用域都在外部函数之内。就像上面例子中,fun2()整个函数的作用域都在fun1()里面,也就是只有在fun1()这个函数体里面,才可以随意地调用fun2()这个内部函数。如果在其他地方试图调用内部函数,就会出错:
在嵌套函数中,内部函数可以引用外部函数的局部变量:
6.3.5 LEGB原则
那现在有一个问题,如果有一个全局变量x=520,fun2()函数内部有一个局部变量x=11,那么程序还会打印88吗?
答案是否定的,程序打印的值是11。
另一个问题,上面三个x变量是同一个对象吗?不妨使用id()函数(获取)来测试一下:
可以看到上面程序返回了三个不同的id值,也就证明了三个x并不是同一个对象,它们只是变量的名字一样而已。像这种名字一样、作用域不同的变量引用,Python引入了LEGB原则进行规范。
LEGB含义解释:
• L-Local:函数内的名字空间。
• E-Enclosing function locals:嵌套函数中外部函数的名字空间。
• G-Global:函数定义所在模块的名字空间。
• B-Builtin:Python内置模块的名字空间。
那么变量的查找顺序依次就是L→E→G→B。
6.3.6 闭包
闭包是函数式编程的一个重要的语法结构,维基百科上对于闭包这个概念是这么解释的:“在计算机科学中,闭包(closure)是词法闭包(lexical closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。”
不同编程语言实现闭包的方式各不相同。Python中的闭包从表现形式上定义为:如果在一个内部函数里,对在外部作用域但不是在全局作用域的变量进行引用(简言之:就是在嵌套函数的环境下,内部函数引用了外部函数的局部变量),那么内部函数就被认为是闭包。
举个例子:
通过上面的例子理解闭包的概念:如果在一个内部函数里(funY()就是这个内部函数)对在外部作用域(但不是在全局作用域)的变量进行引用(x就是被引用的变量,x在外部作用域funX()函数里面,但不在全局作用域里),则这个内部函数就是一个闭包。
注意:
因为闭包的概念是由内部函数而来,所以不能在外部函数以外的地方对内部函数进行调用,下面的做法是错误的:
在闭包中,外部函数的局部变量对应内部函数的局部变量,事实上相当于之前讲的全局变量与局部变量的对应关系,在内部函数中,只能对外部函数的局部变量进行访问,但不能进行修改。
这个错误提示与之前讲解全局变量的时候基本一样,Python认为在内部函数的x是局部变量的时候,外部函数的x就被屏蔽了起来,所以执行x = x + 1的时候,在等号右边根本就找不到局部变量x的值,因此报错。
在Python 3以前并没有直接的解决方案,只能间接地通过容器类型来存放,因为容器类型不是放在栈里,所以不会被“屏蔽”掉。容器类型这个词大家是不是似曾相识?之前介绍的字符串、列表、元组,这些可以存放各种类型数据的“仓库”就是容器类型。于是乎可以把代码改造如下:
到了Python 3的世界里,有了不少的改进。如果希望在内部函数里可以修改外部函数里的局部变量的值,可以使用nonlocal关键字告诉Python这不是一个局部变量,使用方式与global一样:
好了,那么闭包“是什么、怎么用”总算是讲清楚了,那为什么要使用闭包呢?看起来闭包似乎是一种高级但是并没什么用的技巧。其实,闭包概念的引入是为了尽可能地避免使用全局变量,闭包允许将函数与其所操作的某些数据(环境)关联起来,这样外部函数就为内部函数构成了一个封闭的环境。这一点与面向对象编程的概念是非常类似的,在面向对象编程中,对象允许将某些数据(对象的属性)与一个或者多个方法相关联(详细内容请学习第11章:类和对象)。
【扩展阅读】游戏中的移动角色:闭包在实际开发中的应用,可访问http://bbs.fishc.com/thread-42656-1-1.html或扫描此处二维码获取。
扩展阅读
6.3.7 装饰器
这个名字听着可能比较新鲜,在Python中装饰器(decorator)的功能是将被装饰的函数当作参数传递给与装饰器对应的函数(名称相同的函数),并返回包装后的被装饰的函数。听上去有点绕,没关系,下面通过实例来讲述“装饰器是什么”以及“为什么会有装饰器”。
先随意定义一个函数:
现在,有一个新的需求,需要在执行该函数时加上日志:
>>> print("开始调用eat()函数…") 开始调用eat()函数… >>> eat() 开始吃了 >>> print("结束调用eat()函数…") 结束调用eat()函数…
这是一种方法,但代码显然变得臃肿起来,感觉就像大夏天时裹一件貂皮大衣在沙滩上漫步……
或者直接将代码封装到函数中:
这样功能也算是实现了,唯一的问题就是它需要侵入到了原来的代码里面,使得原有的业务逻辑变复杂,这样的代码也不符合“一个函数只做一件事情”的原则。
那么有没有可能在不修改函数代码的提前下,实现功能呢?
有的,刚学过的闭包就可以助你一臂之力:
log(eat)将eat函数作为参数传递给log(),由于wrapper()是log()的闭包,所以它可以访问log()的局部变量func,也就是刚刚传递进来的eat,因此,执行func()与执行eat()是一个效果。这样一来,问题就解决了!既没有修改eat()函数里面的逻辑结构,也不会给主程序带来太多的干扰项。不过这个eat = log(eat)看着总有些别扭,能不能改善一下呢?
可以,Python因此发明了“@语法糖”来解决这个问题。所谓语法糖(Syntactic sugar),就是在计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
有了“@语法糖”,上面的代码就可以这么写:
这样就省去了手动将eat()函数传递给log()再将返回值重新赋值的步骤。
有读者可能会问了:“如果eat()函数有参数怎么办?”
好办,可以将参数扔给内部的wrapper()函数:
但这样的话就必须要时刻关注eat()函数的参数数量,如果修改了eat(),就必须一并修改装饰器log(),不仅不方便也容易出错。防微杜渐,可以在设计的时候就不让这种情况发生:
在定义的时候使用收集参数,将多个参数打包到一个元组中,然后在调用的时候同样使用星号(*)进行解包,这样无论eat()有多少个参数,都不再是问题了。
最后,还有高阶玩法,如果装饰一层觉得不够,还可以一层套一层地加装饰器,像下面这样:
调用eat()的时候,相当于调用buffer(performance(log(eat)))。