第4 章 函数、作用域与抽象

到目前为止,我们已经介绍了数值、赋值语句、输入/输出、比较语句和循环结构。它们在
Python中有多大用处呢?从理论上说,它们可以满足你的所有需求,也就是说,它是图灵完备的。
这意味着如果一个问题可以通过计算来解决,它就可以通过我们介绍过的那些语句来解决。
这并不是说你只能使用这些语句。我们已经介绍了很多语言机制,但代码还只是一个单独的
指令序列,所有指令都混合在一起。例如,第3章我们使用了图4-1中的代码。

x = 25
epsilon = 0.01
numGuesses = 0
low = 0.0
high = max(1.0, x)
ans = (high + low)/2.0
while abs(ans**2 - x) >= epsilon:
numGuesses += 1
if ans**2 < x:
low = ans
else:
high = ans
ans = (high + low)/2.0
print('numGuesses =', numGuesses)
print(ans, 'is close to square root of', x)

图4-1 使用二分查找求近似平方根

这段代码逻辑清晰,但缺少通用性。它只对变量x和epsilon表示的值有效。这意味着,如
果我们想重用这段代码,必须先复制它,而且可能需要编辑变量名,再将其粘贴到我们需要的地
方。因此,我们很难在其他更复杂的程序中使用这段代码。
而且,如果我们想计算的是立方根而不是平方根,那么就必须编辑代码。如果我们想要一个
程序既可以计算平方根也可以计算立方根(或者在两个不同的地方计算平方根),那么这个程序
中就会有很多几乎相同的大块代码。这是一件非常不好的事情。一个程序包含的代码越多,就越
容易出错,也越难以维护。举例来说,假设求平方根的初始程序中有一个错误,而直到测试程序
的时候这个错误才被曝光,那么很有可能只有一处修改了求平方根的代码,而忘记了其他地方的类似的代码也同样需要修改。
Python提供了若干种语言特性,可以相对容易地扩展和重用代码,其中最重要的就是函数。
4.1 函数与作用域
我们已经使用过了很多内置函数,如图4-1中的max和abs。程序员可以定义并使用自己的函
数,就像内置函数一样,这将在代码编写便捷性方面产生一个质的飞跃。
4.1.1 函数定义
在Python中,按如下形式进行函数定义①:
def name of function (list of formal parameters):
body of function
例如,我们可以使用如下代码定义函数maxVal②:
def maxVal(x, y):
if x > y:
return x
else:
return y
def是个保留字,告诉Python要定义一个函数。函数名(本示例中是maxVal)只是个名称,用来
引用函数。
函数名后面括号中的一系列名称(本例中是x, y)是函数的形式参数。使用函数时,形式参
数在函数调用时被绑定(和赋值语句一样)到实际参数(通常指代函数调用时的参数)。例如,
下面的函数调用
maxVal(3, 4)
会将x绑定到3,y绑定到4。
函数体可以是任何一段Python代码。③但是,还有一个特殊的return语句,只能用在函数体中。
函数调用是个表达式,和所有表达式一样,它也有一个值。这个值就是被调用函数返回的值。
例如,表达式maxVal(3, 4)*maxVal(3, 2)的值是12,因为第一次对maxVal的调用返回了4,第
二次则返回3。请注意,执行return语句会结束对函数的调用。
总结一下,当函数被调用时,会执行如下过程。
(1) 构成实参的表达式被求值,函数的形参被绑定到求值结果。例如,调用maxVal(3 + 4, z)
会在解释器求值这次调用时将形参x绑定到7,将形参y绑定到变量z的值。
(2) 执行点(要执行的下一条指令)从调用点转到函数体的第一条语句。

