第3章 Python过程组织与管理   程序过程技术是面向过程程序设计的关键技术,也是面向对象程序设计的支撑技术。程序过程由一组指令描述。随着计算机应用的深入,程序代码量急剧增大,复杂性随之膨胀。在这种情况下,如何组织与管理过程关系到程序的可靠性、可测试性、正确性和执行效率。   由于各种原因,程序代码可能会被正常执行,也可能会在执行中出现异常。作为一个健壮的程序,不仅应当正确地执行不出现异常的代码,还应当能执行可能会出现异常的代码,并在异常出现时可以处理异常,即使无法处理,也应该能显示出现了什么问题,不致让用户莫名其妙。   本章介绍Python在正常执行和有可能出现异常两种情况下的过程组织方式,它们分别称为函数和异常处理。在介绍了这两种过程组织结构后,本章还会介绍与它们有关的标识符访问规则——命名空间与作用域规则。 3.1 Python函数   在Python程序中,函数是组织与管理过程的最基本形式。本节介绍Python函数定义、返回、调用中要使用的一些基本技术。 3.1.1 函数及其关键环节   函数(function)技术是程序模块化的产物。程序模块化是一种控制问题复杂性的手段,其基本思想是将一个复杂的程序按照功能进行分解,使每一个模块成为功能单一的代码块封装体,不仅使结构更加清晰,而且大大降低了复杂性和设计难度,并在一定程度上实现了代码复用,有利于提高程序的可靠性、可测试性、可维护性和正确性。   作为承载模块职能的重要机制,函数具有三方面的意义:一是形成一段代码的封装体;二是实现一个功能;三是可以被重复使用。图3.1为函数被重复使用的示意图。 图3.1 函数被重复使用的示意图   作为程序模块化的元件和一段代码的封装体,不仅要求函数之间的关系要清晰可读,而且要求一个函数中,语句之间的关系也要清晰可读。使语句之间关系清晰的方法是采用结构化的语句结构。图3.2为目前广泛采用的3种结构化的基本语句结构。这3种基本语句结构的共同特点是“单入口、单出口”。 图3.2 3种基本程序结构:顺序、选择和循环   许多程序可以用函数作元件构建而成。在用Python进行开发时,可以采用内置的函数,也可以采用标准库中的或第三方社区开发的函数。但它们是有限的,还需要程序员自己设计一些函数。本节介绍有关函数设计的技术。   函数机制包含定义、调用和返回三大环节。图3.3形象地表示了三者之间的关系。 图3.3 函数的定义、调用和返回   1.函数调用   1)函数调用的形式   函数调用是一个表达式,格式为    函数名(实际参数列表)      函数pow(x,y)用来计算xy,定义时并不知道x是多少,y是多少,所以,x和y称为形式参数(formal parameters),简称形参(parameter)。调用这个函数式,必须说明需要计算的x的实际值和y的实际值。例如,计算28时,调用的表达式为pow(2,8),其中2和8称为实际参数(actual parameter),简称实参(argument)。   调用表达式可以单独构成一个语句,如print();也可用来组成别的表达式,如表达式a = pow(2,8)。   2)函数调用的作用   简单地说,函数就是用一个名字代表一段程序代码。所以,函数调用就是通过一个函数名使用一段代码,并且根据需要还要向函数传递一些数据。这些数据的传递通过参数的虚实结合进行。总之,函数调用是通过3个关键性操作完成的。   (1)参数传递。计算机执行程序的流程在当前程序中,当执行到调用表达式时,就会先把函数调用表达式中的实际参数传递给函数定义中的形式参数。例如,用pow(2,8)调用函数会把2传递给x,把8传递给y。   (2)保存现场。由于当前程序没有结束,所以会有一些中间执行结果和状态。为了能在函数返回时接着执行,就要将这些中间执行结果和状态保存起来。不过,这个操作是系统在后台进行的操作,在程序中并不表现出来。   (3)流程转移。将计算机执行程序的流程从当前调用语句转移到函数的第一个语句,开始执行函数中的语句。   需要注意的是,要调用一个模块中的函数,必须先用import将模块导入。   2.函数定义   Python的函数定义结构如下所示,由函数头(function header)和函数体(function body)两部分组成。    def 函数名 (参数列表): 函数体   1)函数头   函数头由关键字def引出,并由函数名及其后面括在圆括号中的零个或多个形式参数变量名称组成。   Python函数名是函数名变量的简称,必须是合法的Python标识符。def是一个执行语句关键字。当Python解释执行def语句后,就会创建一个函数对象,并将其绑定到函数名变量。函数可能需要0个或多个参数。有多个参数时,参数间要用西文逗号(,)分隔。   函数头后面是一个西文冒号(:),表示函数头的结束和函数体的开始。   2)函数体   函数体用需要的Python语句实现函数的功能。这些语句要按照Python的要求缩进。   3)函数嵌套   函数是用def语句定义的,凡是其他语句可以出现的地方,def语句同样可以出现一个函数的内部。这种在一个函数体内又包含另外一个函数的完整定义的情况称为函数嵌套。   代码3-1 函数嵌套示例。   运行结果如下。    a + b + g = 8, in B. a + g = 3, in A.      说明:   (1)程序的执行顺序在代码3-1中用带箭头的虚线标出。   (2)像函数B这样定义在其他函数(函数A)内的函数叫作内部函数,内部函数所在的函数叫作外部函数。当然,还可以多层嵌套。此时,除了最外层和最内层的函数外,其他函数既是外部函数,又是内部函数。   3.函数返回   函数体中非常重要的语句是return语句。   1)return语句的作用   (1)终止函数中的语句执行,将流程返回到调用处。   (2)返回函数的计算结果。   程序执行返回后,会恢复调用前的现场状态,从调用处的后面继续执行原来的程序。   2)return语句的用法   (1)只返回一个值的return语句。   代码3-2 利用海伦公式计算并返回三角形面积的函数。    import math def triArea(a,b,c): s = (a + b + c) / 2 area = math.sqrt((s – a) * (s – b) * (s – c) * s) return s #返回一个值      (2)不返回值的return语句。这时,函数只执行一些操作。   代码3-3 利用海伦公式计算并打印三角形面积的函数。    import math def triArea(a,b,c): s = (a + b + c) / 2 area = math.sqrt((s – a) * (s – b) * (s – c) * s) print ('三角形面积为:',s ) #打印一个值 return #空的return语句      这种情况下,return语句可以省略。如    import math def triArea(a,b,c): s = (a + b + c) / 2 area = math.sqrt((s – a) * (s – b) * (s – c) * s) print (‘三角形面积为:‘,s ) #打印一个值      (3)在一个函数中可使用多个return语句,但只能有一个return语句被执行。   代码3-4 判断一个数是否为素数的函数。    def isPrimer(number): if number < 2: return False for i in range(2,number): if number % i == 0: return False return True      这个函数中有3个return语句,但调用一次,只能由其中一个执行返回。   (4)返回多个值的return。   代码3-5 在边长为r的正方形中产生一个随机点的函数。    def getRandomPoint(r): x = random.uniform(0.0,r) y = random.uniform(0.0,r) #一个return返回两个值      对于这个函数,可以用下面的语句调用。    x,y = getRandomPoint(r)      实际上,这种返回值可以被看成一个元组对象。 3.1.2 Python函数参数技术   在函数调用时,参数传递是一个关键环节。为了支持灵活多样的应用,Python提供了多种函数参数技术。   1.不可变参数与可变参数   在Python函数中,每个参数都作为一个特殊的变量指向某一对象。因此,当一个程序要调用一个带参函数时,每个实参都按照值传递(pass-by-value)将其引用值传递给形参,即实参变量与形参变量都指向同一个对象。但是,按照实参引用值是可变性对象,还是不可变对象,在函数中的表现会有所不同。   1)实参引用不可变对象   当实参指向int、float、str、bool、元组等不可变对象时,在函数中,任何对于形式参数的修改(赋值)都会使形参变量指向另外的对象,因而不会对实参变量的引用值产生任何影响,即这时对于实参对象值只可引用,不可修改,函数无副作用。   代码3-6 不可变对象变量作参数。    def exchange(a,b): a, b = b, a #交换a,b print('\t Inside the function, a,b = ',a,b,sep = ',') def main(): x = 2; y = 3 print('Before the call, x,y =',x,y,sep = ',') exchange(x,y) #调用函数exchange print ('After the call, x,y = ',x,y,sep = ',') main() #调用函数main      执行结果如下。    Before the call, x,y = ,2,3 Inside the function, a,b = ,3,2 After the call, x,y = ,2,3      2)实参引用可变对象   当实参指向字典、列表等可变对象时,在函数中,任何对于形式参数的修改(赋值)都在实参变量引用的对象上进行,即这时对于实参对象值不仅可以引用,还可以修改,函数有副作用。   代码3-7 列表对象变量作参数。    def exchange(a,i,j): a[i],a[j] = a[j],a[i] print('\t Inside the function, a = ',a) def main(): x = [0,1,3,5,7] print('Before the call, x =',x) exchange(x,1,3) #调用函数exchange,交换列表元素x[1],x[3] print ('After the call, x = ',x) main() #调用函数main      执行结果如下。    Before the call, x = [0,1,3,5,7] Inside the function, a = [0,5,3,1,7] After the call, x = [0,5,3,1,7]      关于列表的更多知识,将在3.1.3节进一步介绍。   2.默认参数、必选参数、可选参数与可变参数   1)有默认值的参数   当函数带有默认参数时,允许在调用时缺省这个参数,即调用方默认这个默认值。   代码3-8 用户定义的幂计算函数。    def power(x, n = 2): p = x for i in range(1,n): p *= x return p      运行情况如下。    >>> power(3) #缺省有默认值的实际参数 9 >>> power(3,3) 27      注意:   (1)默认参数必须指向不可变对象,因为默认参数使用的值是在函数定义时就确定的。   (2)当函数具有多个参数时,有默认值的参数一定要放在最后。   2)可选参数与必选参数   由代码3-8的执行情况可以看出,带有默认值的参数是可选的,所以这类参数也可以称为可选参数。而不带默认值的参数就称为必选参数。可选参数与必选参数的使用要点 如下。   (1)要使某个参数是可选的,就给它一个默认值。   (2)必选参数和默认参数都有时,应当把必选参数放在前面,把默认参数放在后面。   (3)函数具有多个参数时,可以按照变化大小排队,把变化大的参数放在最前面,把变化最小的参数放在最后。程序员可以根据需要决定将哪些参数设计成默认参数。   3)可变数量参数   给一个形参名前加一个星号(*),表明这个参数将接收一个元素个数为任意的元组。   代码3-9 以元组作为可变参数。    def getSum(para1,para2,*para3): total = para1 + para2 for i in para3: total += i return total      运行情况如下。    >>> print(getSum(1,2)) 3 >>> print(getSum(1,2,3,4,5)) 15 >>> print(getSum(1,2,3,4,5,6,7,8)) 36   3.位置参数与命名参数   在函数定义有多个参数的情况下,当函数调用时,实参向形参传递,通常是按照定义的形参列表中的位置顺序依次进行的。这种传递方式称为按位置传递。按位置传递的参数称为位置参数(positional arguments)。   位置参数的排列顺序是程序员的一种偏好。这种位置偏好可能不符合用户的习惯。此外,要求用户必须知道每个参数的意义。这样,参数少了还好,在多个参数的情况下,这种“盲输”难免出错。为此,Python提供了命名参数,也称关键字参数(keyword arguments),使用户可以按名输入实际参数。   1)在实参中指定参数名   代码3-10 在实参中指定参数名示例。    >>> def getStudentInfo(name,gender,age,major,grade): print ('name:',name,',gender:',gender,',age:',age,',major:',major,',grade:',grade)      (1)按位置参数调用情况如下。    >>> getStudentInfo('zhang','M',20,'computer',3) name: zhang ,gender: M ,age: 20 ,major: computer ,grade: 3      (2)按位置并指定参数名调用情况如下。    >>> getStudentInfo(name ='zhang',gender = 'M',age = 20,major ='computer',grade = 3) name: zhang ,gender: M ,age: 20 ,major: computer ,grade: 3      (3)用指定参数名方式调用情况如下。    >> getStudentInfo(major ='computer',grade = 3,name ='zhang',gender = 'M',age = 20) name: zhang ,gender: M ,age: 20 ,major: computer ,grade: 3      (4)选择部分参数用指定参数名方式调用情况如下。    >>> getStudentInfo('zhang', 'M',major ='computer',grade = 3,age = 20) name: zhang ,gender: M ,age: 20 ,major: computer ,grade: 3      2)强制命名参数   在形参列表中加入一个星号(*),会形成强制命名参数(keyword-only),要求在调用时其后的形参必须显式地使用命名参数传递值。   代码3-11 强制命名参数示例。    def getStudentInfo(name,gender,age,*,major,grade): print ('name:',name,',gender:',gender,',age:',age,',major:',major,',grade:',grade)      (1)不按强制命名参数要求调用情况如下。    >>> getStudentInfo('zhang','M',20,'computer',3)   发出如下错误信息。            (2)按强制命名参数要求调用情况如下。    >>> getStudentInfo('zhang','M',20,major ='computer',grade = 3) name: zhang ,gender: M ,age: 20 ,major: computer ,grade: 3      3)使用字典的关键字参数   字典是元素为键-值对的列表(将在下一节进一步介绍)。给最后一个形参名前加一个双星号(**),表明这个参数将接收一个元素数量为0或多个的字典。   代码3-12 以字典作为可变参数。    def getStudentInfo(name,gender,age,**kw): print ('name:',name,',gender:',gender,',age:',age,',other:',kw)      运行情况如下。    >>> getStudentInfo(name ='zhang',gender = 'M',age = 20,major ='computer',grade = 3) name: zhang ,gender: M ,age: 20 ,other: {'major': 'computer', 'grade': 3}    3.1.3 Python函数的第一类对象特性   1.Python函数也是对象   Python一切皆对象。函数也是一类对象,并且与其他对象一样,具有身份、类型和值。因此,函数名就是指向函数对象的名字。   代码3-13 获取函数的类型和id对象特性示例。    >>> def func(): print ('I am a function') >>> print (type(func)) #输出函数的类型 >>> print (id(func)) #输出函数的身份 2182932023360 >>> print (func) #输出函数的值      2.Python函数是第一类对象   第一类对象(first-class object)是指可以赋值给一个变量、可以作为元素添加到集合对象中、可作为参数值传递给其他函数、还可以当作函数返回值的对象。Python函数持有这些特征,也是第一类对象。   代码3-14 函数赋值及作为返回值示例。    >>> def showName(name): def inner(age): print ('My name is:',name) print ('My age is:',age) return inner # 函数作为返回值 >>> F1 = showName #将函数赋值给变量F1 >>> F2 = F1('Zhang') #用F1代表showName,其返回(即inner)赋值给F2 >>> F2(18) #用F2代替inner My name is: Zhang My age is: 18      说明:这里定义了函数showName,其返回值是一个函数。也就是说,变量F1就是返回的inner函数,所以可以用F2('18')执行函数inner。   代码3-15 函数作为参数传递示例。    >>> def func(name): #定义函数func print ('My name is:',name) >>> def showName(arg,name): #arg为形式参数 print('I am a student') arg(name) #arg以函数形式调用 >>> showName(func,'Zhang') #func作为实际参数 I am a student My name is: Zhang      说明:函数名func作为实际参数传给形式参数arg。 3.1.4 函数标注   Python 3.0引入了函数标注,以增强函数的注释功能,让函数原型可以提供更多关于参数和返回的信息。   代码3-16 关于函数参数类型和返回类型的标注。    >>> def getStudentInfo(name:str,gender:str,age:int)->tuple: return ('name:',name,',gender:',gender,',age:',age) >>> print (getStudentInfo('Zhang','M',20)) ('name:', 'Zhang', ',gender:', 'M', ',age:', 20)      代码3-17 关于函数参数和返回的进一步标注。    >>> def getStudent\ Info(name:'一个字符串',gender:'性别',age:(1,50) )-> '返回一个关于学生信息的元组': return ('name:',name,',gender:',gender,',age:',age) >>> print (getStudentInfo('Zhang','M',20)) ('name:', 'Zhang', ',gender:', 'M', ',age:', 20)   说明:   (1)用冒号(:)对函数参数进行标注、使用->对返回值标注时,标注内容可以是任何形式,如参数的类型、作用、取值范围等,并且所有标注都会保存至函数的属性。   (2)查看这些注释可以通过自定义函数的特殊属性_ _annotations_ _获取,结果会以字典的形式返回。例如,对于代码3-17,可以写出    >>> getStudentInfo._ _annotations_ _ {'gender': '性别', 'age': (1, 50), 'return': '返回一个关于学生信息的字典', 'name': '一个字符串'}      (3)进行标注不影响参数默认值的使用。   代码3-18 函数参数标注与默认值一起使用。    >>> def getStudentInfo(name:'一个字符串'='Zhang',gender:'性别'='M',age:(1,50)=20 )-> tuple: return ('name:',name,',gender:',gender,',age:',age) >>> print (getStudentInfo()) ('name:', 'Zhang', ',gender:', 'M', ',age:', 20)    3.1.5 递归   1.递归概述   图3.4为猴子自己画自己的递归场面。这种一个结构自己或部分由自己直接或间接组成的情形称为递归(recursion)。 图3.4 猴子自己画自己的递归场面   在数学和计算机科学中,递归指由一种(或多种)简单的基本情况定义的一类对象或方法,并规定其他所有情况都能被还原为其基本情况。1967年,美籍法国数学家曼德布罗特(B.B.Mandelbort) 在《科学》杂志上发表了题为《英国的海岸线有多长》的著名论文。他认为,海岸线作为曲线,其特征是极不规则、极不光滑的,呈现极其蜿蜒复杂的变化。人们往往不能从形状和结构上区分这部分海岸与那部分海岸有什么本质的不同。然而,这种几乎同样程度的不规则性和复杂性正说明海岸线在形貌上是自相似的,即局部形态和整体形态相似。后来,人们在空中拍摄的100千米长的海岸线与放大了的10千米长海岸线两张照片,在没有建筑物或其他东西作为参照物时,看上去十分相似。   事实上,具有自相似性的形态广泛存在于自然界中,如连绵的山川、飘浮的云朵、岩石的断裂口、布朗粒子运动的轨迹、树冠、花菜、大脑皮层……曼德布罗特把这些部分与整体以某种方式相似的形体称为分形(fractal)。1975年,他创立了分形理论(fractal theory)。分形提供了描述自然形态的几何学方法,使得在计算机上可以从少量数据出发,对复杂的自然景物进行逼真的模拟,并启发人们利用分形技术对信息作大幅度的数据压缩以及进行艺术创作。图3.5为一组分形艺术创作图片。 图3.5 一组分形艺术创作图片   分形创作的基础是递归。图3.6说明了递归图形创作的基本过程。 图3.6 递归图形创作的基本过程   在程序设计领域,递归是指一种重要的算法,主要靠函数不断地直接或间接引用自身实现,直到引用的对象已知。   2.简单递归问题举例——阶乘的递归计算   1)算法分析   通常,求n!可以描述为   n!= 1 * 2 * 3 * … *(n - 1)* n   用递归算法实现,就是先从n考虑,记作fact(n)。但是,n!不是直接可知的,因此要在fact(n)中调用fact(n-1);而fact(n-1)也不是直接可知的,还要找下一个n-1……直到n-1为1时,得到1!=1为止。这时,递归调用结束,开始一级一级地返回,最后求得n!。   这个过程用演绎算式描述,可表示为    n! = n * (n-1)!   用函数形式描述,可以得到如下的递归模型。    非法     (n < 0)    fact ( n )= 1 (n = 0或 n = 1)    n * fact (n - 1) (n > 0)   图3.7为求fact(5)的递归计算过程。 图3.7 求fact(5)的递归计算过程   2)递归算法要素   递归过程的关键是构造递归算法,或递归表达式,如fact(n) = n * fact(n-1)。但是,光有递归表达式还不够。因为递归调用不应无限制地进行下去,当调用有限次以后,就应当到达递归调用的终点得到一个确定值(如图3.7中的fact(1)=1),就应当开始返回。所以,递归有如下二要素。   (1)递归表达式。   (2)递归终止条件,或称递归出口。   3)递归函数参考代码   代码3-19 计算阶乘的递归函数代码。    def?fact(n): if?n == 1 or n == 0: return?1 return?n?*?fact(n?-?1)      函数测试结果如下。    >>> fact(1) 1 >>> fact(5) 120      讨论:递归实际上是把问题的求解变为较小规模的同类型求解的过程,并且通过一系列的调用和返回实现。 3.1.6 lambda表达式   lambda表达式是用关键字lambda定义的函数,也称lambda函数,其基本格式如下。    lambda 参数列表: 表达式      代码3-20 一个计算3个数之和的lambda表达式。    >>> f = lambda a, b = 2, c = 3: a + b + c >>> f(3) 8 >>> f(3,5,1) 9      说明:   (1)lambda表达式具有函数的主要特征:有参数,可以调用并传递参数,还可以让参数具有默认值。   (2)lambda表达式虽然具有函数机能,但没有名字,所以也称为匿名函数。   (3)lambda表达式不像函数那样由语句块组成函数体,它们仅是一种表达式,可以用在任何可以使用表达式的地方,如用lambda表达式作为实际参数。   代码3-21 lambda表达式作为参数。    >>> def apply(f,n): print(f(n)) >>> >>> square = lambda x:x**2 >>> cube = lambda x:x**3 >>> apply(square,4) 16 >>> apply(cube,3) 27      (4)lambda表达式可以嵌套。   代码3-22 嵌套的lambda表达式:计算x * 2 + 2。    >>> incre_two = lambda x:x + 2 >>> multiply_incre_two = lambda x:incre_two(x * 2) >>> print(multiply_incre_two(2)) 6    练习3.1 1.判断题 (1)函数定义可以嵌套。 ( ) (2)函数调用可以嵌套。 ( ) (3)函数参数可以嵌套。 ( ) (4)Python函数调用时的参数传递,只有传值一种方式,所以形参值的变化不会影响实参。 ( ) (5)一个函数中可以定义多个return语句。 ( ) (6)定义Python函数时,无须指定其返回对象的类型。 ( ) (7)可以使用一个可变对象作为函数可选参数的默认值。 ( ) (8)函数有可能改变一个形式参数变量所绑定对象的值。 ( ) (9)函数的形式参数是可选的,可以有,也可以无。 ( ) (10)传给函数的实参必须与函数签名中定义的形参在数目、类型和顺序上一致。 ( ) (11)函数参数可以作为位置参数或命名参数传递。 ( ) (12)Python函数的return语句只能返回一个值。 ( ) (13)函数调用时,如果没有实参调用默认参数,则默认值被当作0。 ( ) (14)无返回值的函数称为None函数。 ( ) (15)递归函数的名称在自己的函数体中至少要出现一次。 ( ) (16)在递归函数中必须有一个控制环节用来防止程序无限期地运行。 ( ) (17)递归函数必须返回一个值给其调用者,否则无法继续递归过程。 ( ) (18)不可能存在无返回值的递归函数。 ( ) 2.选择题 (1)代码    >>> def func(a, b=4,c=5): print (a,b,c) >>> func(1,2)    执行后输出的结果是 。 A.1 2 5 B.1 4 5 C.2 4 5 D.1 2 0 (2)函数    def func(x,y,z = 1. *par, **parameter): print(x,y,z) print (par) print (parameter)    用 func(1,2,3,4,5,m = 6) 调用,输出结果是 。 A. B. C. D. 1 2 1 1 2 3 1 2 3 1 2 1 (3,4,5) (4,5) (4,5) (4,5) ('m': 6) {'m': 6} (6) (m = 6) (3)代码    >>> x,y = 6,9 >>> def foo(): global y x,y = 0,0 >>> x,y    执行后的显示结果是 。 A.0 0 B.6 0 C.0 9 D.6 9 (4)下列关于匿名函数的说法中,正确的是 。 A.lambda是一个表达式,不是语句 B.在lambda的格式中,lambda 参数1,参数2,…:是由参数构成的表达式 C.Lambda可以用 def定义一个命名函数替换 D.对于mn = (lambda x,y:x if x < y else y),mn(3,5) 可以返回两个数字中的大者 3.代码分析题 (1)阅读下面的代码,指出函数的功能。    def f(m,n): if m < n: m,n = n,m while m % n != 0: r = m % n m = n n = r return n    (2)阅读下面的代码,指出程序运行结果。    d = lambda p: p; t = lambda p: p * 3 x = 2; x = d(x); x = t(x); x = d(x); print(x)    (3)阅读下面的代码,指出其中while循环的次数。    def cube(i): i = i * i * i i = 0 while i < 1000: cube(i) i += 1    (4)指出下面的代码输出几个数据,并说明它们之间的关系。    a = 1 id(a) def fun(a): print (id(a)) a = 2 print (id(a)) fun(a) id(a)    (5)指出下面的代码输出几个数据,说明它们之间的关系,并说明此题与(4)题不同的原因。 a = [] id(a) def fun(a): print (id(a)) a.append(1) print (id(a)) fun(a) id(a)    (6)下面这段代码的输出结果是什么?请解释。    def extendList (val,list = []): list.append(val) return list list1 = extendList(10) list2 = extendList(123,[]) list3 = extendList('a') print('list = %s'%list1) print('list = %s'%list2) print('list = %s'%list3)    (7)下面这段代码的输出结果是什么?请解释。    def multipliers(): return ([lambda x:i * x for i in range (4)]) print ([m(2) for m in multipliers()])    4.程序设计题 (1)编写一个函数,求一元二次多项式的值。 (2)编写一个计算f(x)=xn的递归程序。 (3)假设银行一年整存整取的月息为0.32%,某人存入了一笔钱。然后,每年年底取出200元。这样到第5年年底刚好取完。请设计一个递归函数,计算他当初共存了多少钱。 (4)设有n个已经按照从大到小顺序排列的数,现在从键盘上输入一个数x,判断它是否在已知数列中。 (5)用递归函数计算两个非负整数的最大公约数。 (6)约瑟夫问题:M个人围成一圈,从第1个人开始依次从1到N循环报数,并且让每个报数为N的人出圈,直到圈中只剩下一个人为止。请用C语言程序输出所有出圈者的顺序(分别用循环和递归方法)。 (7)分割椭圆。在一个椭圆的边上任选n个点,然后用直线段将它们连接,会把椭圆分成若干块。 (8)台阶问题。一只青蛙一次可以跳1级台阶,也可以跳2级台阶。求该青蛙跳一个n级的台阶总共有多少种跳法。请用函数和lambda表达式分别求解。 (9)变态台阶问题。一只青蛙一次可以跳1级台阶,也可以跳2级台阶……它也可以跳n级台阶。求该青蛙跳一个n级的台阶总共有多少种跳法。请用函数和lambda表达式分别求解。 (10)矩形覆盖。 可以用2×1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2×1的小矩形无重叠地覆盖一个2×n的大矩形,总共有多少种方法?请用函数和lambda表达式分别求解。 3.2 Python异常处理   程序设计是人的智力与问题的复杂性之间在博弈,尽管程序员在设计程序时已经绞 尽了脑汁,但“智者千虑难免一失”,仍可能产生错误(error)。通常,错误可以分为如下3类。   (1)语法错误(syntax error)。语法错误是违背语法规则,导致编译器或解释器无法解析的错误,通常系统会指出错误的位置及其类型。   (2)逻辑错误(logical error)。一个程序通过解释,可以运行,但无法获得预想的结果。这种错误就是逻辑错误,即因程序设计者的逻辑思维不慎密而造成。逻辑错误要通过测试发现。   (3)运行时错误(runtime error),简称异常(exceptions)。一个程序通过解释,可以运行,也可以获得预想的结果,但是有时却无法正常运行。这就是程序出现异常。   避免语法错误的方法是要熟悉语法格式。例如,函数头后、循环头后、if头后以及else后不可缺少冒号(:),该缩进的语句要缩进,语句后面不能加圆点(.),不要使用中文标点符号等。避免逻辑错误的方法是训练科学的思维方法,培养良好的程序设计风格,设计科学的测试用例等。   异常的发生往往是难以预估的,并且相当多的是外界因素,如需要打印时,打印机发生故障;需要访问文件时,磁盘发生故障以及用户给定的除数为零等。在这种情况下,程序员能够做的事情就是检测异常的发生,按照异常的类型进行相应的补救,并可以发出必要的异常信息。必要时也可以在给出信息后终止程序的执行。 3.2.1 异常处理的基本思路与异常类型   Python异常处理是一项将正常执行过程与异常处理过程相分离的技术。其基本思路大致分为两步:首先监视可能会出现异常的代码段,发现有异常,就将其捕获,抛出(引发)给处理部分;处理部分将按照异常的类型进行处理。因此,异常处理的关键是异常类型。   但是,异常的发生是难以预料的。尽管如此,人们也根据经验对常发异常的原因有了基本的了解。附录B列出了Python 3.0标准异常类结构:总的异常类称为Exception,下面又分为几层。严格地说,每一层的类型都应当称为“类”(class),但类的有关概念到第4章才介绍。这里暂且称其为类型。这些类型是内置的,无须导入就可直接使用。应当说,这些类型已经囊括了几乎所有的异常类型。不过,Python也不保证已经包括了全部异常类型。所以,也允许程序员根据自己的需要定义适合自己的异常类。关于这一点,也要在第4章介绍。下面重点介绍几个异常类的用法。   代码3-23 观察ZeroDivisionError (被0除) 引发的异常。    >>> 2 / 0 Traceback (most recent call last): File "", line 1, in 2/0 ZeroDivisionError: division by zero      代码3-24 观察ImportError(导入失败) 引发的异常。    >>> import xyz Traceback (most recent call last): File "", line 1, in import xyz ImportError: No module named 'xyz'      代码3-25 观察NameError(访问未定义名字) 引发的异常。    >>> aName Traceback (most recent call last): File "", line 1, in aName NameError: name 'aName' is not defined      代码3-26 观察SyntaxError(语法错误)现象。    >>> import = 5 #关键字作变量 SyntaxError: invalid syntax >>> for i in range(3) #循环头后无冒号(:) SyntaxError: invalid syntax >>> if a == 5: print (a) #if子句没缩进 SyntaxError: expected an indented block >>> if a = 5: #用==的地方写了= SyntaxError: invalid syntax >>> for i in range(5): #使用了汉语圆括号 SyntaxError: invalid character in identifier >>> x = 'A" #扫描字符串末尾时出错(定界符不匹配) SyntaxError: EOL while scanning string literal      代码3-27 观察TypeError(类型错误)引发的异常。    >>> a = '123' >>> b = 321 >>> a + b Traceback (most recent call last): File "", line 1, in a + b TypeError: Can't convert 'int' object to str implicitly      说明:   (1)上述几个关于异常的代码都是在交互环境中执行的。可以看出,除SyntaxError外,面对其他错误的出现,交互环境都首先给出了“Traceback (most recent call last):”——“跟踪返回(最近一次调用)问题如下:”的提示,然后给出出错位置、谁引发的错误、错误类型及发生原因。这里,提示“Traceback (most recent call last):”隐含了一个意思:这个异常没有被程序捕获并处理。   (2)SyntaxError没有这些提示,这表明这些SyntaxError并没有引发程序异常,因为含有这样的错误是无法编译或解释的。 3.2.2 try-except语句   一般来说,异常处理需要两个基本环节:捕获异常和处理异常。为此,基本的Python异常处理语句由try子句和except子句组成,形成try-except语句。其语法如下。    try: 被监视的语句块 except 异常类1: 异常类1处理代码块 as 异常信息变量 except 异常类2: 异常类2处理代码块 as 异常信息变量 …      说明:   (1)在这个语句中,try子句的作用是监视其冒号(:)后面语句块的执行过程,一有操作错误,便会由 Python解析器引发一个异常,使被监视的语句块停止执行,把发现的异常抛向后面的except子句。   (2)except子句的作用是捕获并处理异常。一个try-except 语句中可以有多个 except 子句。Python对except子句的数量没有限制。try抛出异常后,这个异常就按照except子句的顺序,一一与它们列出的异常类进行匹配,最先匹配的 except就会捕获这个异常,并交后面的代码块处理。   (3)每个except子句不限于只列出一个异常类型,相同的异常类型都可以列在一个except子句中处理。如果except子句中没有异常类,这种子句将会捕获前面没有捕获的其他异常,并屏蔽其后所有except子句。   (4)一条except子句执行后,就不会再由其他except子句处理了。   (5)异常信息变量就是异常发生后,系统给出的异常发生原因的说明,如division by zero、No module named 'xyz'、name 'aName' is not defined、EOL while scanning string literal以及Can't convert 'int' object to str implicitly等。这些信息——字符串对象,将被as后面的变量引用。   代码3-28 try-except语句应用举例。    try: x = eval(input('input x:')) y = eval(input('input y:')) a z = x / y print('计算结果为:',z) except NameError as e: print('NameError:',e) except ZeroDivisionError as e: print('ZeroDivisionError:',e) print('请重新输入除数:') y = eval(input('input y:')) z = x / y print('计算结果为:',z)      测试情况如下。    input x:6 input y:0 NameError: name 'a' is not defined      代码3-29 将代码3-28中变量a注释后的代码。    try: x = eval(input('input x:')) y = eval(input('input y:')) #a z = x / y print('计算结果为:',z) except NameError as e: print('NameError:',e) except ZeroDivisionError as e: print('ZeroDivisionError:',e) print('请重新输入除数:') y = eval(input('input y:')) z = x / y print('计算结果为:',z)      测试情况如下。    input x:6 input y:0 ZeroDivisionError: division by zero 请重新输入除数: input y:2 计算结果为: 3.0      (6)在函数内部,如果一个异常发生,却没有被捕获到,这个异常将会向上层(如向调用这个函数的函数或模块)传递,由上层处理;若一直向上到了顶层都没有被处理,则会由 Python默认的异常处理器处理,甚至由操作系统的默认异常处理器处理。3.2.1节中的几个代码就是由 Python默认异常处理器处理的几个实例。在那里才会给出“Traceback (most recent call last)”的提示。 3.2.3 异常类型的层次结构   观察附录B可以看出,Python 3.0标准异常类型是分层次的,共分为6个层次:最高层是 BaseException;然后是3个二级类 SystemExit、KeyboardInterrupt和 Exception;三级以下都是类 Exception的子类和子子类。越下层的异常类定义的异常越精细,越上层的类定义的异常范围越大。   在try-except 语句中,try具有强大的异常抛出能力。应该说,凡是异常都可以捕获,但except的异常捕获能力由其后列出的异常类决定:列有什么样的异常类,就捕获什么样的异常;列出的异常类级别高,所捕获的异常就是其所有子类。例如,列出的异常为BaseException,则可以捕获所有标准异常。   但是,列出的异常类型级别高了之后,如何知道这个异常是什么原因引起的呢?这就是异常信息变量的作用,由它补充具体异常的原因。虽然如此,但是要捕获的异常范围大了,就不能有针对性地进行具体的异常处理了,除非这些异常都采用同样的手段进行处理,如显示异常信息后一律停止程序运行。 3.2.4 else子句与finally子句   在try-except语句后面可以添加else子句、finally子句,二者选一或二者都添加。   else子句在try没有抛出异常,即没有一个except子句运行的情况下才执行。而finally子句是不管任何情况下都要执行,主要用于善后操作,如对在这段代码执行过程中打开的文件进行关闭操作等。   代码3-30 在try-except语句后添加else子句和finally子句。    try: x = eval(input('input x:')) y = eval(input('input y:')) #a z = x / y print('计算结果为:',z) except NameError as e: print('NameError:',e) except ZeroDivisionError as e: print('ZeroDivisionError:',e) print('请重新输入除数:') y = eval(input('input y:')) z = x / y print('计算结果为:',z) else: print('程序未出现异常。') finally: print('测试结束。')      一次执行情况:    input x:6 input y:0 ZeroDivisionError: division by zero 请重新输入除数: input y:2 计算结果为: 3.0 测试结束。      另一次执行情况:    input x:6 input y:2 计算结果为:3.0 程序未出现异常。 测试结束。    3.2.5 异常的人工触发:raise与assert   前面介绍的异常都是在程序执行期间由解析器自动地、隐式触发的,并且它们只针对内置异常类。但是,这种触发方式不适合程序员自己定义的异常类,并且在设计并调试 except 子句时可能不太方便。为此,Python提供了两种人工显式触发异常的方法:使用 raise与 assert 语句。   1.raise语句   raise语句用于强制性(无理由)地触发已定义异常。   代码3-31 用raise进行人工触发异常示例。    >>> raise KeyError('abcdefg','xyz') Traceback (most recent call last): File "", line 1, in raise KeyError,('abcdefg','xyz') KeyError: ('abcdefg', 'xyz')      2.assert语句   assert语句可以在一定条件下触发一个未定义异常。因此,它有一个条件表达式,还可以选择性地带有一个参数作为提示信息。其语法为    assert 表达式 [,参数]      代码3-32 用assert进行人工有条件触发异常示例。    >>> def div(x,y): assert y != 0, '参数y不可为0' return x / y >>> div(7,3) 2.3333333333333335 >>> div(7,0) Traceback (most recent call last): File "", line 1, in div(7,0) File "", line 2, in div assert y != 0, '参数y不可为0' AssertionError: 参数y不可为0      注意:表达式是正常运行的条件,而不是异常出现的条件。 练习3.2 1.选择题 (1)在 try-except语句中, 。 A.try子句用于捕获异常,except子句用于处理异常 B.try子句用于发现异常,except子句用于抛出并捕获处理异常 C.try子句用于发现并抛出异常,except子句用于捕获并处理异常 D.try子句用于抛出异常,except子句用于捕获并处理异常,触发异常则是由Python解析器自动引发的 (2)在try-except语句中, 。 A.只可以有一个except子句 B.可以有无限多个except子句 C.每个 except 子句只能捕获一个异常 D.可以没有 except子句 (3)else子句和finally子句, 。 A.都是不管什么情况必须执行的 B.else子句在没有捕获到任何异常时执行,finally子句则不管什么情况都要执行 C.else子句在捕获到任何异常时执行,finally子句则不管什么情况都要执行 D.else子句在没有捕获到任何异常时执行,finally子句在捕获到异常后执行 (4)如果Python程序中使用了没有导入模块中的函数或变量,则运行时会抛出 错误。 A.语法 B.运行时 C.逻辑 D.不报错 (5)在Python程序中,执行到表达式123 + ‘abc’时,会抛出 信息。 A.NameError B.IndexError C.SyntaxError D.TypeError (6)试图打开一个不存在的文件时所触发的异常是 。 A.KeyError B.NameError C.SyntaxError D.IOError 2.代码分析 指出下列代码的执行结果,并上机验证。    def testException(): try: aInt = 123 print (aint) print (aInt) except NameError as e: print('There is a NameError',e) except KeyError as e: print('There is a KeyError',e) except ArithmeticError as e: print('There is a ArithmeticError',e) testException()    若 print(aInt)与print(aint)交换,又会出现什么情况? 3.3 Python命名空间与作用域   在Python程序中,需要用到许多名字。本节讨论名字的两个基本属性:命名空间(namespace)和作用域 (scope)。 3.3.1 Python命名空间   1.命名空间的概念   一个程序需要由多个模块组成;每个模块又往往由多个函数组成;每个函数中要使用多个变量。这样,就要使用大量的名字。为了让不同的模块和函数可以由不同的人开发,就要解决各模块和函数之间名字的冲突问题。因此,命名空间(或称名字空间)是从名字到对象的映射区间,或者说名字绑定到对象的区间。引入命名空间后,每一个命名空间就是一个名字集合,不可有重名;而各命名空间独立存在,在不同的命名空间中允许使用相同的名字,它们分别绑定在不同的对象上,因而不会造成名字之间的碰撞(name collision)。   在Python中,大部分的命名空间都是由字典实现的:键为名字,值是其对应的对象。所以,一个命名空间就是一个字典对象。因此,也可以把命名空间理解为保存名字及其引用关系的地方。   代码3-33 处在两个不同命名空间的变量i。    def fun1(): ??i = 100 ? def fun2(): ??i = 200      说明:这是一个在同一模块中有两个函数的例子。这两个函数中的 i,是用了相同名字的变量,但这是两个独立的名字,分别属于不同的命名空间,就像两个不同家庭中的孩子用了相同的名字一样,并非同一人,或者像存在不同文件夹中的同名文件,但内容不一定相同。   2.Python命名空间的基本级别及其生命周期   Python在开发和应用过程中形成了如表3.1所示的几种常见的命名空间。Python程序在运行期间会有多个名字空间并存。不同命名空间在不同的时刻创建,并且有不同的生存周期。也就是说,每当一个Python程序开始运行(即 Python解释器启动),就会创建一个built-in namespace,引入关键字、内置函数名、内置变量和内置异常名字等;若文件以顶层程序文件(主模块,即_ _name_ _为'_ _main_ _')执行,则会为之创建一个全局命名空间,保存主模块中定义的名字。此后,每当加载一个其他模块,就会为之创建一个全局命名空间,引入该模块中定义的变量名、函数名、类名、异常名字等;每当开始执行def或lambda、class,就会为之创建一个局部命名空间,存储该关键字引出的一段代码中定义的变量等名字。这样,就在一个Python程序运行时建立起了不同级别的命名空间。显然,内置命名空间最大,全局命名空间次之,局部命名空间最小。 表3.1 Python基本命名空间及其生命周期 命名空间名称 说 明 创 建 时 刻 撤 销 时 刻 局部命名空间 (local namespace) 函数局部命名空间:绑定在函数中的名字 def/lambda定义的语句块执行时 函数返回或有未捕获异常时 类局部命名空间:类定义中定义的名字 解释器读到类定义时 类定义结束后 全局命名空间 (global namespace) 由直接定义在某个模块中的变量名、类名、函数名、异常名字等标识符组成 Python解释器启动以及模块被加载时 程序执行结束时 内置命名空间 (built-in namespace) 包括关键字、内置函数、内置变量和内置异常名字等 Python解释器启动时 程序执行结束时      注意:内置变量实际上同样是以模块的形式存在,模块名为 builtins。   需要强调,在Python程序中,只有module(模块)、class(类)、def(函数)、lambda才会创建新的命名空间。而在if-elif-else、for/while、try-except\try-finally等关键字引出的语句块中,并不会创建局部命名空间。   代码3-34 语句块不涉及命名空间示例。    if True: variable = 100 print (variable) print ("******") print (variable)      代码的输出为    100 ****** 100      说明:在这段代码中,if语句中定义的variable变量在if语句外部仍然能够使用。这说明if引出地方语句块中不会产生本地(局部)作用域,所以变量variable仍然处在全局作用域中。   3.标识符的创建及其与命名空间的绑定   在Python中,名字空间的形成不是简单地将名字放进名字空间,而是由某些语句操作进行的。或者说,通过一些操作将标识符引入到(或绑定到)对应的名字空间中。这类操作仅限于下列几种。   (1)赋值操作:在Python中,赋值语句起绑定或重绑定(bind or rebind)的作用。对一个变量进行初次赋值会在当前命名空间中引入新的变量,即把名字和对象以及命名空间做一个绑定,后续赋值操作则只将变量绑定到另外的对象。赋值操作不会复制。函数调用的参数传递是赋值,不是复制。   (2)参数声明:参数声明会将形式参数变量引入到函数的局部命名空间中。   (3)函数和类的定义中,用于引入新的函数名和类名。   (4)import语句:import语句在当前的全局命名空间中引入模块中定义的标识符。   (5)在if-elif-else、for-else、while、try-except\try-finally等关键字的语句块中,只会在当前作用域中引入新的变量名,并不创建新的命名空间。例如,在代码3-34中,if语句块只能在当前的全局命名空间中引入变量。下面是一个for的实例。   代码3-35 for语句在当前命名空间中引入新的变量示例。    >>> if _ _name_ _ == '_ _main_ _': for a in range(5,10): print ('a = ',a) def fun(): for b in range(3,5): print ('b = ',b) print ('b = ',b) print ('a = ',a) fun() a = 5 a = 6 a = 7 a = 8 a = 9 a = 9 b = 3 b = 4 b = 4      由a和b的输出情况可以看出,for语句可以把一个变量绑定到当前命名空间中,即它不会创建一个新的名字空间,仅把新的变量引入到当前命名空间。   4.dir()函数   dir()函数用于返回一个列表对象,在该列表中保存有指定命名空间中的排好序的标识符字符串。命名空间用参数指定;若参数缺省,则表示当前名字空间。   代码3-36 dir()函数应用示例。    >>> dir() # 当前名字空间 ['_ _annotations_ _', '_ _builtins_ _', '_ _doc_ _', '_ _loader_ _', '_ _name_ _', '_ _package_ _', '_ _spec_ _'] >>> import math >>> dir(math) # 对math模块中命名的标识符列表 ['_ _doc_ _', '_ _loader_ _', '_ _name_ _', '_ _package_ _', '_ _spec_ _', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc'] >>> dir(print) # 对print命名空间中的标识符列表 ['_ _call_ _', '_ _class_ _', '_ _delattr_ _', '_ _dir_ _', '_ _doc_ _', '_ _eq_ _', '_ _ format _ _', '_ _ge_ _', '_ _getattribute_ _', '_ _gt_ _', '_ _hash_ _', '_ _init_ _', '_ _init_ subclass_ _', '_ _le_ _', '_ _lt_ _', '_ _module_ _', '_ _name_ _', '_ _ne_ _', '_ _new_ _', '_ _qualname_ _', '_ _reduce_ _', '_ _reduce_ex_ _', '_ _repr_ _', '_ _self_ _', '_ _setattr_ _', '_ _sizeof_ _', '_ _str_ _', '_ _subclasshook_ _', '_ _text_signature_ _']    3.3.2 Python作用域   命名空间是一套程序中使用的名字及其引用关系的存储体系。作用域关注的是在程序的某一个代码区间中,哪些名字空间中的名字是可见的(可访问的)以及有无读或写的限制。所以,作用域是与命名空间相关但又不同的概念。   1.名字的直接访问和属性访问   作用域是与名字的可访问性相关的概念,并且是从直接访问的角度进行考虑的。直接访问是相对于属性访问的概念。   为了说明属性访问和直接访问,先举一个生活中的例子。假设一个村子里有多个张三:A家的张三、B家的张三、C家的张三……当人们不在A家说A家的张三时,一定是说“A家张三”。这就是属性访问。若在A家时,说A家的张三,就只说“张三”即可。这就是直接访问。   由于在某个命名空间中定义的名字实际上就是这个命名空间的属性,因此,不在某命名空间处访问其名字时,就不能进行直接访问,而应采用属性访问方式,如math.pi等。在Python程序中,如果一个名字前面没有(.),就是直接访问。显然,从作用域的角度看,import math与from math import pi的区别就在于前者是将标识符math引入到当前命名空间,而后者是将名字math.pi引入到当前命名空间。   代码3-37 两种import对作用域的影响示例。    >>> import math >>> dir() # 当前名字空间中添加了math ['_ _annotations_ _','_ _builtins_ _','_ _doc_ _','_ _loader_ _','_ _name_ _','_ _package_ _', '_ _spec_ _', 'math'] >>> from math import pi >>> dir() ['_ _annotations_ _', '_ _builtins_ _', '_ _doc_ _', '_ _loader_ _', '_ _name_ _', '_ _package_ _', '_ _spec_ _', 'math', 'pi']      有了直接访问的概念,就可以进一步理解作用域了。一个作用域是程序的一块文本区域(textual region),在该文本区域内,对于某命名空间可以直接访问,而不需要通过属性访问。显然,作用域讨论的可见性是对直接访问而言。   2.Python作用域级别与闭包作用域   在内置(built-in/Python)、全局(global/模块)和本地(local/函数)3级作用域的基础上,Python 3.0又增添了一种闭包(closure/嵌套)作用域:如果在一个内部函数里,对在外部函数内(但不是在全局作用域)的变量进行访问(引用),那么内部函数就被认为是闭包。   这样,Python 3.0就形成如图3.8所示的从小到大的4级作用域:L(local,本地/局部)、E(enclosing,闭包/嵌套)、G(global,模块/全局)和B(built-in,内置/Python)。 图3.8 Python 3.0的4级作用域   作用域与名字空间是对应的。所以,Python 3.0也就有了对应的4级命名空间。   3.Python作用域规则   1)一般作用域规则   (1)内置标识符——内置命名空间的标识符在代码所有位置都是可见的,可以随时被访问。   (2)其他标识符(全局标识符和局部标识符)只有与某个命名空间绑定后,才可在这个作用域中可见——被引用。   代码3-38 企图访问未经绑定的变量错误示例。    >>> def f(): print (i) i = 100 >>> f() Traceback (most recent call last): File "", line 1, in f() File "", line 2, in f print (i) UnboundLocalError: local variable 'i' referenced before assignment      说明:在这段代码中,print()企图在未与所在的名字空间绑定之前访问(引用)名字i,引起UnboundLocalError错误。   (3)规则(2)最常见的形式是,在嵌套的命名空间中,内层命名空间中定义的名字在外层作用域中是不可见的;而外层命名空间中定义的名字在内层作用域中可以引用,但不可直接修改。   2)全局作用域规则   (1)全局作用域的作用范围仅限于单个文件(模块)。全局变量是位于该文件内部的顶层变量名。也就是说,这里的“全局”指的是在一个文件中位于顶层的变量名仅对该文件中的代码而言是全部的。   (2)全局变量可以在本地作用域(函数内部)中被引用,但不可以被直接赋值;只有经过global声明的全局变量,才可以在本地(局部)作用域中被赋值(修改)。   代码3-39 在本地(局部)作用域中引用全局变量示例。    >>> if _ _name_ _ == '_ _main_ _': a = 100 def f(): print(a) >>> f() 100      代码3-40 企图在本地(局部)作用域中修改全局变量示例。    >>> if _ _name_ _ == '_ _main_ _': a = 100 def f(): a += 100 print (a) >>> f() Traceback (most recent call last): File "", line 1, in f() File "", line 4, in f a += 100 UnboundLocalError: local variable 'a' referenced before assignment      代码3-41 在本地(局部)作用域中对用global修饰的全局变量赋值示例。    >>> if _ _name_ _ == '_ _main_ _': a = 100 def f(): global a a += 100 print (a) >>> f() 200      3)闭包作用域规则   在嵌套函数中,如果内层函数引用了外层函数的变量,则形成一个闭包。被引用的外层函数变量称为内层函数的自由变量。但是,自由变量只可在内层被引用,不可直接修改。只有使用关键字nonlocal声明的外层本地变量才是可以修改的。   代码3-42 自由变量被引用示例。    >>> def external(start): state = start def internal(label): print(label,state) #闭包中引用自由变量 return internal >>> F = external(3) >>> F('spam') spam 3 >>> F('nam') nam 3      代码3-43 企图直接修改自由变量的示例。    >>> def external(start): state = start def internal(label): state += 2 #企图在闭包中直接修改自由变量 print(label,state) return internal >>> F = external(3) >>> F('spam') Traceback (most recent call last): File "", line 1, in F('spam') File "", line 4, in internal state += 2 UnboundLocalError: local variable 'state' referenced before assignment      代码3-44 用nonlocal声明后的自由变量才可以被修改示例。    >>> def external(start): state = start def internal(label): nonlocal state #先用nonlocal声明自由变量 state += 2 #修改已经用nonlocal声明的自由变量 print(label,state) return internal >>> F = external(3) >>> F('spam') spam 5 >>> F('nam') nam 7      由上述示例可以看出:   (1)nonlocal语句与global语句的作用和用法非常相似。   (2)作用域一定是命名空间,而命名空间不一定是作用域。   4.globals()和locals()函数   locals()和globals()?是两个内置函数,可以分别以字典形式返回当前位置的可用本地命名空间和全局(包括了内置)命名空间。   代码3-45 locals()和globals()?应用示例。    >>> if _ _name_ _ == '_ _main_ _': a = 200 def external(start): print ('globals(1):',globals()) print ('locals(1):',locals()) state = start print ('locals(2):',locals()) def internal(label): print ('locals(3):',locals()) print(label,state) print ('locals(4):',locals()) return internal >>> F = external(3) globals(1): {'_ _name_ _': '_ _main_ _', '_ _doc_ _': None, '_ _package_ _': None, '_ _loader_ _': , '_ _spec_ _': None, '_ _annotations_ _': {}, '_ _builtins_ _': , 'a': 200, 'external': , 'F': .internal at 0x0000028973A53E18>} locals(1): {'start': 3} locals(2): {'start': 3, 'state': 3} >>> F('spam') locals(3): {'label': 'spam', 'state': 3} spam 3 locals(4): {'label': 'spam', 'state': 3}      结论:在不同位置,可用命名空间可能有所不同,因为名字与对象的绑定情况不同,也就是作用域不同。 3.3.3 Python名字解析的LEGB规则   作用域的意义在于告诉程序员如何正确地访问一个名字。为此,需要从这个名字去找它所绑定的对象。名字解析就是当在程序代码中遇到一个名字时,去正确地找到与之绑定的对象的过程。为了快速、正确地进行名字解析,需要一套正确且行之有效的规则。Python根据其“名字-对象”命名空间的4级层级关系提出了著名的LEGB-rule。这个规则可以简要描述为:Local→Enclosing→Global→Built-in。   具体地说,就是当在函数中使用未确定的变量名时,Python会按照优先级依次搜索4个作用域,以此确定该变量名的意义。首先搜索本地(局部)作用域(L),其次是上一层嵌套结构中def或lambda函数的嵌套作用域(E),之后是全局作用域(G),最后是内置作用域(B)。按这个查找规则,在第一处找到的地方停止。如果没有找到,则会提示NameError错误。   对程序员来说,掌握了这些规则,可以在出现有关错误信息时快速而正确地找到问题之所在。   代码3-46 用LEGB规则推断应用示例。    >>> if _ _name_ _ == '_ _main_ _': x = 'abcdefg' def test(): print (x) print (id(x)) x = 12345 print (x) print (id(x)) >>> test() Traceback (most recent call last): File "", line 2, in test() File "", line 2, in test print (x) UnboundLocalError: local variable 'x' referenced before assignment      说明:这段代码运行时出现UnboundLocalError错误。这是因为Python虽然是一种解释性语言,但代码还是需要编译成pyc文件来执行,只不过这个过程代码执行者看不见。此段代码在编译过程中按照?LEGB规则,首先将函数内部的x认定为本地变量,而不再是模块变量。因此,遇到第1个print(x)就认为这里的x是一个在赋值前就被引用的本地变量。   注意:LEGB规则仅对简单变量名有效。对类及其实例的属性的名字来说,查找规则则有所不同。 练习3.3 1.判断题 (1)global语句的作用是将本地变量升格为全局变量。 ( ) (2)nonlocal语句的作用是将全局变量降格为本地变量。 ( ) (3)本地变量创建于函数内部,其作用域从其被创建位置起,到函数返回为止。 ( ) (4)全局变量创建于所有函数的外部,并且可以被所有函数访问。 ( ) (5)在函数内部没有办法定义全局变量。 ( ) 2.代码分析题 阅读下面的代码,指出程序运行结果并说明原因。 (1)    a = 1 def second(): a = 2 def thirth(): global a print (a) thirth() print (a) second() print(a)    (2)    a = 1 def second(): a = 2 def thirth(): nonlocal a print (a) thirth() print (a) second() print(a)    (3)    x = 'abcd' def func(): print (x) func()    (4)    x = 'abcd' def func(): x = 'xyz' func() print (x) (5)    x = 'abcd' def func(): x = 'xyz' print (x) func() print (x)    (6)    x = 'abcd' def func(): global x = 'xyz' func() print (x)    (7)    x = 'abcd' def func(): x = 'xyz' def nested(): print (x) nested() func() x    (8)    def func(): x = 'xyz' def nested(): nonlocal x x = 'abcd' nested() print (x) func()