def语句定义的函数是所有程序的基石。

7.1 编写可接受任意数量参数的函数

(1)可接受任意数量的位置参数的函数,使用*开头的参数。例如:

>>> def avg(first, *rest):
...     return (first + sum(rest)) / (1 + len(rest))
... 
>>> avg(1,2)
1.5
>>> avg(1,2,3,4)
2.5

这个示例中,rest是一个元祖,它包含了其他所有传递过来的位置参数。代码在之后的计算中会将其视为一个序列来处理。

(2)接受任意数量的关键字参数,使用**开头的参数。例如:

>>> import html
>>> def make_element(name, value, **attrs):
...     keyvals = [' %s="%s"' % item for item in attrs.items()]
...     attr_str = ''.join(keyvals)
...     element = '<{name}{attrs}>{value}</{name}>'.format(name=name, attrs=attr_str, value=html.escape(value))
...     return element
... 
>>> make_element('item','Abcd', size='large', quet=6)
'<item size="large" quet="6">Abcd</item>'
>>> make_element('div', '小试牛刀', style='color:red;')
'<div style="color:red;">小试牛刀</div>'

这里的attrs是一个字典,它包含了所有传递过来的关键字参数。

(3)函数能同时接受任意数量的位置参数和关键字参数,只要联合使用*和**即可。例如:

>>> def anyargs(*args, **kwargs):
...     print(args)     #tuple
...     print(kwargs)   #dict
... 
>>> anyargs(2,3,4,param1="a",param2="b")
(2, 3, 4)
{'param1': 'a', 'param2': 'b'}

这个函数中所有的位置参数都放置在元祖args中,所有的关键字参数都会放置在字典kwargs中。

总结:在函数定义中,以*打头的参数只能作为最后一个位置参数出现,而以**打头的参数只能作为最后一个参数出现。(在*打头的参数后仍然可以有其他参数出现,但是出现在*打头的参数后,只能作为关键字参数使用)例如:

>>> def anyargs(x, *args, y, **kwargs):
...     print(args)
...     print(kwargs)
...     print(x)
...     print(y)
... 
>>> anyargs(2,3,4,param1="a",y="1,2",param2="b")
(3, 4)
{'param1': 'a', 'param2': 'b'}
2
1,2
>>> anyargs(2,3,4,y="1,2",param1="a",param2="b")
(3, 4)
{'param1': 'a', 'param2': 'b'}
2
1,2

7.2 带参数注解的函数,增加代码可读性。函数注解会保存在__annotations__属性中。例如:

>>> def addInt(x:int, y:int) -> int:
...     return x + y
... 
>>> addInt(2,9)
>>> addInt.__annotations__
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}

7.4 从函数中返回多个值

>>> def myfun():  
...     return 1,2,3   #实际上返回的是一个元祖
... 
>>> a,b,c = myfun()   #元祖解包赋值给变量
>>> a
1
>>> b
2
>>> c
3
>>> d = myfun()   #代表了整个元祖
>>> d
(1, 2, 3)

7.5 定义默认参数

默认参数赋值,应该总是不可变的对象,例如:None、True、False、数字、或者字符串。特别要注意,绝对不要编写这样的代码:

def fun(a, b=[]):  # NO!  会陷入各种麻烦中,默认值在函数体外被修改时,调用会产生持续影响
    ...

#None会被判定为False,还有长度为0的字符串,列表,元祖,字典 也会被判断为False
>>> def fun(a, b=None):
...     if not b:   # NO! 可以使用 b is None  代替这句
...             b = []
... 
>>>

7.6 定义匿名或内联函数

>>> b = lambda x,y: x + y     #等价于 def b(x, y)
>>> b(3, 4)
7
>>> def b(x, y):
...     return x + y
... 
>>> b(3, 4)
7

下面lambda表达式中x是一个自由变量,此实例可以验证,lambda在运行时才进行绑定,而不是定义的时候绑定(与def不同)

>>> x = 10
>>> a = lambda y: x + y
>>> a(10)
20
>>> x = 20
>>> a(10)
30

再来看一下易出错的循环迭代,示例如下:

>>> funcs = [lambda x: x+n for n in range(5)] #执行时候n始终为最后一次的赋值
>>> for f in funcs:
...     print(f(0))
... 
4
4
4
4
4
>>> funcs = [lambda x, n=n: x+n for n in range(5)]   #执行时候n每次赋值
>>> for f in funcs:
...     print(f(0))
... 
0
1
2
3
4

7.7 访问定义在闭包中的变量

通过函数来扩展闭包,使得闭包内层定义的变量可以背访问和修改。一般来说,在闭包内层定义的变量对于外界来说是完全隔离的。但是可以通过编写来存取函数,并将它们作为函数属性附加到闭包上来提供对内层变量的访问。例如:

>>> def sample():
...     n = 0
...     def fun():
...             print('n = ', n)
...     def get_n():
...             return n
...     def set_n(value):
...             nonlocal n     #nonlocal 声明使得编写函数来修改内层变量成为可能
...             n = value
...     fun.get_n = get_n     #函数属性能够将存取函数以直接的方式附加到闭包函数上
...     fun.set_n = set_n
...     return fun
... 
>>> f = sample()
>>> f()
n =  0
>>> f.set_n(10)
>>> f()
n =  10
>>> f.get_n()
10

闭包模拟成类实例,速度比class定义的实例,测试出大约快8%,测试中的大部分时间都花在对实例变量的直接访问上,闭包要更快一些,这是因为不用涉及额外的self变量。

>>> import sys
>>> class aaa():
...     def __init__(self, locals = None):
...             if locals is None:
...                     locals = sys._getframe(1).f_locals
...             self.__dict__.update((key,value) for key, value in locals.items() if callable(value))
...     def __len__(self):
...             return self.__dict__['__len__']()
... 
>>> def Stack():   #闭包模拟成类实例
...     items = []
...     def push(item):
...             items.append(item)
...     def pop():
...             items.pop()
...     def __len__():
...             return len(items)
...     return aaa()
... 
>>> s = Stack()
<__main__.aaa object at 0x7f89f34773c8>
>>> s.push(10)
>>> s.push(20)
>>> s.push('hello')
>>> len(s)
3
>>> s.pop()  #pop出'hello'
>>> len(s)
2
>>> s.pop()  #pop出20
>>> s.pop()  #pop出10
>>> len(s)
0
>>> class Stack2:      #实例
...     def __init__(self):
...             self.items = []
...     def push(self, item):
...             self.items.append(item)
...     def pop(self):
...             return self.items.pop()
...     def __len__(self):
...             return len(self, items)
... 
#性能对比
>>> from timeit import timeit
>>> s = Stack()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')   #闭包耗用时间
0.3924146047793329
>>> s = Stack2()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')   #实例耗用时间
0.4104466247372329
>>>