《Python从小白到大牛》第10章 函数式编程

0.048字数 5769阅读 113

《Python从小白到大牛》已经上市!

程序中反复执行的代码可以封装到一个代码块中,这个代码块模仿了数学中的函数,具有函数名、参数和返回值,这就是程序中的函数。

Python中的函数很灵活,它可以在模块中,但类之外定义,即函数,作用域是当前模块;也可以在别的函数中定义,即嵌套函数;还可以在类中定义,即方法。

定义函数

在前面的学习过程中也用到了一些函数,如果len()、min()和max(),这些函数都由Python官方提供的,称为内置函数(Built-in
Functions, 缩写BIF)。

注意
Python作为解释性语言函数必须先定义后调用,也就是定义函数必须在调用函数之前,否则会有错误发生。

本节介绍自定义函数,自定义函数的语法格式如下:

def 函数名(参数列表) : 
    函数体
    return 返回值

在Python中定义函数时,关键字是def,函数名需要符合标识符命名规范;多个参数列表之间可以用逗号(,)分隔,当然函数也可以没有参数。如果函数有返回数据,就需要在函数体最后使用return语句将数据返回;如果没有返回数据,则函数体中可以使用return
None或省略return语句。

函数定义示例代码如下:

# coding=utf-8
# 代码文件:chapter10/ch10.1.py


def rectangle_area(width, height): ①
    area = width * height
    return area ②


r_area = rectangle_area(320.0, 480.0) ③

print("320x480的长方形的面积:{0:.2f}".format(r_area))

上述代码第①行是定义计算长方形面积的函数rectangle_area,它有两个参数,分别是长方形的宽和高,width和height是参数名。代码第②行代码是通过return返回函数计算结果。代码第③行是调用rectangle_area函数。

函数参数

Python中的函数参数很灵活,具体体现在传递参数有多种形式上。本节介绍几种不同形式参数和调用方式。

使用关键字参数调用函数

为了提高函数调用的可读性,在函数调用时可以使用关键字参数调用。采用关键字参数调用函数,在函数定义时不需要做额外的工作。

示例代码如下:

# coding=utf-8
# 代码文件:chapter10/ch10.2.1.py


def print_area(width, height):
    area = width * height
    print("{0} x {1} 长方形的面积:{2}".format(width, height, area))


print_area(320.0, 480.0)  # 没有采用关键字参数函数调用   ①
print_area(width=320.0, height=480.0)  # 采用关键字参数函数调用②
print_area(320.0, height=480.0)  # 采用关键字参数函数调用 ③
# print_area(width=320.0, height)  # 发生错误 ④
print_area(height=480.0, width=320.0)  # 采用关键字参数函数调用 ⑤

print_area函数有两个参数,在调用时没有采用关键字参数函数调用,见代码第①行,也可以使用关键字参数调用函数,见代码第②行、第③行和第⑤行,其中width和height是参数名。从上述代码比较可见采用关键字参数调用函数,调用者能够清晰地看出传递参数的含义,关键字参数对于有多参数函数调用非常有用。另外,采用关键字参数函数调用时,参数顺序可以与函数定义时参数顺序不同。

注意
在调用函数时,一旦其中一个参数采用了关键字参数形式传递,那么其后的所有参数都必须采用关键字参数形式传递。代码第④行的函数调用中第一个参数width采用了关键字参数形式,而它后面的参数没有采用关键字参数形式,因此会有错误发生。

参数默认值

在定义函数的时候可以为参数设置一个默认值,当调用函数的时候可以忽略该参数。来看下面的一个示例:

# coding=utf-8
# 代码文件:chapter9/ch10.2.2.py

def make_coffee(name="卡布奇诺"):
    return "制作一杯{0}咖啡。".format(name)

上述代码定义了makeCoffee函数,可以帮助我做一杯香浓的咖啡。由于我喜欢喝卡布奇诺,就把它设置为默认值。在参数列表中,默认值可以跟在参数类型的后面,通过等号提供给参数。

在调用的时候,如果调用者没有传递参数,则使用默认值。调用代码如下:

coffee1 = make_coffee("拿铁")  ①
coffee2 = make_coffee() ②

