使用def语句定义函数是所有程序的基础。本章的目标是讲解一些更加高级和不常见的函数定义与使用模式。设计到的内容包括默认参数、任意数量参数、强制关键字参数、注解和闭包。另外,一些高级的控制流程和利用回调函数传递数据的技术在这里也会讲解到。
接受任意数量参数的函数
为了能让一个函数接受任意数量的位置参数,可以使用一个*参数。例如:
def avg(first, *rest):
return (first + sum(rest))/(1 + len(rest))
avg(1, 2)
avg(1, 2, 3, 4)
在上面的例子中,rest是所有其他位置参数组成的元组。
为了接受任意数量的关键字参数,使用一个以**开头的参数。比如:
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 elements
make_element("item", "Albatross", size="large", quantity=6)
make_element("p", "<spam>")
在这里,attrs是一个包含所有被传进来的关键字参数的字典。
如果你还希望某个函数能同时接受任意数量的位置参数和关键字参数,可以同时使用*和**,比如:
def angargs(*args, **kwargs):
print(args)
print(kwagrs)
使用这个函数时,所有位置参数会被放到args元组中去,所有关键字参数会被放到字典kwargs中去。
注意:
一个参数只能出现在函数定义中最后一个位置参数后面,而**参数只能出现在最后一个参数。还有一点需要注意的是,在参数后面仍可以定义其他参数。
def a(x, *args, y):
pass
def b(x, *args, y, **kwargs):
pass
上面的例子是强制关键字参数。后面的章节中将会介绍,现在需要记住的就是其中y不算数位置参数。
只接受关键字参数的函数
强制关键字参数放到某个参数,或者单个后面就能达到这种效果。
def recv(maxsize, *, block):
pass
# 使用下面这种调用方式的话,会报一个TypeError的错误
recv(1024, True)
# 正确的使用方式应该像这样
recv(1024, block=True)
给函数增加元信息
例如:
def add(x:int, y:int) -> int:
return x + y
python解释器不会对上面的注解添加任何语义。它们不会被类型检查,运行时跟没有加注解前效果没有任何差别。
注意:
函数注解只存储在函数的__annotations__
属性中
定义有默认参数的函数
定义一个有可选参数的函数是非常简单的,直接在函数定义中给参数指定一个默认值,并放到参数列表最后就可以了。
def spam(a, b=42):
print(a, b)
spam(1)
# 输出 1, 42
spam(1, 2)
# 输出 1, 2
如果默认参数是一个可修改的容器比如列表,集合或字典,可以使用None作为默认值。
def spam(a, b=None):
if b is None:
b = []
注意:我们在这里探讨一些东西
- 默认参数的值仅仅在函数定义的时候赋值一次。例如
x = 42
def spam(a, b=x):
print(a, b)
spam(1)
# 1, 42
x = 23
spam(1)
# 1, 42
我们改变x的值时,对默认参数值并没有影响,这是因为在函数定义的时候它就已经确认了它的默认值了。
2. 默认参数的值应该是不可变对象如:None,True,False,数字,或字符串等。千万不要像下面这样写:
def spam(a, b=[])
def spam(a, b=[]):
print(b)
return b
x = spam(1) # 此时x为[]
x.append(99)
x.appned("Yow!")
print(spam(1))
# 输出[99, "Yow!"]
上面的结果不是我们想要的。为了避免这种情况的发送,最好是将默认值设置为None,然后在函数里面检查它。但是在测试None的时候使用is操作符是非常重要的,也是这种方式的关键点。有时候搭建可能会犯这样的错误。
def spam(a, b=None):
if not b:
b = []
这样写的问题在于尽管None值确实被当成False,但是还有其他的对象(比如:长度为0的字符串,列表,元祖,字典等)都会被当成False。因此,上面的代码会无将一些其他的输入也当成是没有输入。
3. 最后一个问题比较微妙,那就是一个函数需要测试某个可选参数是否被使用者传递进来。这时候需要小心的是你不能用某个默认值,比如None,0或者False值来擦拭用户提供的值(因为这些值都是合法的值,是可能被用户传递进来的)
为了解决这个问题,你可以创建一个独一无二的私有对象实例,例如:
_no_value = object()
def spam(a, b=_no_value):
if b is _no_value:
print("No b value supplied")
定义匿名函数
add = lambda x, y: x + y
add(2, 3)
# 5
add("hello", "world")
# helloworld
其中x, y相当于入参,返回值为x+y。
尽管lambda表达式允许你定义简单函数,但是它的时候是有限制的。你只能指定单个表达式,它的值就是最后的返回值。也就是不能包含其他的语言特性了,包括多个语句,条件表达式,迭代以及异常处理等。
匿名函数捕获变量值
看看以下代码的效果
x = 10
a = lamdba y: x + y
x = 20
b = lambda y: x + y
print(a(10))
print(b(10))
如上a(10)和b(10)返回的都是30。
这是因为lambda表达式中的x是一个自由变量,在运行时绑定值,而不是定义时就绑定,这根函数的默认值参数定义时不同的。因此,在调用这个lambda表达值的时候,x是执行是的值。
如果你想让某个匿名函数在定义时就捕获到值,可以将那个参数值定义成默认参数即可。
x = 10
a = lambda y, x=x: x + y
x = 20
b = lamdba y, x=x: x + y
a(10)
# 20
b(10)
# 30
下面我们列出一些lambda经常使用错误的一些例子:
funcs = [lambda x: x + n for n in range(5)]
for f in funcs:
print(0)
4
4
4
4
我们可以用下面这种方式修改
funcs = [lamdba x, n=n: x + n for n in rang(5)]
for f in funcs:
print(f(0))
0
1
2
3
4
通过使用默认值参数的形式,lambda函数在定义的时候就能绑定到值。
减少可调用对象的参数个数
如果需要减少某个函数的参数个数,你可以使用functools.partial()。partial()函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。为了演示清除,假设假设你有下面这样的函数:
def spam(a, b, c, d):
print(a, b, c, d)
现在我们用partial()函数啦固定某些参数值:
from functools import partial
s1 = partial(spam, 1)
s1(2, 3, 4)
# 1, 2, 3, 4
s1(4, 5, 6)
# 1, 4, 5, 6
s2 = partila(spam, d=42)
s2(1, 2, 3)
# 1, 2, 3, 52
s3 = partial(spam, 1, 2, d=42)
s3(3)
# 1, 2, 3, 42
s3(4)
# 1, 2, 4, 42
s3(5)
# 1, 2, 5 42
可以看出partial()固定某些参数并返回一个新的callable对象,这个新的callable接受未赋值的参数,然后跟之前已经赋值过的参数合并起来,最后将所有参数传递给原始函数。
将单个方法的类转换为函数
大多数情况下,可以使用闭包来将单个方法的类转换成函数。举个例子:
from urllib.request import urlopen
class UrlTemplate:
def __init__(self, template):
self.template = template
def open(self, **kwargs):
return urlopen(self.template.format_map(kwargs))
yahoo = UrlTemplate("http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}")
for line in yahoo.open(names="IBM,AAPL,FB", fields="sliciv"):
print(line.decode("utf-8"))
上面的类可以被替换成下面的函数
def url_template(template):
def opener(**kwargs):
return urlopen(template.format_map(kwargs))
return opener
yahoo = url_template("http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}")
for line in yahoo(name="IBM,AAPL,FB", fields="sliciv"):
print(line.decode("utf-8"))
一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义是的环境。因此,在我们的解决方案中,opener()函数记住了template参数的值,并在接下来的调用中使用它。
访问闭包中定义的变量
通常来讲,闭包的内部变量对于外界来讲是完全隐蔽的。但是,你可以通过编写访问函数并将其作为函数属性绑定到闭包上来实现这个目的。
def sample():
n = 0
def func():
print("n=", n)
def get_n():
return n
def set_n(value):
nonlocal n
n = value
func.get_n = get_n
func.set_n = set_n
return func
f = sample()
f()
# n = 0
f.set_n(10)
# n = 10
f.get_n()
# 10