函数中的参数问题

在函数内部给参数赋值对外部没有任何影响的情况

>>> 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'
...
>>> 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[:]

现在nnames包含两个相等不同的列表。

>>> n is names
False
>>> n == names
True

现在如果(像在函数change中那样)修改n,将不会影响names

>>> n[0] = 'Mr. Gumby'
>>> n
['Mr. Gumby', 'Mrs. Thing']
>>> names
['Mrs. Entity', 'Mrs. Thing']

下面来尝试结合使用这种技巧和函数change

>>> change(names[:])
>>> names
['Mrs. Entity',  'Mrs. Thing']

注意到参数n包含的是副本,因此原始列表是安全的。

为何要修改参数

在提高程序的抽象程度方面,使用函数来修改数据结构(如列表或字典)是一种不错的方式。假设你要编写一个程序,让它存储姓名,并让用户能够根据名字、中间名或姓找人。为此,你可能使用一个类似于下面的数据结构:

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']

如你所见,将人员添加到这个数据结构中有点繁琐,在多个人的名字、中间名或姓相同时尤其如此,因为在这种情况下需要对存储在名字、中间名或姓下的列表进行扩展。下面来添加我的妹妹,并假设我们不知道数据库中存储了什么内容。

先了解一下方法setdefault(key,default=None)

功能:在已创建的字典中添加键和值(key - 键,default = 值),其中值可以为默认None或者自己赋予(数值型,字符串,字典,列表等)。返回值为dict().setdefault("name","Bob")-------'Bob'

>>> my_sister = 'Anne Lie Hetland'
>>> storage['first'].setdefault('Anne', []).append(my_sister) #storage['first'].setdefault('Anne', [])值为空列表[]
>>> 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': {}}

 如你所见,这个函数承担了初始化职责,让代码的可读性高了很多。

下面先来编写获取人员姓名的函数,再接着编写存储人员姓名的函数。

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执行如下步骤。

(1) 将参数datafull_name提供给这个函数。这些参数被设置为从外部获得的值。

(2) 通过拆分full_name创建一个名为names的列表。

(3) 如果names的长度为2(只有名字和姓),就将中间名设置为空字符串。

(4) 将'first''middle''last'存储在元组labels中(也可使用列表,这里使用元组只是为了省略方括号)。

(5) 使用函数zip将标签和对应的名字合并,以便对每个标签-名字对执行如下操作:

  • 获取属于该标签和名字的列表;
  • full_name附加到该列表末尾或插入一个新列表。

下面来尝试运行该程序:

>>> MyNames = {}
>>> init(MyNames)
>>> store(MyNames, 'Magnus Lie Hetland')
>>> lookup(MyNames, 'middle', 'Lie')
['Magnus Lie Hetland']

注意 这种程序非常适合使用面向对象编程。

位置参数和关键字参数

位置参数

def hello_1(greeting, name):
    print('{}, {}!'.format(greeting, name))

def hello_2(name, greeting):
    print('{}, {}!'.format(name, greeting))

关键字参数

>>> hello_2(greeting='Hello', name='world')
world, Hello!

关键字参数最大的优点在于,可以指定默认值。

你可结合使用位置参数和关键字参数,但必须先指定所有的位置参数,否则解释器将不知道它们是哪个参数(即不知道参数对应的位置)。

注意 通常不应结合使用位置参数和关键字参数,除非你知道这样做的后果。一般而言,除非必不可少的参数很少,而带默认值的可选参数很多,否则不应结合使用关键字参数和位置参数。

有时候,允许用户提供任意数量的参数很有用

请尝试使用下面这样的函数定义:

def print_params(*params):
    print(params)

这里好像只指定了一个参数,但它前面有一个星号。这是什么意思呢?尝试使用一个参数来调用这个函数,看看结果如何。

>>> print_params('Testing')
('Testing',)

注意到打印的是一个元组,因为里面有一个逗号。这么说,前面有星号的参数将被放在元组中?复数params应该提供了线索。

>>> print_params(1, 2, 3)
(1, 2, 3)

参数前面的星号将提供的所有值都放在一个元组中,也就是将这些值收集起来。

星号不会收集关键字参数。

>>> print_params_2('Hmm...', something=42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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)
{'z': 3, 'x': 1, 'y': 2}

如你所见,这样得到的是一个字典而不是元组。可结合使用这些技术。

前面介绍了如何将参数收集到元组和字典中,但用同样的两个运算符(***)也可执行相反的操作。与收集参数相反的操作是什么呢?假设有如下函数:

def add(x, y):
    return x + y

注意 模块operator提供了这个函数的高效版本。

同时假设还有一个元组,其中包含两个你要相加的数。

params = (1, 2)

这与前面执行的操作差不多是相反的:不是收集参数,而是分配参数。这是通过在调用函数(而不是定义函数)时使用运算符*实现的。

>>> add(*params)
3

这种做法也可用于参数列表的一部分,条件是这部分位于参数列表末尾。通过使用运算符**,可将字典中的值分配给关键字参数。

如果在定义和调用函数时都使用***,将只传递元组或字典。因此还不如不使用它们,还可省却些麻烦。

>>> 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,我在定义和调用它时都没有使用,但这两种做法的效果相同。因此,只有在定义函数(允许可变数量的参数)调用函数时(拆分字典或序列)使用,星号才能发挥作用。

提示 使用这些拆分运算符来传递参数很有用,因为这样无需操心参数个数之类的问题,如下所示:

def foo(x, y, z, m=0, n=0):
    print(x, y, z, m, n)
def call_foo(*args, **kwds):
    print("Calling foo!")
    foo(*args, **kwds)

这在调用超类的构造函数时特别有用。

posted on 2019-07-25 09:42  iBoundary  阅读(495)  评论(0编辑  收藏  举报

导航