print(coffee1)  # 制作一杯拿铁咖啡。
print(coffee2)  # 制作一杯卡布奇诺咖啡。

其中第①行代码是传递"拿铁"参数,没有使用默认值。第②行代码没有传递参数,因此使用默认值。

提示
在其他语言中make_coffee函数可以采用重载实现多个版本。Python不支持函数重载,而是使用参数默认值的方式提供类似函数重载的功能。因为参数默认值只需要定义一个函数就可以了,而重载则需要定义多个函数,这会增加代码量。

可变参数

Python中函数的参数个数可以变化,它可以接受不确定数量的参数,这种参数称为可变参数。Python中可变参数有两种,在参数前加*或**形式,*可变参数在函数中被组装成为一个元组,**可变参数在函数中被组装成为一个字典。

  1. *可变参数

下面看一个示例:

def sum(*numbers, multiple=1):
    total = 0.0
    for number in numbers:
        total += number
    return total * multiple

上述代码定义了一个sum()函数,用来计算传递给它的所有参数之和。*numbers是可变参数。在函数体中参数numbers被组装成为一个元组,可以使用for循环遍历numbers元组,计算他们的总和,然后返回给调用者。

下面是三次调用sum()函数的代码:

print(sum(100.0, 20.0, 30.0))  # 输出150.0
print(sum(30.0, 80.0))  # 输出110.0
print(sum(30.0, 80.0, multiple=2))  # 输出220.0 ①

double_tuple = (50.0, 60.0, 0.0)  # 元组或列表 ②
print(sum(30.0, 80.0, *double_tuple))  # 输出220.0 ③

可以看到,每次所传递参数的个数是不同的,前两次调用时都省略了multiple参数,第三次调用时传递了multiple参数,此时multiple应该使用关键字参数传递,否则有错误发生。

如果已经有一个元组变量(见代码第②行),能否传递给可变参数呢?这需要使用对元组进行拆包,见代码第③行在元组double_tuple前面加上单星号(*),单星号在这里表示将double_tuple拆包为50.0,
60.0, 0.0形式。另外,double_tuple也可以是列表对象。

注意
*可变参数不是最后一个参数时,后面的参数需要采用关键字参数形式传递。代码第①行30.0,
80.0是可变参数,后面multiple参数需要关键字参数形式传递。

  1. **可变参数

下面看一个示例:

def show_info(sep=':', **info):
    print('-----info------')
    for key, value in info.items():
        print('{0} {2} {1}'.format(key, value, sep))

上述代码定义了一个show_info()函数,用来输出一些信息,其中参数sep信息分隔符号,默认值是冒号(:)。**info是可变参数,在函数体中参数info被组装成为一个字典。

注意 **可变参数必须在正规参数之后,如果本例函数定义改为show_info(**info,sep=':')形式,会发生错误。

下面是三次调用show_info()函数的代码:

show_info('->', name='Tony', age=18, sex=True)  ①
show_info(sutdent_name='Tony', sutdent_no='1000', sep='-') ②

stu_dict = {'name': 'Tony', 'age': 18}  # 创建字典对象 
show_info(**stu_dict, sex=True, sep='=')  # 传递字典stu_dict ③

上述代码第①行是调用函数show_info(),第一个参数'->'是传递给sep,其后的参数name='Tony',
age=18,
sex=True是传递给info,这种参数形式事实上就是关键字参数,注意键不要用引号包裹起来。

代码第②行是调用函数show_info(),sep也采用关键字参数传递,这种方式sep参数可以放置在参数列表的任何位置,其中的关键字参数被收集到info字典中。

代码第③行是调用函数show_info(),其中字典对象stu_dict,传递时stu_dict前面加上双星号(**),双星号在这里表示将stu_dict拆包为key=value对的形式。

函数返回值

Python函数的返回值也是比较灵活的,主要有三种形式:无返回值、单一返回值和多返回值。前面使用的函数基本都单一返回值,本节重点介绍无返回值和多返回值两种形式。

无返回值函数

有的函数只是为了处理某个过程,此时可以将函数设计为无返回值的。所谓无返回值,事实上是返回None,None表示没有实际意义的数据。

