函数
函数是可重复使用的实现单一或者相关功能的代码段。函数能够提高应用的模块性和代码的重复利用率。除了内建函数以外,Python 允许用户自定义函数。要定义一个函数需要遵守以下基本的规则。
- 函数代码段由
def
开头,后接函数标识符和圆括号。 - 任何传入的参数和自变量都必须放在圆括号中间。
- 函数的第一行语句可以选择性的使用三引号字符串,即 docstring,来对函数进行说明。
- 函数内容以冒号起始,并且保持缩进一致。
- 使用
return
结束函数并返回值。不带表达式的return
相当于返回None
。
所以定义函数的一般格式为:
def 函数名(函数参数表):
函数体
定义好的函数通过函数标识符进行调用,调用格式为函数标识符(参数列表)
。
def area(width, height):
return width * height
print(area(15, 10))
参数传递
Python 中的数据类型分为可变类型和不可变类型两种,其中字符串、元组、数字是不可变对象,列表、集合、字典等则是可变对象。在进行参数传递时,不同类型的对象在传递时也不相同。
- 不可变类型:类似于 C++的值传递,传递的只是实参的复制,对形参的修改不影响实参本身。
- 可变类型:类似于 C++的引用传递,传递的是实参的内存地址,对形参的修改将使实参受到影响。
由于 Python 中一切都是对象,所以不能使用值传递或者引用传递的概念,而是应该使用传递可变对象或者传递不可变对象。简而言之,被传递的对象是可变的,那么在函数中对形参的修改,也将直接反映到被传递的对象上,反之亦然。
参数定义
Python 中函数的参数有四种类型:
- 必需参数,
- 关键字参数,
- 默认参数,
- 不定长参数。
必需参数是必需以定义时的顺序传入函数的参数,调用时参数的数量和顺序必须和声明时完全一致。大部分的函数都是采用此种方式声明和调用。这也是函数定义的默认状态。
关键字参数是使用关键字来确定传入的参数值,这时解释器可以根据参数名来确定和匹配参数值,也就允许函数调用时,参数的顺序和声明时的顺序不一致。
def area(width, height):
return width * height
# 使用必需参数形式调用
area(10, 5)
# 使用关键字参数形式调用
area(height=5, width=10)
参数可以在函数声明时设定默认值,这样在函数调用时,如果没有传递这个参数,解释器就会使用设定的默认值填充参数。默认参数在定义时必须放置在参数列表最后的位置。
def area(width, height=5):
return width * height
# 使用默认参数
area(10)
# 不使用默认参数
area(10, 8)
不定长参数可以允许函数能够处理比声明时更多的参数,与之前的参数不同,不定长参数不会命名,也没有默认值。不定长参数的使用格式如下:
def 函数标识符([常规形参表], *不定长形参名):
"docstring"
函数体
return 返回表达式
不定长参数使用*
在其前方进行标记,所有未命名的变量参数都会形成一个元组传入不定长形参中。用于标记不定长参数的*
可以单独出现,例如:func(a, b, *, c)
,这种情况下,星号后的参数必须以命名参数的形式出现。
def f(a,b, /, **kwargs):
print(a, b, kwargs)
# 以下调用将输出:10 20 {'a': 1, 'b': 2}
f(10, 20, a=1, b=2)
如果使用**
在不定长参数前进行标记,则会将所有未显式定义的命名参数转化为字典。例如:
def foo(country, **kwargs):
print(kwargs)
foo('China', province='Hebei', city='Shijiazhuang')
return
语句
return 表达式
语句用于退出函数,选择性的向调用方返回一个表达式。不带参数的return
语句会返回None
。
Python 允许在一个函数中返回多个元素,同时被返回的元素将组合成一个元组,可以使用解包语句来获取其内容。例如:
def twins()
return 0, 1
zero, one = twins()
# 如果只需要从其中获得部分元素,则可以使用_来屏蔽掉不需要的元素,例如
zero, _ = twins()
此外,一个函数中可以有多个return
语句,但只有最后一个return
语句的结果被返回,这尤其是在使用try...finally...
语句时需要注意。
生成器
不使用return
返回值,而使用yield
返回值的函数称为生成器。与return
不同的是,yield
用于不断的返回值,也就是说是用于迭代操作。生成器实际上就是一个迭代器。
生成器在运行过程中,遇到yield
时就会暂停运行并保存当前的运行信息,并返回yield
的值;当执行next()
时,生成器会从保存的位置继续运行,并抛出下一个值。所以调用生成器返回的是一个迭代器对象。如果生成器在运行过程中遇到了return
则会直接抛出StopIteration
终止迭代。
如果函数需要返回一个容量很大的列表,并且其中的内容是动态计算得到的,那么使用生成器会更加经济、快速。
以下示例构建了一个用于生成斐波那契数列的生成器。读者可以自行在交互式解释器中实验这个函数的运行。
def fibonancci(n):
a, b, counter = 0, 1, 0
while True:
if (counter > n):
return
yield a
a, b = b, a + b
counter += 1
生成器不仅可以用在for
循环中,还可以用于函数的参数。如果这个函数接受一个iterable
(可迭代)参数,就可以将生成器传递给这个函数。
除了可以使用next()
来获取生成器中的下一个值以外,还可以使用send(arg)
来获取生成器中的下一个值。这两个函数的区别是send()
函数会将参数传递给yield
表达式作为yield
表达式的值。这里需要仔细理解生成器的整个操作过程,因为这个操作过程是 Python 进行异步处理的核心概念。以下给出一个生成器接受send()
传递的值的例子。
def echo():
m = 0
while m < 100:
m = yield m
f = echo()
value = f.next() # 或者可以调用 f.send(None)
print(value)
value = f.send(98)
print(value)
yield
关键字会返回其右侧的值,并从send()
函数中取得新值作为其表达式值,在下一次迭代时,将会从yield
的下一行开始执行。函数next()
和send()
的返回值就是本次yield
关键字右侧的值。
生成器在初始化后的第一次调用必须使用next()
或者send(None)
,在第一次调用生成器时,这两条语句是等同的。之所以给send()
传递参数None
,是因为在调用send()
时,还没有出现第一个yield
,所以send()
没有对应的yield
表达式可以修改,必须传递None
值以保证生成器的正常运行。
在日常使用时,还会发现yield from
关键字形式。yield from
关键字允许在一个生成器中调用另一个生成器,并抛出另一个生成器生成的值。yield from
常用来将嵌套循环或序列扁平化。以下给出一个扁平化的例子。
# 没有进行扁平化的生成器
def chain(*iterables):
for it in iterables:
for value in it:
yield value
# 扁平化后的生成器
def chain(*iterables):
for it in iterables:
yield from it
以下示例使用yield from
实现了一个类似于递归的扁平化循环嵌套型序列的方法,请仔细体会。
from collections import Iterable
def flatten(items):
for it in items:
if isinstance(it, Iterable):
yield from flatten(it)
else:
yield it
匿名函数
使用lambda
关键字可以创建一个只包含一条语句的匿名函数。其定义格式为:lambda 参数列表:表达式
。匿名函数默认返回表达式的结果。
area = lambda width, height: width * height
print(area(10, 5))
匿名函数在其他语言中也称为闭包、Lambda 表达式,其功能主旨都是基本相同的。这里要注意的是,匿名函数中变量作用域与普通函数是有不同的,所以不能将其作为普通函数的完全替代品来使用。
变量作用域
Python 中变量的作用域也是由变量的定义位置决定的,并且范围由小到大有以下四种作用域。
- 局部作用域(Local);
- 闭包外作用域(Enclosing);
- 全局作用域(Global);
- 内建作用域(Built-in)。
在 Python 中只有模块、类和函数才会引入新的作用域,除此之外其他的代码块是不会引入新的作用域的。
定义在函数内部的变量拥有一个局部作用域,局部变量只能在其被声明的函数内部访问,如果在函数内部要修改外部作用域的变量时,需要使用global
关键字来修饰声明要引用的变量。例如:
num = 1
def foo():
global num
num = 2
print(num)
foo()
如果需要修改嵌套作用域,而外层作用域又非全局作用域,则需要使用nonlocal
关键字。例如:
def outer():
num = 10
def inner():
nonlocal num
num = 20
print(num)
inner()
outer()
修饰器
修饰器可以在不修改目标函数代码的情况下,在目标函数执行前后增加一些额外功能,例如函数计时、鉴权等。修饰器是一个函数,它需要返回一个新的函数,修饰器在模块加载时就会执行生成一个个被修饰的新函数。
以下示例定义了一个用于计时的修饰器。
import time
def timing(fn):
def new_fn(*args):
start = time.time() # 在被修饰的函数执行前进行操作
result = fn(*args) # 调用被修饰的函数,被修饰的函数调用结果收集起来
end = time.time() # 在被修饰的函数执行后进行操作
duration = end - start
print("%s secnods cunsomed in executing %s" % (duration, fn.__name__))
return result # 返回被修饰的函数执行结果
return new_fn # 返回局部定义的函数
# 使用@来调用修饰器函数对目标函数进行修饰
@timing
def acc(start, end):
a = 0
for i in xrange(start, end):
a += i
return a
acc(10, 1000000)
修饰器函数使用@
进行调用来标记目标函数,并且不允许与函数定义书写在同一行。修饰器函数可以接收参数,定义时只需要再在外面套一层函数即可,带参数的修饰器在调用时直接按照函数调用格式书写参数即可。
以下根据上例修改了一个带参数的修饰器,可以设定是否保留函数元信息。
import time
import functools
def timing(keep_meta=False):
def real_dec(fn):
if keep_meta:
@functools.wraps(fn)
def new_fn(*args):
pass # 这里照搬上例中的计时和调用代码即可
else:
def new_fn(*args):
pass # 这里照搬上例中的计时和调用代码即可
return new_fn
return real_dec
@timing(keep_meta=True)
def acc(start, end):
pass # 这里也照搬上例中的处理代码