(3) 执行函数体中的代码,直至遇到return语句。这时,return后面的表达式的值就成为这
次函数调用的值;或者没有语句可以继续执行,这时函数返回的值为None;如果return后面没
有表达式,这次调用的值也为None。
(4) 这次函数调用的值就是返回值。
(5) 执行点移动到紧跟在这次函数调用后面的代码。
参数有一个特性,称为Lambda抽象①。它允许程序员编写的代码所处理的不是具体对象,而
是函数调用者选定用作实参的任何对象。
实际练习:编写一个函数isIn,接受两个字符串作为参数,如果一个字符串是另一个字符串
的一部分,返回True,否则返回False。提示:你可以使用内置的str类型的操作符in。
4.1.2 关键字参数和默认值
在Python中,有两种方法可以将形参绑定到实参。最常用的方法是使用位置参数,这是我们
目前使用过的唯一一种方法,即第一个形参绑定到第一个实参,第二个形参绑定到第二个实参,
以此类推。Python还支持关键字参数:形参根据名称绑定到实参。看下面的函数定义:
def printName(firstName, lastName, reverse):
if reverse:
print(lastName + ', ' + firstName)
else:
print(firstName, lastName)
函数printName假定firstNamelastName是字符串变量,reverse是布尔型变量。如果
reverse == True,就输出lastName, firstName,否则输出firstName lastName。
以下每行代码都是对printName的等价调用:
printName('Olga', 'Puchmajerova', False)
printName('Olga', 'Puchmajerova', reverse = False)
printName('Olga', lastName = 'Puchmajerova', reverse = False)
printName(lastName = 'Puchmajerova', firstName = ' Olga',
reverse = False)
尽管关键字参数可以在实参列表中以任意顺序出现,但将关键字参数放在非关键字参数后面
是不合法的。因此,下面的代码会产生一条错误信息:
printName('Olga', lastName = 'Puchmajerova', False)
关键字参数经常与默认参数值结合使用。例如下面的代码:
def printName(firstName, lastName, reverse = False):
if reverse:
print(lastName + ', ' + firstName)
else:
print(firstName, lastName)

默认值允许程序员不指定所有参数即可调用函数。如:
printName('Olga', 'Puchmajerova')
printName('Olga', 'Puchmajerova', True)
printName('Olga', 'Puchmajerova', reverse = True)
会输出:
Olga Puchmajerova
Puchmajerova, Olga
Puchmajerova, Olga
最后两次对printName的调用在语义上是等价的,只不过最后一次调用可以让阅读代码的人
知道True的意义。
4.1.3 作用域
我们看另一个小例子:
def f(x): #name x used as formal parameter
y = 1
x = x + y
print('x =', x)
return x
x = 3
y = 2
z = f(x) #value of x used as actual parameter
print('z =', z)
print('x =', x)
print('y =', y)
代码运行后会输出:
x = 4
z = 4
x = 3
y = 2
为什么会这样呢?调用f时,形参x在函数内部被绑定到实参x的值。需要特别注意的是,尽
管实参和形参的名称是一样的,但它们并不是同一个变量。每个函数都定义了一个命名空间,也
称为作用域。形式参数x和局部变量y在f内部使用,仅存在于f定义的作用域中。函数体中的赋值
语句x = x + y将局部名称x绑定到对象4。f中的赋值语句根本不会影响f作用域之外的名称x和y
的绑定。
对“作用域”可以进行如下理解。
(1) 在最顶层,比如shell层,有一个符号表会跟踪记录这一层所有的名称定义和它们当前的
绑定。
(2) 调用函数时,会建立一个新的符号表(常称为栈帧)。这个表跟踪记录函数中所有的名称
定义(包括形参)和它们当前的绑定。如果函数体内又调用了一个函数,就再建立一个栈帧。

(3) 函数结束时,它的栈帧也随之消失。
在Python中,可以通过阅读程序文本确定一个名称的作用域。这称为静态或词法作用域。图
4-2中的示例代码说明了Python的作用域规则。

def f(x):
def g():
x = 'abc'
print('x =', x)
def h():
z = x
print('z =', z)
x = x + 1
print('x =', x)
h()
g()
print('x =', x)
return g
x = 3
z = f(x)
print('x =', x)
print('z =', z)
z()

图4-2 嵌套作用域

与这段代码相关的栈帧历史如图4-3所示。图中第一列包含的是函数f之外的名称集合,也就
是变量x和y,以及函数名称f。第一条赋值语句将x绑定到3。

图4-3 栈帧

赋值语句z = f(x)首先使用与x绑定的值调用函数f,对表达式f(x)求值。进入函数f时,会