无返回值函数示例代码如下:

# coding=utf-8
# 代码文件:chapter9/ch10.3.1.py

def show_info(sep=':', **info): ①
    """定义**可变参数函数"""
    print('-----info------')
    for key, value in info.items():
        print('{0} {2} {1}'.format(key, value, sep))
    return  # return None 或省略   ②


result = show_info('->', name='Tony', age=18, sex=True)
print(result)  # 输出 None 


def sum(*numbers, multiple=1): ③
    """定义*可变参数函数"""
    if len(numbers) == 0:
        return  # return None 或省略 ④
    total = 0.0
    for number in numbers:
        total += number
    return total * multiple


print(sum(30.0, 80.0))  # 输出110.0
print(sum(multiple=2)) # 输出 None ⑥

上述代码定义了两个函数,这个两个函数事实上是在10.3.2节示例基础上的重构。其中代码第①行的show_info()只是输出一些信息,不需要返回数据,因此可以省略return语句。如果一定要使用return语句,见代码第②行在函数结束前使用return或return
None。

对于本例中的show_info()函数强加return语句显然是多此一举,但是有时使用return或return
None是必要的。代码第③行定义了sum()函数,如果numbers中数据是空的,后面的求和计算也就没有意义了,可以在函数的开始判断numbers中算法有数据,如果没有数据则使用return或return
None跳出函数,见代码第④行。

多返回值函数

有时需要函数返回多个值,实现返回多个值的实现方式有很多,简单实现是使用元组返回多个值,因为元组作为数据结构可以容纳多个数据,另外元组是不可变的,使用起来比较安全。

下面来看一个示例:

# coding=utf-8
# 代码文件:chapter9/ch10.3.2.py


def position(dt, speed):  ①
    posx = speed[0] * dt  ②
    posy = speed[1] * dt  ③
    return (posx, posy)  ④


move = position(60.0, (10, -5)) ⑤

print("物体位移:({0}, {1})".format(move[0], move[1])) ⑥

这个示例是计算物体在指定时间和速度时的位移。第①行代码是定义position函数,其中dt参数是时间,speed参数是元组类型,speed第一个元素X轴上的速度,speed第二个元素Y轴上的速度。position函数的返回值也是元组类型。

函数体中的第②行代码是计算X方向的位移,第③行代码是计算Y方向的位移。最后,第④行代码将计算后的数据返回,(posx,
posy)是元组类型实例。

第⑤行代码调用函数,传递的时间是60.0秒,速度是(10,
-5)。第⑥行代码打印输出结果,结果如下:

物体位移:(600.0, -300.0)

函数变量作用域

变量可以在模块中创建,作用域是整个模块,称为全局变量。变量也可以在函数中创建,默认情况下作用域是整个函数,称为局部变量。

示例代码如下:

# coding=utf-8
# 代码文件:chapter9/ch10.4.py

# 创建全局变量x
x = 20 ①

def print_value():
    print("函数中x = {0}".format(x)) ②

print_value()
print("全局变量x = {0}".format(x))

输出结果:

函数中x = 20
全局变量x = 20

上述代码第①行是创建全局变量x,全局变量作用域是整个模块,所以在print_value()函数中也可以访问变量x,见代码第②行。

修改上述示例代码如下:

# 创建全局变量x
x = 20 

def print_value():
    # 创建局部变量x
    x = 10  ①
    print("函数中x = {0}".format(x)) 

print_value()
print("全局变量x = {0}".format(x))

输出结果:

函数中x = 10
全局变量x = 20

上述代码的是print_value()函数中添加x =
10语句,见代码第①行,这会函数中创建x变量,函数中的x变量与全局变量x命名相同,在函数作用域内会屏蔽全局x变量。

函数中创建的变量默认作用域是当前函数,这可以防止程序员少犯错误,因为函数中创建的变量,如果作用域是整个模块,那么在其他函数中也可以访问,Python无法从语言层面定义常量,所以在其他函数中由于误操作修改了变量,这样一来很容易导致程序出现错误。即便笔者不赞同,但Python提供这种可能,通过在函数中将变量声明为global的,可以把变量的作用域变成全局的。

