第6章 抽象
本章会介绍如何将语句组织成函数,这样,你可以告诉计算机如何做事,并且只需告诉一次。有了函数以后,就不必反反复复向计算机传递同样的具体指令了。本章还会详细介绍参数(parameter)和作用域(scope)的概念,以及递归的概念及其在程序中的用途。
6.1懒惰即美德
目前为止我们所写的程序都很小,如果想要编写大型程序,很快就会遇到麻烦。考虑一下如果在一个地方编写了一段代码,但在另一个地方也要用到这段代码,这时会发生什么。例如,假设我们编写了一小段代码来计算斐波那契数列(任一个数都是前两数之和的数字序列):
fibs = [0, 1] for i in range(8): fibs.append(fibs[-2]+fibs[-1])
运行之后,fibs会包含斐波那契数列的前10个数字:
>>> fibs
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
如果想要一次计算前10个数的话,没有问题。你甚至可以将用户输入的数字作为动态范围的长度使用,从而改变for语句块循环的次数:
fibs = [0, 1] num = int(input('How many Fibonacci numbers do you want? ')) for i in range(num-2): fibs.append(fibs[-2]+fibs[-1]) print(fibs)
但是如果想用这些数字做其他事情呢?当然可以在需要的时候重写同样的循环,但是如果已经编写的是一段复杂的代码——比如下载一系列网页并且计算词频——应该怎么做呢?你是否希望在每次需要的时候把所有的代码重写一遍呢?当然不用,真正的程序员不会这么做的,他们都很懒,但不是用错误的方式犯懒,换句话说就是他们不做无用功。
那么真正的程序员怎么做?他们会让自己的程序抽象一些,上面的程序可以改写为较抽象的版本:
num = int(input('How many Fibonacci numbers do you want? ')) print(fibs(num))
这个程序的具体细节已经写得很清楚了(读入数值,然后打印结果)。事实上计算斐波那契数列是由一种抽象的方式完成的:只需要告诉计算机去做就好,不用特别说明应该怎么做。名为fibs的函数被创建,然后在需要计算斐波那契数列的地方调用它即可。如果这函数要被调用很多次的话,这么做会节省很多精力。
6.2抽象和结构
抽象可以节省很多工作,实际上它的作用还要更大,它是使得计算机程序可以让人读懂的关键(这也是最基本的要求,不管是读还是写程序)。计算机非常乐于处理精确和具体的指令,但是人可就不同了。如果由人问我去电影院怎么走,估计他也不会希望我回答“向前走10步,左转90度,再走5步,右转45度,走123步”。弄不好就迷路了,对吧?
现在,如果我告诉他“一直沿着街走,过桥,电影院就在左手边”,这样就明白多了吧!关键在于大家都知道怎么走路和过桥,不需要明确指令来指导这些事。
组织计算机程序也是类似的。程序应该是非常抽象的,就像“下载网页、计算频率,打印每个单词的频率”一样易懂。事实上,我们现在就能把这段描述翻译成Python程序:
page = download_page() freqs = compute_frequencies(page) for word, freq in freqs: print(word, freq)
虽然没有明确地说出它是怎么做的,但读完代码就知道程序做什么了。只需要告诉计算机下载网页并且计算词频。这些操作的具体细节会在其他地方给出——在单独的函数定义中。
6.3创建函数
函数是可以调用的(可能带有参数,也就是放在圆括号中的值),它执行某种行为并且返回一个值。
就像前一节内容中介绍的,创建函数是组织程序的关键。那么怎么定义函数呢?使用def(或“函数定义”)语句即可:
def hello(name): print('Hello,' + name + '!')
运行这段程序就会得到一个名为hello的新函数,它可以返回一个将输入的参数作为名字的问候语。可以像使用内建函数一样使用它:
>>> print(hello('world')) Hello,world! >>> print(hello('Gumby')) Hello,Gumby!
很精巧吧?那么想想看怎么写个返回斐波那契数列列表的函数吧。简单!只需要使用刚才的代码,把从用户输入获取数字改为作为参数接收数字:
def fibs(num): result = [0, 1] for i in range(num-2): result.append(result[-2]+result[-1]) return result
执行这段语句后,编译器就知道如何计算斐波那契数列了——所以现在就不用关注细节了,只要用函数fibs就行:
>>> fibs(10) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] >>> fibs(15) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
本例中的num和result的名字都是随便起的,但是return语句非常重要。return语句是用来从函数中返回值的(前例中的hello函数也有用到)。
6.3.1文档化函数
如果想要给函数写文档,让其他使用该函数的人能理解的话,可以加入注释(以#开头)。另外一个方式就是直接写上字符串。这类字符串在其他地方可能会非常有用,比如在def语句后面(以及在模块或类的开头)。如果在函数的开头写下字符串,它就会作为函数的一部分进行存储,这称为文档字符串。下面代码演示了如何给函数添加文档字符串:
def square(x): 'Calculates the square of the number x.' return x*x
文档字符串可以按如下方式访问:
>>> square.__doc__ 'Calculates the square of the number x.'
注意:__doc__是函数属性。属性名中的双下划线表示它是个特殊属性。
内建的help函数是非常有用的。在交互式解释器中使用它,就可以得到关于函数,包括它的文档字符串的信息:
>>> help(square) Help on function square in module __main__: square(x) Calculates the square of the number x.
6.3.2并非真正函数的函数
数学意义上的函数,总在计算其参数后返回点什么。Python的有些函数却并不返回任何东西。在其他语言中(比如Pascal),这类函数可能有其他名字(比如过程)。但是Python的函数就是函数,即便它从学术上讲并不是函数。没有return语句,或者虽有return语句但return后边没有跟任何值的函数不返回值:
def test(): print('This is printed') return print('This is not')
这里的return语句只起到结束函数的作用:
>>> x = test() This is printed
可以看到,第2个print语句被跳过了(类似于循环中的break语句,不过这里是跳出函数)。但是如果test不返回任何值,那么x又引用什么呢?让我们看看:
>>> x
>>>
没东西,再仔细看看:
>>> print(x) None
好熟悉的值:None。所以所有的函数的确都返回了东西:当不需要它们返回值的时候,它们就返回None。看来刚才“有些函数并不是真的是函数”的说法有些不公平了。
警告:千万不要被默认行为所迷惑。如果在if语句内返回值,那么要确保其他分支也有返回值,这样一来当调用者期待一个序列的时候,就不会意外地返回None。
6.4参数魔法
函数使用起来很简单,创建起来也不复杂。但函数参数的用法有时就有些神奇了。还是线从最基础的介绍起。
6.4.1值从哪里来
函数被定义后,所操作的值是从哪里来的呢?一般来说不用担心这些,编写函数只是给程序需要的部分(也可能是其他程序)提供服务,能保证函数在被提供给可接收参数的时候正常工作就行,参数错误的话显然会导致失败(一般来说这时候要用断言和异常)。
注意:写在def语句中函数名后面的变量通常叫做函数的形参,而调用函数的时候提供的值是实参,或者称为参数。
6.4.2我能改变参数吗
函数通过它的参数获得一系列值。那么这些值能改变吗?如果改变了又会怎样?参数只是变量而已,所以它们的行为其实和你预想的一样。在函数内为参数赋予新值不会改变外部任何变量的值:
>>> def try_to_change(n): n = 'Mr. Gumby' >>> name = 'Mrs. Entity' >>> try_to_change(name) >>> name 'Mrs. Entity'
在try_to_change内,参数n获得了新值,但是它没有影响到name变量。n实际上是个完全不同的变量,具体的工作方式类似于下面这样:
>>> name = 'Mrs. Entity' >>> n = name # 这句的作用基本上等于传参数 >>> n = 'Mr. Gumby' # 在函数内部完成的 >>> name 'Mrs. Entity'
结果是显而易见的。当变量n改变的时候,变量name不变。同样,当在函数内部把参数重绑定(赋值)的时候,函数外的变量不会受到影响。
注意:参数存储在局部作用域(local scope)内。
字符串(以及数字和元组)是不可变的,即无法被修改(耶就是说只能用新的值覆盖)。所以它们做参数的时候也就无需多做介绍。但是考虑一下如果将可变的数据结构如列表用作参数的时候会发生什么:
>>> def change(n): n[0] = 'Mr. Gumby' >>> names = ['Mrs. Entity', 'Mrs. Thing'] >>> change(names) >>> names ['Mr. Gumby', 'Mrs. Thing']
本例中,参数被改变了。这就是本例和前面例子中至关重要的区别。前面的例子中,局部变量被赋予了新值,但是这个例子中变量names所绑定的列表的确改变了。有些奇怪吧?其实这种行为并不奇怪,下面不用函数调用再做一次:
>>> names = ['Mrs. Entity', 'Mrs. Thing'] >>> n = names # 再来一次,模拟传参行为 >>> n[0] = 'Mr. Gumby' # 改变列表 >>> names ['Mr. Gumby', 'Mrs. Thing']
这类情况在前面已经出现了多次。当两个变量同时引用一个列表的时候,它们的确是同时引用一个列表。就是这么简单。如果想避免出现这种情况,可以复制一个列表的副本。当在序列中做切片的时候,返回的切片总是一个副本。因此,如果你复制了整个列表的切片,将会得到一个副本:
>>> names = ['Mrs. Entity', 'Mrs. Thing'] >>> n = names[:]
现在n和names包含两个独立(不同)的列表,其值相等:
>>> n is names False >>> n == names True
如果现在改变n(就像在函数change中做的一样),则不会影响到names:
>>> n[0] = 'Mr, Gumby' >>> n ['Mr, Gumby', 'Mrs. Thing'] >>> names ['Mrs. Entity', 'Mrs. Thing']
再用change试一下:
>>> change(names[:]) >>> names ['Mrs. Entity', 'Mrs. Thing']
现在参数n包含一个副本,而原始的列表是安全的。
注意:函数的局部名称——包括参数在内——并不和外面的函数名称(全局的)冲突。
1.为什么要修改参数
使用函数改变数据结构(比如列表或字典)是一种将程序抽象化的好方法。假设需要编写一个存储名字并且能用名字、中间名或姓查找联系人的程序,可以使用下面的数据结构:
storage = {} storage['first'] = {} storage['middle'] = {} storage['last'] = {}
storage这个数据结构是带有3个键‘first’、‘middle’、‘last’的字典。每个键下面都又存储一个字典。子字典中,可以使用名字(名字、中间名或姓)作为键,插入联系人列表作为值。比如要把我自己的名字加入这个数据结构,可以像下面这么做:
>>> me = 'Magnus Lie Hetland' >>> storage['first']['Magnus'] = [me] >>> storage['middle']['Lie'] = [me] >>> storage['last']['Hetland'] = [me]
每个键下面都存储了一个以人名组成的列表。本例中,列表中只有我。
现在如果想得到所有注册的中间名为Lie的人,可以像下面这么做:
>>> storage['middle']['Lie'] ['Magnus Lie Hetland']
将人名加入列表中的步骤有点枯燥乏味,尤其是要加入很多姓名相同的人时,因为需要扩展已经存储了那些名字的列表。例如,下面加入我姐姐的名字,而且假设不知道数据库中已经存储了什么:
>>> my_sister = 'Anne Lie Hetland' >>> storage['first'].setdefault('Anne', []).append(my_sister) >>> storage['middle'].setdefault('Lie', []).append(my_sister) >>> storage['last'].setdefault('Hetland', []).append(my_sister) >>> storage['first']['Anne'] ['Anne Lie Hetland'] >>> storage['middle']['Lie'] ['Magnus Lie Hetland', 'Anne Lie Hetland']
如果要写个大程序来这样更新列表,那么很显然程序很快就会变得臃肿且笨拙不堪了。
抽象的要点就是隐藏更新时繁琐的细节,这个过程可以用函数实现。下面的例子就是初始化数据结构的函数:
def init(data): data['first'] = {} data['middle'] = {} data['last'] = {}
上面的代码只是把初始化语句放到了函数中,使用方法如下:
>>> storage = {} >>> init(storage) >>> storage {'first': {}, 'middle': {}, 'last': {}}
可以看到,函数包办了初始化的工作,让代码更易读。
注意:字典的键并没有特定的顺序,所以当字典打印出来的时候,顺序是不同的。
在编写存储名字的函数前,先写个获得名字的函数:
def lookup(data, label, name): return data[label].get(name)
标签(比如‘middle’)以及名字(比如‘Lie’)可以作为参数提供给lookup函数使用,这样会获得包含全名的列表。换句话说,如果我的名字已经存储了,可以像下面这样做:
>>> lookup(storage, 'middle', 'Lie')
注意,返回的列表和存储在数据结构中的列表是相同的,所以如果列表被修改了,那么也会影响数据结构(没有查询到人的时候就问题不大了,因为函数返回的是None)。
def storage(data, full_name): names = full_name.split() if len(names) == 2: names.insert(1, ' ') labels = 'first', 'middle', 'last' for label, name in zip(labels, names): people = lookup(data, label, name) if people: people.append(full_name) else: data[label][name] = [full_name]
store函数执行以下步骤。
(1)使用参数data和full_name进入函数,这两个参数被设置为函数在外部获得的一些值。
(2)通过拆分full_name,得到一个叫做names的列表。
(3)如果names的长度为2(只有首名和末名),那么插入一个空字符串作为中间名。
(4)将字符串‘first’、‘middle’和‘last’作为元组存储在labels中(也可以使用列表,这里只是为了方便而去掉括号)。
(5)使用zip函数联合标签和名字,对于每一个(lable,name)对,进行以下处理:
- 获得属于给定标签和名字的列表;
- 将full_name添加到列表中,或者插入一个需要的新列表。
来试用一下刚刚实现的程序:
>>> MyNames = {} >>> init(MyNames) >>> storage(MyNames, 'Magnus Lie Hetland') >>> lookup(MyNames, 'middle', 'Lie') ['Magnus Lie Hetland']
好像可以工作,再试试:
>>> storage(MyNames, 'Robbin Hood') >>> storage(MyNames, 'Robbin Locksley') >>> lookup(MyNames, 'first', 'Robbin') ['Robbin Hood', 'Robbin Locksley'] >>> storage(MyNames, 'Mr. Gumby') >>> lookup(MyNames, 'middle', ' ') ['Robbin Hood', 'Robbin Locksley', 'Mr. Gumby']
可以看到,如果某些人的名字、中间名或姓相同,那么结果中会包含所有这些人的信息。
注意:这类程序很适合进行面向对象程序设计。
2.如果我的参数不可变呢
在某些语言(比如C++、Pascal和Ada)中,重新绑定参数并且使这些改变影响到函数外的变量是很平常的事情。但在Python中这是不可能的:函数只能修改参数对象本身。但是如果你的参数不可变(比如是数字),又该怎么办呢?
不好意思,没有办法。这个时候你应该从函数中返回所有你需要的值(如果多于一个的话就以元组形式返回)。例如,将变量的数值增1的函数可以这样写:
>>> def inc(x): return x + 1 >>> foo = 10 >>> foo = inc(foo) >>> foo 11
如果真的想改变参数,那么可以使用一点小技巧,即将值放置在列表中:
>>> def inc(x): x[0] = x[0] + 1 >>> foo = [10] >>> inc(foo) >>> foo [11]
这样就会只返回新值,代码看起来也比较清晰。
6.4.3关键字参数和默认值
目前位置我们所使用的参数都叫做位置参数,因为它们的位置很重要,事实上比它们的名字更加重要。本节中引入的这个功能可以回避位置问题,当你慢慢习惯使用这个功能以后,就会发现程序规模越大,它们的作用也就越大。
考虑下面的两个函数:
>>> def hello_1(greeting, name): print('%s, %s!' % (greeting, name)) >>> def hello_2(name, greeting): print('%s, %s!' % (name, greeting))
两个代码所实现的是完全一样的功能,只是参数顺序反过来了:
>>> hello_1('Hello', 'world') Hello, world! >>> hello_1('Hello', 'world') Hello, world!
有些时候(尤其是参数很多的时候),参数的顺序是很难记住的。为了让事情简单些,可以提供参数的名字:
>>> hello_1(greeting='Hello', name='world') Hello, world!
这样一来,顺序就完全没影响了:
>>> hello_1(name='world',greeting='Hello') Hello, world!
但参数名和值一定要对应:
>>> hello_2(greeting='Hello', name='world') world, Hello!
这类使用参数名提供的参数叫做关键字参数。它的主要作用在于可以明确每个参数的作用,也就避免了下面这样的奇怪的函数调用:
>>> store('Mr. Brainsample', 10, 20, 13, 5)
可以使用:
>>> store(patient='Mr. Brainsample', hour=10, minute=20, day=13, month=5)
尽管这么做打的字就多了些,但是很显然,每个参数的含义变得更加清晰。而且就算弄乱了参数的顺序,对于程序的功能也没有任何影响。
关键字参数最厉害的地方在于可以在函数中给参数提供默认值:
def hello_3(greeting='Hello', name='world'): print('%s, %s!' % (greeting, name))
当参数具有默认值的时候,调用的时候就不用提供参数了!可以不提供、提供一些或提供所有的参数:
>>> hello_3() Hello, world! >>> hello_3('Greetings') Greetings, world! >>> hello_3('Greetings', 'universe') Greetings, universe!
可以看到,位置参数这个方法不错,只是在提供名字的时候同时还要提供问候语。但是如果只想提供name参数,而让greeting使用默认值该怎么办呢?相信此时此刻你已经猜到了:
>>> hello_3(name='Gumby') Hello, Gumby!
很简洁吧?还没完。位置参数和关键字参数是可以联合使用的。把位置参数放置在前面就可以了。如果不这样做,解释器会不知道它们到底谁是谁(也就是它们应该处的位置)。
注意:除非完全清楚程序的功能和参数的意义,否则应该避免混合使用位置参数和关键字参数。一般来说,只有在强制要求的参数个数比可修改的具有默认值的参数个数少的时候,才使用上面提到的参数书写方法。
例如,hello函数可能需要名字作为参数,但是也允许用户自定义名字,问候语和标点:
def hello_4(name, greeting='Hello', punctuation='!'): print('%s, %s%s' % (greeting, name, punctuation))
函数的调用方式很多,下面是其中一些:
>>> hello_4('Mars') Hello, Mars! >>> hello_4('Mars', 'Howdy') Howdy, Mars! >>> hello_4('Mars', 'Howdy', '...') Howdy, Mars... >>> hello_4('Mars', punctuation='.') Hello, Mars. >>> hello_4('Mars', greeting='Top of the morning to ya') Top of the morning to ya, Mars! >>> hello_4() Traceback (most recent call last): File "<pyshell#159>", line 1, in <module> hello_4() TypeError: hello_4() missing 1 required positional argument: 'name'
注意:如果为name也赋默认值,那么最后一个语句就不会产生异常。
6.4.4收集参数
有些时候让用户提供任意数量的参数是很有用的。比如在名字存储程序中,用户每次只能存一个名字。如果能像下面这样存储多个名字就更好了:
>>> store(data, name1, name2, name3)
用户可以给函数提供任意多的参数。实现起来也不难。
试着像下面这样定义函数:
def print_params(*params): print(params)
这里我只指定了一个参数,但是前面加上了个星号。这是什么意思?让我们用一个参数调用函数看看会发生什么:
>>> print_params('Testing') ('Testing',)
可以看到,结果作为元组打印出来,因为里面有个逗号(长度为1的元组有些怪,不是吗)。所以在参数前使用星号就能打印出元组?那么在Params中使用多个参数看看会发生什么:
>>> print_params(1, 2, 3)
(1, 2, 3)
参数前的星号将所有值放置在同一个元组中。可以说是将这些值收集起来,然后使用。不知道能不能于普通参数联合使用。让我们再写个函数:
def print_params_2(title, *params): print(title) print(params)
试试看:
>>> print_params_2('Params:', 1, 2, 3) Params: (1, 2, 3)
没问题!所以星号的意思就是“收集其余的位置参数”。如果不提供任何供收集的元素,params就是个空元组:
>>> print_params_2('Nothing') Nothing ()
的确如此,很有用。那么能不能处理关键字参数(也是参数)呢?
>>> print_params_2('Hmm...', something=42) Traceback (most recent call last): File "<pyshell#175>", line 1, in <module> print_params_2('Hmm...', something=42) TypeError: print_params_2() got an unexpected keyword argument 'something'
看来不行。所以我们需要另外一个能处理关键字参数的“收集”操作。那么语法应该怎么写呢?会不会是“**”?
def print_params_3(**params): print(params)
至少解释器没有报错。调用一下看看:
>>> print_params_3(x=1, y=2, z=3)
{'x': 1, 'y': 2, 'z': 3}
返回的是字典而不是元组。放一起用看看:
def print_params_4(x, y, z=3, *pospar, **keypar): print(x, y, z) print(pospar) print(keypar)
和我们期望的结果别无二致:
>>> print_params_4(1, 2, 3, 5, 6, 7, foo=1, bar=2) 1 2 3 (5, 6, 7) {'foo': 1, 'bar': 2} >>> print_params_4(1,2) 1 2 3 () {}
联合使用这些功能,可以做的事就多了。
现在回到原来的问题上:怎么实现多个名字同时存储。解决方案如下:
def store(data, *full_names): for full_name in full_names: names = full_names.split() if len(names) == 2: names.insert(1, ' ') labels = 'first', 'middle', 'last' for label, name in zip(labels, names): people = lookup(data, label, name) if people: people.append(full_name) else: data[label][name] = [full_name]
使用这个函数就像上一节中的只接受一个名字的函数一样简单:
>>> d = {} >>> init(d) >>> store(d, 'Han Solo')
但是现在可以这样使用:
>>> store(d, 'Luke Skywalker', 'Anakin Skywalker') >>> lookup(d, 'last', 'Skywalker') >>> ['Luke Skywalker', 'Anakin Skywalker']
6.4.5参数收集的逆过程
如何将参数收集为元组和字典已经讨论过了,但是事实上,如果使用*和**的话,也可以执行相反的操作。那么参数收集的逆过程是什么样?假设有如下函数:
def add(x, y): return x + y
注意:operator模块中包含此函数的效率更高的版本。
比如说有个包含由两个要相加的数字组成的元组:
params = (1, 2)
这个过程或多或少有点像我们上一节中介绍的方法的逆过程。不是要收集参数,而是分配它们在“另一端”。使用*运算符就简单了——不过是在调用而不是在定义时使用:
>>> add(*params)
3
对于参数列表来说工作正常,只要扩展的部分是最新的就可以。可以使用同样的技术来处理字典——使用双星号运算符。假设之前定义了hello_3,那么可以这样使用:
>>> params = {'name': 'Sir Robin', 'greeting': 'Well met'}
>>> hello_3(**params)
Well met, Sir Robin!
在定义或者调用函数时使用星号(或者双星号)仅传递元组或字典,所以可能没遇到什么麻烦:
>>> def with_stars(**kwds): print(kwds['name'], 'is', kwds['age'], 'years old') >>> def without_stars(kwds): print(kwds['name'], 'is', kwds['age'], 'years old') >>> args = {'name': 'Mr. Gumby', 'age':42} >>> with_stars(**args) Mr. Gumby is 42 years old >>> without_stars(args) Mr. Gumby is 42 years old
可以看到,在with_stars中,我在定义和调用函数时都使用了星号。而在without_stars中两处都没用,但得到了同样的效果。所以星号只在定义函数(允许使用不定数目的参数)或者调用(“分割”字典或者序列)时才有用。
6.4.6练习使用参数
有了这么多种提供和接受参数的方法,很容易犯晕吧!所以让我们把这些方法放在一起举个例子。首先,我定义了一些函数:
def story(**kwds): return 'Once upon a time, there was a ' \ '%(job)s called %(name)s.' % kwds def power(x, y, *others): if others: print('Received redundant parameters:', others) return pow(x, y) def interval(start, stop=None, step=1): 'Imitates range() for step > 0' if stop is None: # 如果没有为stop提供值…… start, stop = 0, start # 指定参数 result = [] i = start # 计算start索引 while i < stop: # 直到计算到stop的索引 result.append(i) # 将索引添加到result内…… i += step # 用step(>0)增加索引i…… return result
让我们试一下:
>>>print(story(job='king', name='Gumby')) Once upon a time, there was a king called Gumby. >>>print(story(name='Sir Robin', job='brave knight')) Once upon a time, there was a brave knight called Sir Robin. >>>params = {'job': 'language', 'name': 'Python'} >>>print(story(**params)) Once upon a time, there was a language called Python. >>>del params['job'] >>>print(story(job='stroke of genius', **params)) Once upon a time, there was a stroke of genius called Python. >>>power(2,3) 8 >>>power(3,2) 9 >>>params = (5,) * 2 >>>power(*params) 3125 >>>power(3, 3, 'Hello, world') Received redundant parameters:('Hello, world',) 27 >>>interval(10) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>>interval(1,5) [1, 2, 3, 4] >>>interval(3,12,4) [3, 7, 11] >>>power(*interval(3,7)) 81
6.5作用域
到底什么是变量?你可以把它们看作是值的名字。在执行x=1赋值语句后,名称x引用到值1.这就像用字典一样,键引用值,当然,变量和所对应的值用的是个“不可见”的字典。实际上这么说已经很接近真实情况了。内建的vars函数可以返回这个字典:
>>> x = 1 >>> scope = vars() >>> scope['x'] 1 >>> scope['x'] += 1 >>> x 2
警告:一般来说,vars所返回的字典是不能修改的,因为根据官方Python文档的说法,结果是未定义的。换句话说,可能得不到想要的结果。
这类“不可见字典”叫做命名空间或者作用域。那么到底有多少个命名空间?除了全局作用域外,每个函数调用都会创建一个新的作用域:
>>> def foo(): x = 42 >>> x = 1 >>> foo() >>> x 1
这里的foo函数改变(重绑定)了变量x,但是在最后的时候,x并没有变。这是因为当调用foo的时候,新的命名空间就被创建了,它作用于foo内的代码块。赋值语句x=42只在内部作用域(局部命名空间)起作用,所以它并不影响外部(全局)作用域种的x。函数内的变量被称为局部变量(local variable,这是与全局变量相反的概念)。参数的工作原理类似于局部变量,所以用全局变量的名字作为参数名并没有问题。
>>> def output(x): print(x) >>> x = 1 >>> y = 2 >>> output(y) 2
目前为止一切正常。但是如果需要在函数内部访问全局变量怎么办呢?而且只想读取变量的值(也就是不想重绑定变量),一般来说是没有问题的:
>>> def combine(parameter): print(parameter + external) >>> external = 'berry' >>> combine('Shrub') Shrubberry
警告:像这样引用全局变量是很多错误的引发原因。慎重使用全局变量。
屏蔽引发的问题
读取全局变量一般来说并不是问题,但是还是有个会出问题的事情。如果局部变量或者参数的名字和想要访问的全局变量名相同的话,就不能直接访问了。全局变量会被局部变量屏蔽。
如果的确需要的话,可以使用globals函数获取全局变量值,该函数的近亲是vars,它可以返回全局变量的字典(locals返回局部变量的字典)。例如,如果前例中有个叫做parameter的全局变量,那么就不能在combine函数内部访问该变量,因为你有一个与之同名的参数。必要时,能使用globals()['paramter']获取:
>>> def combine(parameter): print(parameter + globals()['parameter']) >>> parameter = 'berry' >>> combine('Shrub') Shrubberry
11

浙公网安备 33010602011771号