建立一个栈帧,如第二列所示。栈帧中的名称是x(形参,并不是调用上下文中的x)、g和h。变
量g和h被绑定到function类型的对象,这些函数的特性由f中的函数定义给出。
在函数f中调用函数h时,会建立另一个栈帧,如第三列所示。这个栈帧仅包含局部变量z。
为什么不包含x呢?只有一个名称是函数的形参或是被绑定到函数体内一个对象的变量时,才能
添加到函数作用域。在函数h内部,x仅仅出现在一个赋值语句的右侧。出现一个没有和函数体内
(函数h的内部)任何一个对象绑定的名称(本例中是x)时,解释器会搜索与该函数定义上层作
用域相关的栈帧(即与f相关的栈帧)。如果发现这个名称(x),就使用名称绑定的值(4)。如果
没有发现,就产生一条错误消息。
函数h返回后,与这次对h的调用相关的栈帧就会消失(从栈的顶端弹出),如第四列所示。
请注意,不能从栈的中间移除帧,只能移除最近添加的帧。正是因为它具有这种“后入先出”的
性质,所以我们称之为栈(就像自助餐厅中待用的盘子)。
然后调用函数g,一个包含g中局部变量x的栈帧被添加进来(第五列)。函数g返回后,这个
帧被弹出(第六列)。函数f返回后,包含函数f相关名称的栈帧被弹出,我们又回到最初的栈帧
(第七列)。
请注意,函数f返回时,尽管变量g不再存在,但与这个名称绑定过的function类型的对象
仍然存在。因为函数就是对象,而且可以像任何一种其他对象一样被返回。所以,z可以绑定到f
返回的值,而且z()也是函数调用,可以调用在f中与名称g绑定的函数——尽管名称g在f的上下
文之外是不可见的。
那么,图4-2中的代码会输出什么呢?它会输出:
x = 4
z = 4
x = abc
x = 4
x = 3
z = <function f.<locals>.g at 0x1092a7510>
x = abc
引用名称时,顺序并不重要。只要在函数体内任何地方有对象与名称进行绑定(即使在名称
作为赋值语句左侧项之前,就已经出现在某个表达式中),就认为这个名称是函数的局部变量①。
看下面的代码:
def f():
print(x)
def g():
print(x)
x = 1
x = 3
f()
x = 3
g()

调用函数f时,会输出3。但在函数g中,执行到print语句时,会产生一条错误信息:
UnboundLocalError: local variable 'x' referenced before assignment
发生这种情况是因为,print语句后面的赋值语句使x成为函数g中的局部变量,执行print语句
时还没有被赋值。
晕了吗?多数人都需要一点时间才能搞清楚作用域的规则。但不要太在意,现在你只需勇往
直前,大胆使用函数。你通常会发现,在函数中只需使用局部变量,那些作用域中的微妙之处毫
无影响。
4.2 规范
图4-4定义了一个函数findRoot,扩展了图4-1中求平方根的二分查找算法。图中还有一个函
数testFindRoot,用来测试findRoot是否像我们预期的那样工作。

def findRoot(x, power, epsilon):
"""x和epsilon是整数或者浮点数,power是整数
epsilon>0 且power>=1
如果y**power和x的差小于epsilon,就返回浮点数y,
否则返回None"""
if x < 0 and power%2 == 0: #Negative number has no even-powered
#roots
return None
low = min(-1.0, x)
high = max(1.0, x)
ans = (high + low)/2.0
while abs(ans**power - x) >= epsilon:
if ans**power < x:
low = ans
else:
high = ans
ans = (high + low)/2.0
return ans
def testFindRoot():
epsilon = 0.0001
for x in [0.25, -0.25, 2, -2, 8, -8]:
for power in range(1, 4):
print('Testing x =', str(x), 'and power = ', power)
result = findRoot(x, power, epsilon)
if result == None:
print(' No root')
else:
print(' ', result**power, '~=', x)

图4-4 求根的近似值