修改上述示例代码如下:

# 创建全局变量x
x = 20 

def print_value():
    global x  ①
    x = 10   ②
    print("函数中x = {0}".format(x)) 

print_value()
print("全局变量x = {0}".format(x))

输出结果:

函数中x = 10
全局变量x = 10

代码第①行是在函数中声明x变量作用域为全局变量,所以代码第②行修改x值,就是修改全局变量x的数值。

生成器

在一个函数中使用return关键字返回数据,但是有时候会使用yield 关键字返回数据。yield 关键字的函数返回的是一个生成器(generator)对象,生成器对象是一种可迭代对象。

如果想计算平方数列,通常的实现代码如下:

def square(num):  ①
    n_list = []

    for i in range(1, num + 1):
        n_list.append(i * i) ②

    return n_list ③

for i in square(5):  ④
    print(i, end=' ')

返回结果是:

1 4 9 16 25

首先定义一个函数,见代码第①行。代码第②行通过循环计算一个数的平方,并将结果保存到一个列表对象n_list中。最后返回列表对象,见代码第③行。代码第④行是遍历返回的列表对象。

在Python中还可以有更加好解决方案,实现代码如下:

def square(num):

    for i in range(1, num + 1):
        yield i * i    ①


for i in square(5):  ②
    print(i, end=' ')

返回结果是:

1 4 9 16 25

代码第①行使用了yield关键字返回平方数,不再需要return关键字了。代码第②行调用函数square()返回的是生成器对象。生成器对象是一种可迭代对象,可迭代对象通过next()方法获得元素,代码第②行的for循环能够遍历可迭代对象,就是隐式地调用生成器的next()方法获得元素的。

显式地调用生成器的next()方法,在Python Shell中运行示例代码如下:

>>> def square(num):

    for i in range(1, num + 1):
            yield i * i

        
>>> n_seq = square(5)
<generator object square at 0x000001C8F123CE60>
>>> n_seq.__next__()  ①
1
>>> n_seq.__next__()
4
>>> n_seq.__next__()
9
>>> n_seq.__next__()
16
>>> n_seq.__next__()
25
>>> n_seq.__next__() ②
Traceback (most recent call last):
  File "<pyshell#33>", line 1, in <module>
    n_seq.__next__()
StopIteration
>>>

上述代码第①行~第②行调用了6次next()方法,但第6次调用则会抛出StopIteration异常,这是因为已经没有元素可迭代了。

生成器函数是通过yield返回数据,与return不同的是:return语句一次返回所有数据,函数调用结束;而yield语句只返回一个元素数据,函数调用不会结束,只是暂停,直到next()方法被调用,程序继续执行yield语句之后的语句代码。这个过程如图10-1所示。

图10-1 生成器函数执行过程

注意
生成器特别适合用于遍历一些大序列对象,它无须将对象的所有元素都载入内存后才开始进行操作。而是仅在迭代至某个元素时才会将该元素载入内存。

嵌套函数

在本节之前定义的函数都是全局函数,并将他们定义在全局作用域中。函数还可定义在另外的函数体中,称作“嵌套函数”。

示例代码:

# coding=utf-8
# 代码文件:chapter10/ch10.6.py

def calculate(n1, n2, opr):
    multiple = 2

    # 定义相加函数
    def add(a, b): ①
        return (a + b) * multiple

    # 定义相减函数
    def sub(a, b): ②
        return (a - b) * multiple

    if opr == '+':
        return add(n1, n2)
    else:
        return sub(n1, n2)

print(calculate(10, 5, '+'))  # 输出结果是30
# add(10, 5) 发生错误 ③
# sub(10, 5)  发生错误 ④

上述代码中定义了两个嵌套函数:add()和sub(),见代码第①行和第②行。嵌套函数可以访问所在外部函数calculate()中的变量multiple,而外部函数不能访问嵌套函数局部变量。另外,嵌套函数的作用域在外部函数体内,因此在外部函数体之外直接访问嵌套函数会发生错误,见代码第③行和第④行。

函数式编程基础

函数式编程(functional
programming)与面向对象编程一样都一种编程范式,函数式编程,也称为面向函数的编程。

