函数

函数是可重复使用的实现单一或者相关功能的代码段。函数能够提高应用的模块性和代码的重复利用率。除了内建函数以外,Python 允许用户自定义函数。要定义一个函数需要遵守以下基本的规则。

  1. 函数代码段由def开头,后接函数标识符和圆括号。
  2. 任何传入的参数和自变量都必须放在圆括号中间。
  3. 函数的第一行语句可以选择性的使用三引号字符串,即 docstring,来对函数进行说明。
  4. 函数内容以冒号起始,并且保持缩进一致。
  5. 使用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),这种情况下,星号后的参数必须以命名参数的形式出现。

Info

在 Python 3.8 版本中,新增了一个标记/,在/前方的参数必须是位置参数,不被作为关键字参数,但是/不要放置在*之后。例如以下示例。

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

Info

这个示例中存在若干个 Pythonic 用法,请注意观察。

生成器不仅可以用在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 # 这里也照搬上例中的处理代码

Info

Python 支持在函数中定义函数,这样定义出来的函数要注意其作用域,如果未将其作为返回值返回,则只能在定义它的函数中使用。需要注意@functools.wraps()修饰器的作用,在后面的框架中,会常常用到这个修饰器,它表示修饰器不会对被修饰的函数造成影响,这会在一些情况下避免潜在问题的出现,例如 Flask 的路由处理函数。