函数testFindRoot差不多与findRoot一样长。对于新手程序员来说,编写这样的测试函数好像是白费力气。但熟练的程序员深知,编写测试代码经常是“一本万利”的事情。它完全能够
避免在调试(找到程序不工作的原因并修复)过程中一次又一次地向shell输入测试用例,还可以
促使我们思考哪种测试最具启发性。
三引号之间的文本在Python中称为文档字符串。按照惯例,Python程序员使用文档字符串提
供函数的规范。可以使用内置函数help访问这些字符串。例如,如果我们进入shell并输入
help(abs),系统会显示:
Help on built-in function abs in module built-ins:
abs(x)
Return the absolute value of the argument.
如果图4-4中的代码已经被加载到IDE中,那么在shell中输入help(findRoot)会显示:
findRoot(x, power, epsilon)
Assumes x and epsilon int or float, power an int,
epsilon > 0 & power >= 1
Returns float y such that y**power is within epsilon of x.
If such a float does not exist, it returns None
如果在编辑器中输入findRoot(,会显示形参列表。
函数的规范定义了函数编写者与使用者之间的约定。我们将函数使用者称为客户。可以认为
约定包括以下两部分。
 假设:客户使用函数时必须满足的前提条件,通常是对实参的限制。它几乎总是限定每
个参数可以接受的变量类型,偶尔对一个或多个参数的取值添加限制条件。例如,函数
findRoot的文档字符串前两行描述了客户必须满足的假设。
 保证:调用方法满足条件时,函数应当实现的功能。函数findRoot的文档字符串后两行
描述了函数必须实现的结果保证。
函数是一种创建基本程序元素的方式。我们非常乐于像内置函数一样使用求根函数和很多其
他复杂操作,就像使用内置函数max和abs一样。函数通过分解和抽象的功能,大大提高了编程的
便捷性。
分解实现了程序结构化。它允许我们将程序分成多个逻辑上独立的部分,并可以通过各种设
定实现重用。
抽象隐藏了细节。它允许我们将一段代码当作黑箱使用。所谓黑箱,是指那些我们不能看见、
不需看见甚至根本不想看见内部细节的东西。①抽象的精髓在于,在具体背景之下,保留那些该
保留的,忽略那些该忽略的。在编程中有效使用抽象的关键在于,找到一个对于抽象创建者和抽
象潜在使用者都很合适的相关性表示。这才是真正的程序设计艺术。
抽象归根结底就是忽略。有很多方式可以对此形象地进行解释,例如,多数年轻人的听觉
器官。

年轻人:今晚我可以用一下汽车吗?
父母:可以,但必须在夜里12点之前回来,而且要加满油。
年轻人听到的:可以。
年轻人忽略了所有那些他认为不相关的烦人细节。抽象是个多对一的过程。即使父母说的是:
“可以,但必须在凌晨2点之前回来,别弄脏车。”也一定会被抽象为“可以”。
再打个比方,假设你需要准备一门包含25个课时的计算机科学课程。完成任务的一种方式是,
雇25位教授,请每个人按照他们最感兴趣的题目准备一节1小时的课程。尽管这样你能够得到25
小时的精彩演讲,但整个课程给人的感觉就像是皮兰德娄的戏剧《六个寻找作者的剧中人》一样
(或者像那种请了15位嘉宾的政治学课程)。如果每位教授都单独工作,他们就无法将自己课程中
的内容与其他课程中的内容联系起来。
不管怎样,在不产生太多不必要的工作量的情况下,我们应该让所有人都知道其他人在干什
么。这就是抽象的作用。你可以写出25份课程说明书,说明每节课中学生应该学到的内容,但并
不给出具体的授课细节。这样在教学上的效果可能不是最好的,但至少会起到一些作用。
组织机构使用程序员团队完成任务时,就采用这种方法。给定一个模块的规范,程序员就可
以着手实现这个模块,而不用过于担心团队中的其他程序员在做什么。而且,其他程序员也可以
使用这份规范编写使用这个模块的代码,不用过于担心这个模块是如何实现的。
findRoot的规范就是对所有满足其实现的一种抽象。findRoot的客户可以假定实现满足了
规范,但不能做更多假设。例如,客户可以假定调用findRoot(4.0, 2, 0.01)会返回一个平方
值为3.99~4.01的数值。但返回值可以是正数,也可以是负数。然而,即使4.0是一个完全平方数,
返回值也不一定是2.0或2.0。
4.3 递归
你可能听说过递归,也完全有可能认为这是一项高深的编程技术。它其实是计算机科学家广
为散布的一种具有迷惑性的都市传奇,目的是使人们认为我们比实际更聪明。递归是一种非常重
要的思想,但绝非高深莫测,而且它也不只是一项编程技术。
作为一种描述性方法,递归的应用非常广泛,甚至那些连做梦都没有编过程序的人也使用过
递归。我们可以看一下美国是如何定义“本国出生”的公民的,大致如下:
 任何在美国境内出生的儿童;
 在美国境外出生的婚生儿童,父母是美国公民,并且双亲之一在孩子出生前在美国居
住过;
 在美国境外出生的婚生儿童,双亲之一是美国公民,他(她)在孩子出生前至少在美国
居住5年,且其中至少2年是在其14岁生日之后。
第一部分非常简单;如果你出生在美国,那么你就是本国出生公民(如巴拉克·奥巴马)。
如果你不是在美国出生的,那么先确定你的父母是否是美国公民(本国出生或入籍)。为了确定父母是否是美国公民,必须看看祖父母的情况,以此类推。
一般情况下,递归定义包括两部分。其中至少有一种基本情形可以直接得出某种特定情形的
结果(如上面例子中的第一种情况),还至少有一种递归情形(或称归纳情形)定义了该问题在
其他情形下的结果,其他情形通常是同样问题的简化版本。
世界上最简单的递归定义可能是自然数的阶乘函数(在数学中一般使用!表示)①。经典的归
纳定义是:
1! = 1
(n +1)! = (n + 1) * n!
第一个等式定义了基本情形。第二个等式在前一个数的阶乘的基础上定义了所有自然数的阶
乘——除基本情形外。
图4-5给出阶乘的迭代实现(factI)和递归实现(factR)。

def factI(n): def factR(n):
"""假设n是正整数 """假设n是正整数
返回n!""" 返回n!"""
result = 1 if n == 1:
while n > 1: return n
result = result * n else:
n -= 1 return n*factR(n - 1)
return result

图4-5 阶乘的迭代实现和递归实现
这个函数相当简单,所以每种实现都很好理解。但第二种实现方式是对阶乘初始递归定义的
一种更明显的转译。
通过在factR函数体内调用factR实现factR,这看上去像是在作弊。这种实现是有效的,其
工作原理与迭代实现其实一样。我们知道factI中的迭代终会结束,因为n从一个正值开始,每次
循环都会减1。这意味着,n的值不能永远大于1。同样地,如果调用factR时传入1,那么它不用
进行递归调用就可以返回一个值。当它进行递归调用时,使用的值总是比调用它所用的值小1。
最终,递归会在调用factR(1)时结束。
4.3.1 斐波那契数列
斐波那契数列是另一个经常使用递归方式定义的常用数学函数。“他们像兔子一样繁殖”经
常用来形容人口增长过快。1202年,意大利数学家比萨的列奥纳多(也称为斐波那契)得出了一个公式,用来计算兔子的繁殖情况。尽管在我们看来,他的假设有些不太现实。①
假设一对新生的兔子被放到兔栏中(更坏的情况是放到野外),一只是公兔,一只是母兔。
再假设兔子在一个月大时就可以交配(令人惊奇的是,有些品种确实可以),并有一个月的妊娠
期(令人惊奇的是,有些品种确实如此)。最后,假设这些神话般的兔子永远不死,并且母兔从
第二个月之后每月都能产下一对小兔(一公一母)。那么6个月后,会有多少只母兔?
第一个月的最后一天(称之为第0月),只有1只母兔(准备在下个月的第一天怀孕)。第二个
月的最后一天,还是只有1只母兔(因为不到下个月的第一天,它不会分娩)。下个月的第一天,
会有2只母兔(一只怀孕,一只没怀孕)。以此类推,可以在图4-6的表格中看到这个过程。

图4-6 母兔数量的增长

请注意,对于n > 1的月份,females(n) = females(n  1) + females(n  2),females(n)表示第n个
月的母兔数量。这绝非偶然。每只在第n – 1月活着的母兔在第n月仍然活着,而且,每只在第n –
2月活着的母兔会在第n月产下一只新的母兔。新的母兔加上在第n – 1月活着的母兔,就是第n月
母兔的数量。
母兔数量的增长可以很自然地使用以下递推公式描述②:
females(0) = 1
females(1) = 1
females(n + 2) = females(n+1) + females(n)
这个定义与阶乘的递归定义有些不同。
 它有两种基本情形,而不是一种。一般来说,只要需要,我们可以有任意多种基本情形。
 在递归情形中,有两个递归调用,而不是一个。同样,如果需要,可以有任意多个调用。
图4-7包含了斐波那契递推的直接实现③,以及一个用来测试数列的函数。

def fib(n):
"""假定n是正整数
返回第n个斐波那契数"""
if n == 0 or n == 1:
return 1
else:
return fib(n-1) + fib(n-2)
def testFib(n):
for i in range(n+1):
print('fib of', i, '=', fib(i))

图4-7 斐波那契数列的递归实现

解决这个问题时,编写代码是最容易的一个环节。一旦将这个关于兔子的模糊问题明确为一
组递归公式,代码几乎就自动完成了。找到某种抽象方式表示问题的解,常常是编程过程中最困
难的部分。本书后续内容会深入讨论这个问题。
或许你已经猜到了,这个模型并不适用于描述野生兔子数量的增长。1859年,澳大利亚农场主
托马斯·奥斯汀从英格兰进口了24只兔子作为狩猎目标。10年后,澳大利亚每年大约有2 000 000
只兔子被射杀或被捕获,但对整体数量还是没有明显的影响。兔子太多了,远远超过第120个斐
波那契数。①
尽管斐波那契数列确实不是一个能够精确预测兔子数量增长的模型,但它仍然具有很多有趣
的数学特性。斐波那契数在自然界中也很常见。
实际练习:如果使用图4-7中的函数fib计算fib(5),那么需要计算多少次fib(2)的值?
4.3.2 回文
递归也经常用于很多与数值无关的问题中。图4-8包含了一个函数isPalindrome,可以检查
一个字符串在顺读和倒读时是否一样。
函数isPalindrome包含了两个内部辅助函数。主函数的客户不关心辅助函数,他们只关心
isPalindrome是否符合规范。但你不能不关心,因为通过研究这种实现方式可以学到一些东西。
辅助函数toChars将所有字母转换为小写,并且移除了所有非字母字符。它首先使用一种内
置字符串方法生成一个与s完全一样的字符串,唯一的区别是所有大写字母都转换为小写。我们
讲到“类”的时候会介绍更多关于方法调用的知识,眼下可以将它看作一种特殊形式的函数调用。
调用时,我们不将第一个(本例中是唯一一个)参数放在函数名后面的小括号中,而使用点标记
法将这个参数放在函数名之前。

def isPalindrome(s):
"""假设s是字符串
如果s是回文字符串则返回True,否则返回False。
忽略标点符号、空格和大小写。"""
def toChars(s):
s = s.lower()
letters = ''
for c in s:
if c in 'abcdefghijklmnopqrstuvwxyz':
letters = letters + c
return letters
def isPal(s):
if len(s) <= 1:
return True
else:
return s[0] == s[-1] and isPal(s[1:-1])
return isPal(toChars(s))

图4-8 回文检测

辅助函数isPal使用递归完成实际的工作。两种基本情形是长度为0或1的字符串,这说明,
只有字符串长度为2或更大时,才能出现递归情形。else字句中的合取项①是从左到右进行求值的。
代码先检查字符串的第一个字母和最后一个字母是否相同,如果相同,则继续检查去掉这两个字
母之后的字符串是否是回文字符串。除非第一个合取项取值为True,否则第二个合取项不被求值。
在本例子中,这一点在语义上是无关的。但在本书后面,我们会给出一个例子,其中这种布尔表
达式的短路求值在语义上是相关的。
这种对isPalindrome的实现是分治策略的典型例子。(这种原则与分治算法密切相关,但又
有点不一样,我们会在第10章进行讨论。)这种解决问题的原则就是,将一个困难问题分解成一
组子问题逐个解决。分解出来的子问题具有以下特性:
 子问题比初始问题更容易解决;
 子问题的解决方案可以组合起来解决初始问题。
“分治策略”是一种非常古老的思想。裘力斯·凯撒实行了罗马人所称的“分而治之”(divide
et impera),英国人也通过这种方式出色地控制了印度次大陆。本杰明·富兰克林非常熟悉英国
人的这套玩弄权术的把戏,这使得他在签署美国《独立宣言》时说出了那句著名的话:“我们必
须团结一致,否则就必定会被分别绞死。”
本例中,我们将初始问题分解为一个更简单的情形(检查一个更短的字符串是否是回文字符
串)和一个我们可以解决的简单情形(比较单个字符),然后使用and将这两个问题的解组合起来。
图4-9中的代码可以告诉我们如何实现这种解决方式。

def isPalindrome(s):
"""假设s是字符串
如果s是回文字符串则返回True,否则返回False。
忽略标点符号、空格和大小写。"""
def toChars(s):
s = s.lower()
letters = ''
for c in s:
if c in 'abcdefghijklmnopqrstuvwxyz':
letters = letters + c
return letters
def isPal(s):
print(' isPal called with', s)
if len(s) <= 1:
print(' About to return True from base case')
return True
else:
answer = s[0] == s[-1] and isPal(s[1:-1])
print(' About to return', answer, 'for', s)
return answer
return isPal(toChars(s))
def testIsPalindrome():
print('Try dogGod')
print(isPalindrome('dogGod'))
print('Try doGood')
print(isPalindrome('doGood'))

图4-9 实现回文检测的代码

运行testIsPalindrome时,它会输出:
Try dogGod
isPal called with doggod
isPal called with oggo
isPal called with gg
isPal called with
About to return True from base case
About to return True for gg
About to return True for oggo
About to return True for doggod
True
Try doGood
isPal called with dogood
isPal called with ogoo
isPal called with go
About to return False for go
About to return False for ogoo
About to return False for dogood
False

4.4 全局变量
如果试着使用一个非常大的数调用函数fib,那么你可能会发现函数需要运行很长一段时间。
假设我们想知道究竟进行了多少次递归调用,可以对代码进行仔细分析,然后找出答案,第9章
会讨论如何操作。另外一种方法是,添加一些代码计算调用次数。这时就要使用全局变量。
我们之前编写的所有函数中,只能通过参数和返回值和外部环境进行交互。在多数情况下应
该这么做,因为这样可以使程序更容易阅读、测试和调试。但偶尔会有一些时候,使用全局变量
更加方便。看图4-10中的代码。

def fib(x):
"""假设x是正整数
返回第x个斐波那契数"""
global numFibCalls
numFibCalls += 1
if x == 0 or x == 1:
return 1
else:
return fib(x-1) + fib(x-2)
def testFib(n):
for i in range(n+1):
global numFibCalls
numFibCalls = 0
print('fib of', i, '=', fib(i))
print('fib called', numFibCalls, 'times.')

图4-10 使用全局变量
每个函数中,global numFibCalls这行代码都会告诉Python,名称numFibCalls是定义在代
码所在函数外层的模块(参见4.5节)作用域中的,而不是在代码所在函数的作用域中的。如果
我们没有包括global numFibCalls这行代码,那么名称numFibCalls就会被认为是函数fib和
testFib的局部变量。因为在fib和testFib这两个函数中,numFibCalls出现在赋值语句的左侧。
函数fib和testFib都可以不受限制地访问变量numFibCalls引用的对象,函数testFib每次调用
fib时,都将numFibCalls绑定到0。每次进入函数fib时,fib都会增加numFibCalls的值。
调用fib(6),会生成如下输出:
fib of 0 = 1
fib called 1 times.
fib of 1 = 1
fib called 1 times.
fib of 2 = 2
fib called 3 times.
fib of 3 = 3
fib called 5 times.
fib of 4 = 5
fib called 9 times.
fib of 5 = 8
fib called 15 times.
fib of 6 = 13
fib called 25 times.
介绍全局变量这个主题时,我们是心怀忐忑的。从20世纪70年代开始,正统计算机科学家都
强烈反对使用全局变量,因为随意使用全局变量会引发很多问题。使程序清晰易读的关键就是局
部性。人们一次只能阅读一段程序,理解这段程序所需的上下文越少,效果就越好。因为全局变
量可以在程序中的很多地方被修改或读取,所以草率地使用全局变量会破坏局部性。尽管如此,
全局变量有时真的很有用。

4.5 模块
迄今为止,我们进行各种操作的前提是,假设整个程序保存在一个文件中。当程序比较小时,
这样做非常合理。但程序变得越来越大时,将程序的不同部分保存在不同文件中通常会更加方便。
例如,假设多人合作编写同一个程序,那么他们试图更新同一个文件时,那简直就是噩梦。Python
模块允许我们方便地使用多个文件中的代码来构建程序。
模块就是一个包含Python定义和语句的.py文件。例如,我们可以创建一个包含图4-11代码的
circle.py文件。

pi = 3.14159
def area(radius):
return pi*(radius**2)
def circumference(radius):
return 2*pi*radius
def sphereSurface(radius):
return 4.0*area(radius)
def sphereVolume(radius):
return (4.0/3.0)*pi*(radius**3)

图4-11 一些关于圆与球的代码

程序可以通过import语句访问一个模块。如下面的代码:
import circle
pi = 3
print(pi)
print(circle.pi)
print(circle.area(3))
print(circle.circumference(3))
print(circle.sphereSurface(3))

会输出:
3
3.14159
28.27431
18.849539999999998
113.09724
模块通常保存在单独的文件中。每个模块都有自己的私有符号表,所以,在circle.py中,我
们可以像往常一样访问对象(如pi和area)。运行import M语句后,会将模块M绑定到import语
句所在的作用域中。因此,在导入上下文中,我们使用点标记法表示引用的名称是定义在导入模
块中的。①例如,在circle.py外部,pi和circle.pi表示引用的是不同的对象(在本例中的确如此)。
乍看上去,使用点标记法有些麻烦。但换个角度想想,当我们导入一个模块时,根本不知道
这个模块在实现时使用了哪些局部名称。使用点标记法可以充分限定变量名,避免名称冲突造成
程序损害的可能性。例如,在circle模块外部执行赋值语句pi = 3时,就不会改变circle模块
内部的pi的值。
还有一种import语句的变种,允许导入程序不需使用模块名称即可访问定义在被导入模块中
的名称。执行语句from M import *会将M中定义的所有对象绑定到当前作用域,而不是M本身。
例如,以下代码:
from circle import *
print(pi)
print(circle.pi)
会先输出3.14159,然后产生一条错误消息:
NameError: name 'circle' is not defined
有些Python程序员不赞成使用这种形式的import语句,因为他们相信这种方式增加了代码的
阅读难度。
正如我们所见,模块可以包含可执行的语句,也可以包含函数定义。通常,这些语句用来对
模块进行初始化。基于这个原因,模块中的语句仅在模块第一次被导入程序时才执行。而且,一
个模块在每个解释器会话中只能被导入一次。如果你启动了shell,导入一个模块,然后修改这个
模块中的内容,那么解释器仍然会继续使用这个模块的初始版本。这在调试程序时会引起令人困
惑的状况。你疑惑不解时,可以启动一个新的shell。
现在很多有用的模块已经成为标准Python程序库的一部分。例如,现在几乎不需要你自己实
现一般的数学函数和字符串函数。关于Python程序库的详细说明请参考:http://docs.python.org/2/
library/。
4.6 文件
所有计算机系统都使用文件保存计算过程的结果,并供下次计算使用。Python为创建和使用文件提供了非常多的功能。下面介绍几种最基本的方式。
每种操作系统(如Windows和MAC OS)都通过自己的文件系统创建和使用文件。Python通
过文件句柄处理文件,实现了操作系统的独立性。以下代码:
nameHandle = open('kids', 'w')
指示操作系统创建一个名为kids的文件,并返回其文件句柄。open函数的参数'w'表示文件是以
可写方式打开的。下面的代码打开一个文件,使用write方法向文件写入两行数据,然后关闭文
件。程序使用完文件后,请一定记得关闭文件,否则写入的内容可能部分或全部丢失。
nameHandle = open('kids', 'w')
for i in range(2):
name = input('Enter name: ')
nameHandle.write(name + '\n')
nameHandle.close()
在Python字符串中,转义字符\用来表示它后面的字符具有特殊意义。在本例中,字符串'\n'
表示一个换行符。
我们可以以只读方式打开文件(使用参数'r'),然后输出其中的内容。因为Python将文件看
成是行的序列,所以可以使用for语句遍历文件内容:
nameHandle = open('kids', 'r')
for line in nameHandle:
print(line)
nameHandle.close()
如果输入名称David和Andrea,就会输出:
David
Andrea
David和Andrea之间有一个空行,因为每次输出到文件行尾的'\n'时,都会开始一个新行。可以
使用print line[:-1]避免输出空行。下面的代码:
nameHandle = open('kids', 'w')
nameHandle.write('Michael\n')
nameHandle.write('Mark\n')
nameHandle.close()
nameHandle = open('kids', 'r')
for line in nameHandle:
print(line[:-1])
nameHandle.close()
会输出:
Michael
Mark
请注意,我们覆盖了文件kids原来的内容。如果不想这样做,可以使用参数'a'用追加(不
使用可写方式)方式打开文件。例如,如果运行下面的代码:

nameHandle = open('kids', 'a')
nameHandle.write('David\n')
nameHandle.write('Andrea\n')
nameHandle.close()
nameHandle = open('kids', 'r')
for line in nameHandle:
print(line[:-1])
nameHandle.close()

会输出:
Michael
Mark
David
Andrea
图4-12总结了一些常用的文件操作。

open(fn, 'w'):fn是一个表示文件名的字符串。创建一个文件用来写入数据,返回文件
句柄。
open(fn, 'r'):fn是一个表示文件名的字符串。打开一个已有文件读取数据,返回文件
句柄。
open(fn, 'a'):fn是一个表示文件名的字符串。打开一个已有文件用来追加数据,返回文件
句柄。
fh.read():返回一个字符串,其中包含与文件句柄fh相关的文件中的内容。
fh.readline():返回与文件句柄fh相关的文件中的下一行。
fh.readlines():返回一个列表,列表中的每个元素都是与文件句柄fh相关的文件中的一行。
fh.write(s):将字符串s写入与文件句柄fh相关的文件末尾。
fh.writeLines(S):S是个字符串序列。将S中的每个元素作为一个单独的行写入与文件句柄
fh相关的文件。
fh.close():关闭与文件句柄fh相关的文件。

图4-12 文件操作常用函数


 

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

___Y1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值