Python并不是彻底的函数式编程语言,但是还是提供了一些函数式编程必备的技术,主要有:函数类型和Lambda表达式,他们是实现函数式编程的基础。

函数类型

Python提供了一种函数类型function,任何一个函数都有函数类型,但是函数调用时,就创建了函数类型实例,即函数对象。函数类型实例与其他类型实例一样,在使用场景上没有区别:它可以赋值给一个变量;也可以作为参数传递给一个函数;还可以作为函数返回值使用。

为了理解函数类型,先重构10.6节中嵌套函数的示例,示例代码如下:

# coding=utf-8
# 代码文件:chapter10/ch10.7.1.py

def calculate_fun(opr): ①
    # 定义相加函数
    def add(a, b):
        return a + b

    # 定义相减函数
    def sub(a, b):
        return a - b

    if opr == '+':
        return add ②
    else:
        return sub ③

f1 = calculate_fun('+')  ④
f2 = calculate_fun('-')  ⑤

print(type(f1))

print("10 + 5 = {0}".format(f1(10, 5)))  ⑥
print("10 - 5 = {0}".format(f2(10, 5)))  ⑦

输出结果:

<class 'function'>
10 + 5 = 30
10 - 5 = 10

上述代码第①行重构了calculate_fun()函数的定义,现在只接收一个参数opr。代码第②行是在opr

'+'为True时返回add函数名,否则返回sub函数名,见代码第③行。这里的函数名本质上函数对象。calculate_fun()函数与10.5节示例calculate()函数不同之处在于,calculate_fun()函数返回的是函数对象,calculate()函数返回的是整数(相加或相减计算之后的结果)。所以代码第④行的f1是add()函数对象,代码第⑤行的f2是sub()函数对象。

函数对象是可以与函数一样进行调用的。事实上在第⑥行之前没有真正调用add()函数进行相加计算,f1(10,
5)表达式才真的调用了add()函数。第⑦行的f2(10, 5)表达式是调用sub()函数。

Lambda表达式

理解了函数类型和函数对象,学习Lambda表达式就简单了。Lambda表达式本质上一种匿名函数,匿名函数也是函数有函数类型,也可以创建函数对象。

定义Lambda表达式语法如下:

lambda 参数列表 : Lambda体

lambda是关键字声明这是一个Lambda表达式,“参数列表”与函数的参数列表是一样的,但不需要小括号包裹起来,冒号后面是“Lambda体”,Lambda表达式主要的代码在此处编写,类似于函数体。

注意
Lambda体部分不能是一个代码块,不能包含多条语句,只有一条语句,语句会计算一个结果返回给Lambda表达式,但是与函数不同是,不需要使用return语句返回。与其他语言中的Lambda表达式相比,Python中提供Lambda表达式只能处理一些简单的计算。

重构10.7.1节示例,代码如下:

# coding=utf-8
# 代码文件:chapter10/ch10.7.2.py

def calculate_fun(opr):
    if opr == '+':
        return lambda a, b: (a + b) ①
    else:
        return lambda a, b: (a - b) ②

f1 = calculate_fun('+')
f2 = calculate_fun('-')

print(type(f1))

print("10 + 5 = {0}".format(f1(10, 5)))
print("10 - 5 = {0}".format(f2(10, 5)))

输出结果:

<class 'function'>
10 + 5 = 30
10 - 5 = 10

上述代码第①行替代了add()函数,第②行替代了sub()函数,代码变的非常的简单。

三大基础函数

函数式编程本质是通过函数处理数据,过滤、映射和聚合是处理数据的三大基本操作。针对但其中三大基本操作Python提供了三个基础的函数:filter()、map()和reduce()。

  1. filter()

过滤操作使用filter()函数,它可以对可迭代对象的元素进行过滤,filter()函数语法如下:

filter(function, iterable)

其中参数function是一个函数,参数iterable是可迭代对象。filter()函数调用时iterable会被遍历,它的元素逐一传入function函数,function函数返回布尔值,在function函数中编写过滤条件,如果为True的元素被保留,如果为False的元素被过滤掉。

下面通过一个示例介绍一下filter()函数使用,示例代码如下:

