抽 象
6.4 参数魔法
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没有影响。说到底,
这是一个完全不同的变量。传递并修改参数的效果类似于下面这样:
>>> name = 'Mrs. Entity'
>>> n = name # 与传递参数的效果几乎相同
>>> n = 'Mr. Gumby' # 这是在函数内进行的
>>> name 'Mrs. Entity'
这里的结果显而易见:变量n变了,但变量name没变。同样,在函数内部重新关联参数(即 给它赋值)时,函数外部的变量不受影响。
注意 参数存储在局部作用域内。作用域将在本章稍后讨论。
字符串(以及数和元组)是不可变的(immutable),这意味着你不能修改它们(即只能替换 为新值)。因此这些类型作为参数没什么可说的。但如果参数为可变的数据结构(如列表)呢?
>>> def change(n):
... n[0] = 'Mr. Gumby'
1
>>> names = ['Mrs. Entity', 'Mrs. Thing']
>>> change(names)
>>> names
['Mr. Gumby', 'Mrs. Thing']
在这个示例中,也在函数内修改了参数,但这个示例与前一个示例之间存在一个重要的不同。
在前一个示例中,只是给局部变量赋了新值,而在这个示例中,修改了变量关联到的列表。这很 奇怪吧?其实不那么奇怪。下面再这样做一次,但这次不使用函数调用。
>>> names = ['Mrs. Entity', 'Mrs. Thing']
>>> n = names # 再次假装传递名字作为参数
>>> n[0] = 'Mr. Gumby' # 修改列表
>>> names
['Mr. Gumby', 'Mrs. Thing']
这样的情况你早就见过。将同一个列表赋给两个变量时,这两个变量将同时指向这个列表。
就这么简单。要避免这样的结果,必须创建列表的副本。对序列执行切片操作时,返回的切片都 是副本。因此,如果你创建覆盖整个列表的切片,得到的将是列表的副本。
>>> names = ['Mrs. Entity', 'Mrs. Thing']
>>> n = names[:]
['Mr. Gumby', 'Mrs. Thing']
>>> names
['Mrs. Entity', 'Mrs. Thing']
下面来尝试结合使用这种技巧和函数change。
>>> change(names[:])
>>> names
['Mrs. Entity', 'Mrs. Thing']
能使用一个类似于下面的数据结构:
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
{'middle': {}, 'last': {}, 'first': {}}
如你所见,这个函数承担了初始化职责,让代码的可读性高了很多。
1
def lookup(data, label, name):
return data[label].get(name)
函数lookup接受参数label(如'middle')和name(如'Lie'),并返回一个由全名组成的列表。
换而言之,如果已经存储了作者的姓名,就可以像下面这样做:
>>> lookup(storage, 'middle', 'Lie') ['Magnus Lie Hetland']
请注意,返回的是存储在数据结构中的列表。因此如果对返回的列表进行修改,将影响数据 结构。(未找到任何人时除外,因为在这种情况下返回的是None。)
下面来编写将人员存储到数据结构中的函数。(如果不能马上看懂这个函数,也不用担心。)
def store(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执行如下步骤。
>>> init(MyNames)
>>> store(MyNames, 'Magnus Lie Hetland')
>>> lookup(MyNames, 'middle', 'Lie') ['Magnus Lie Hetland']
看起来能正确地运行。下面再来尝试几次。
>>> store(MyNames, 'Robin Hood')
>>> store(MyNames, 'Robin Locksley')
>>> lookup(MyNames, 'first', 'Robin') ['Robin Hood', 'Robin Locksley']
>>> store(MyNames, 'Mr. Gumby')
>>> lookup(MyNames, 'middle', '')
['Robin Hood', 'Robin 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]
但更清晰的解决方案是返回修改后的值。