users = ['Tony', 'Tom', 'Ben', 'Alex']

users_filter = filter(lambda u: u.startswith('T'), users) ①
print(list(users_filter))

输出结果:

['Tony', 'Tom']

代码第①行调用filter()函数过滤users列表,过滤条件是T开通的元素,lambda u:
u.startswith('T')是一个Lambda表达式,它提供了过滤条件。filter()函数还不是一个列表,需要使用list()函数转换过滤之后的数据为列表。

再看一个示例:

number_list = range(1, 11)
nmber_filter = filter(lambda it: it % 2 == 0, number_list)
print(list(nmber_filter))

该示例实现了获取1~10数字中的偶数,输出结果如下:

[2, 4, 6, 8, 10]
  1. map()

映射操作使用map()函数,它可以对可迭代对象的元素进行变换,map()函数语法如下:

map(function, iterable)

其中参数function是一个函数,参数iterable是可迭代对象。map()函数调用时iterable会被遍历,它的元素逐一传入function函数,在function函数中对元素进行变换。

下面通过一个示例介绍一下map()函数使用,示例代码如下:

users = ['Tony', 'Tom', 'Ben', 'Alex']

users_map = map(lambda u: u.lower(), users) ①  
print(list(users_map))

输出结果:

['tony', 'tom', 'ben', 'alex']

上述代码第①行调用map()函数将users列表元素转换为小写字母,变换使用Lambda表达式lambda
u:
u.lower()。map()函数返回的还不是一个列表,需要使用list()函数将过滤之后的数据转换为列表。

函数式编程时数据可以从一个函数“流”入另外一个函数,但是遗憾的是Python并不支持“链式”API。例如想获取users列表中“T”开通的名字,再将其转换为小写字母,这样的需求需要使用filter()函数进行过滤,再使用map()函数进行映射变换。实现代码如下:

users = ['Tony', 'Tom', 'Ben', 'Alex']

users_filter = filter(lambda u: u.startswith('T'), users)

# users_map = map(lambda u: u.lower(), users_filter) ①
users_map = map(lambda u: u.lower(), filter(lambda u: u.startswith('T'), users)) ②

print(list(users_map))

上述代码第①行和第②行实现相同功能。

  1. reduce()

聚合操作会将多个数据聚合起来输出单个数据,聚合操作中最基础的是归纳函数reduce(),reduce()函数会将多个数据按照指定的算法积累叠加起来,最后输出一个数据。

reduce()函数语法如下:

reduce(function, iterable[, initializer])

参数function是聚合操作函数,该函数有两个参数,参数iterable是可迭代对象;参数initializer初始值。

下面通过一个示例介绍一下reduce()函数使用。下面示例实现了对一个数列求和运算,代码如下:

from functools import reduce ①

a = (1, 2, 3, 4)
a_reduce = reduce(lambda acc, i: acc + i, a)  # 10 ②
# a_reduce = reduce(lambda acc, i: acc + i, a, 2)  # 12 ③
print(a_reduce)

reduce()函数是在functools模块中定义的,所以要使用reduce()函数需要导入functools模块,见代码第①行。代码第②行是调用reduce()函数,其中lambda
acc, i: acc +
i是进行聚合操作的Lambda表达式,该Lambda表达式有两个参数,其中acc参数是上次累积计算结果,i当前元素,acc

i表达式是进行累加。reduce()函数最后的计算结果是一个数值,直接可以使用通过reduce()函数返回。代码第行是传入了初始值2,则计算的结果是12。

本章小结

通过对本章内容的学习,读者可以熟悉在Python中如何定义函数、函数参数和函数返回值,了解函数变量作用域和嵌套函数。最后还介绍了Python中函数式编程基础。

配套视频

http://www.zhijieketang.com/classroom/10/courses

配套源代码

http://www.zhijieketang.com/group/8

纸质版电商

京东:https://item.jd.com/12468732.html
当当:http://product.dangdang.com/25574315.html

作者微博:@tony_关东升
邮箱:eorient@sina.com
智捷课堂微信公共号:zhijieketang
Python读者服务QQ群:565736812

推荐阅读更多精彩内容