Python3入门到精通——函数式编程与装饰器

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

匿名函数

顾名思义,匿名函数直观上就是省去命名的函数。Python 中通过lambda关键字定义匿名函数,格式为

lambda parameter_list : expression

匿名函数接收的参数列表等同于func(param)中的参数列表param,区别在于函数体,匿名函数中只能是一些简短的表达式,不能定义、赋值变量,以表达式的计算结果作为返回值

匿名函数同样可以作为赋值对象

In [1]:
def add(x,y):
    return x+y

lam_add = lambda x,y: x+y  # 同样可以作为赋值对象
In [2]:
add(1,2)
Out[2]:
3
In [3]:
lam_add(1,2)
Out[3]:
3
In [4]:
lambda x,y: a = x+y  # 不允许赋值,只能写表达式
  File "<ipython-input-4-0901228abce8>", line 1
    lambda x,y: a = x+y  # 不允许赋值,只能写表达式
                                       ^
SyntaxError: can't assign to lambda

从定义中可以看出匿名函数的突出特点在于表达式部分,也正因如此,其他语言如 C#,Java 称这种程式为 lambda 表达式,匿名函数另有其专门的形式

三元表达式

lambda 表达式里常常会有一种叫做三元运算的操作,形式如下

true_res if expression else false_res

expression为真时返回true_res,反之返回false_res。其效果等同于 Java 等其他语言中的三元运算 expression ? true_res : false_res

别看其中带有if else关键字,它可不是条件语句,只是特殊点的表达式

In [5]:
1 if 2 < 3 else 0
Out[5]:
1

map

另一个常和 lambda 表达式一同出现的是映射(map),注意这是一个类,逻辑等同于数学中的映射,将输入空间进行某些运算投射到输出空间中。map 格式如下

map(func, *iterables) --> map object

其接收一个函数和可迭代数据结构组成的参数列表,计算结果保存为一个 map 对象并将其返回。代码形式上看,map 相当于简化版本的 for 循环,对每一个元素都进行相同的 func 操作

In [7]:
def double(x):
    return 2*x

res = map(double, [1,2,3,4,5])

map 对象的内容通过可迭代数据结构初始化后查看

In [8]:
list(res)
Out[8]:
[2, 4, 6, 8, 10]

有了 map 和 lambda 表达式后,一些简单的运算逻辑就可以很简洁的表达出来,计算效率并不会发生变化。

lambda 表达式可以充当 map 中的 func 功能,比如上面求取2*x的计算逻辑就可以使用下面的方法替代

In [10]:
input_x = [1,2,3,4,5,6]

list(map(lambda x : 2*x, input_x))
Out[10]:
[2, 4, 6, 8, 10, 12]

因为 map 可以接收变长参数列表,所以可以再复杂些,但要注意 lambda 表达式的参数个数和 map 参数列表长度要相同

In [12]:
input_x = [1,2,3,4,5,6]
input_y = [1,2,3,4,5,6]

list(map(lambda x,y : 2*x + y, input_x, input_y))
Out[12]:
[3, 6, 9, 12, 15, 18]

要留意参数列表中各参数长度一般要相同,如果内部元素个数不相同,以元素个数最小的参数为准,即保证运算逻辑成立的最大元素个数

In [13]:
input_x = [1,2,3,4,5,6]
input_y = [1,2,3,4]

list(map(lambda x,y : 2*x + y, input_x, input_y))  # 结果只有 4 个值
Out[13]:
[3, 6, 9, 12]
In [15]:
input_x = [1,2,3,4]  # 是谁少了无所谓
input_y = [1,2,3,4,5,6]

list(map(lambda x,y : 2*x + y, input_x, input_y))
Out[15]:
[3, 6, 9, 12]

reduce

lambda 表达式也常和缩减计算(reduce)一起使用,Python 中使用 reduce 要从 functools 模块中导入,reduce 是一个函数,其格式如下:

reduce(function, sequence[, initial]) -> value

reduce会从左到右依次从sequence中取出两个值进行function计算,然后将计算结果作为参数之一和下一个sequence元素一起再一次进行funciton计算,不断重复直到sequence“消耗”完毕,这也是“缩减”的由来。如果指定了initial,第一次计算时initial会参与计算,也就是说第一次从sequence中只取一个值

和 lambda 同用时注意因缩减计算每次有两个参数,lambda 表达式的参数也必须是两个

In [18]:
from functools import reduce

input_x = [1,2,3,4]

reduce(lambda x,y: x+y, input_x) # 1+2+3+4
Out[18]:
10
In [21]:
input_y = ['1','2','3','4']
reduce(lambda x,y: x+y, input_y, 'begin:') # 字符串拼接
Out[21]:
'begin:1234'

filter

再来看一个常和 lambda 表达式一起使用的兄弟,过滤器 filter,这也是一个类,形式如下:

filter(function or None, iterable) --> filter object

filter 接收两个参数,一个是过滤判别函数function,该函数必须有返回值且返回值能判断真假,另一个是要遍历的数据iterable。如果funciton为空,保留iterable里值为真的内容

filter 返回的也是一个对象,所以需要用 list 之类的数据结构获取里面的内容

In [25]:
input_x = [1,0,1,0,1,0,1]

list(filter(None, input_x))  # 1,0 本身就可以代表真与假
Out[25]:
[1, 1, 1, 1]
In [26]:
input_x = [1,2,3,4,5,6]

list(filter(lambda x: x%2, input_x)) # 筛选奇数
Out[26]:
[1, 3, 5]

map, reduce, filter 和 lambda 表达式是函数式编程的特征标识,lambda 表达式更是可以视为最基本的算子。相对的,命令式编程的标志性特征是函数定义 def,if-else 语句以及循环。函数式编程更强调结果而不是过程。

函数式编程相较于命令式编程,有以下优点:

  1. 减少了可变量(Immutable Variable)的声明,程序更加安全
  2. 少了非常多的状态变量的声明与维护,天然适合并行计算等任务
  3. 代码更为简洁,可读性更强

函数式编程中的函数实质上不是计算机编程概念里的函数,而是数学里的函数,即一种映射关系。也就是说函数输出值只和参数有关,与其他状态无关,只要参数不变,无论调用多少次结果都一样。

函数式编程里的变量也不是通常意义上的变量,即状态存储单元,实质是数学里的变量,是一个值的名称,变量值不可变,也就是不能像命令式编程中那样多次赋值。x = x + 1 程序中可行是依赖于状态可变,在数学中会被判定为假

装饰器

无论使用哪种编程语言,总免不了对代码的补充、修订和重构,日积月累,人们为了提高开发效率总结出了一些经验和一系列原则。其中有一个很重要的原则——开闭原则,说的是

对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的

就是说当需求变更,不应该直接修改旧有代码内容,而是通过扩展类或者函数功能实现这一目的。比如说下面有若干函数,分别打印-+*构成的分割线

In [1]:
def line_split():
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '-'
    print(res)
    
def plus_split():
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '+'
    print(res)

def multiple_split():
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '*'
    print(res)
In [2]:
line_split()
----------
In [3]:
plus_split()
++++++++++
In [4]:
multiple_split()
**********

突然我们有了新的需求,要求输出这些函数的运行时长。一个直观的想法是逐个添加功能代码

In [ ]:
from time import time

def bad_change():  # 违反开闭原则
    start = time()
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '-'
    end = time()
    print(res)
    print(start - end)

这样直接修改旧有函数内容违反了开闭原则。因为只是一两个函数进行简单的修改或许看不出,但当很多函数都要增加这一功能,之后又要撤销这个功能,都要像这样直接修改内部内容不仅繁琐且容易出错。

稍好些的设计是重新设计一个函数,专门负责运行时间的计算

In [15]:
import time

def time_cost(function): # 虽然实现了功能拓展,但也将功能分离了
    start = time.time()
    function()
    end = time.time()
    print('time costs: %.3fs'% (end - start))
In [16]:
time_cost(line_split)
----------
time costs: 0.102s
In [17]:
time_cost(plus_split)
++++++++++
time costs: 0.086s
In [18]:
time_cost(multiple_split)
**********
time costs: 0.090s

这样我们就实现了功能的便捷拓展,但却将功能分离了,我们的本意是让原有函数本身具备这样一种功能,而不是将这种功能外包给其他函数。

这时就要用到装饰器了,Python 的装饰器类似于 C# 的特性,Java 的注解。装饰器一般结构定义如下

def decorator(function):
    def wrapper():
        function()
    return wrapper

wrapper实现了对function的扩展并被decorator返回。通过与语法糖的配合实现既拓展功能,又不分离功能,语法糖使用方法如下

@decorator
def function():
    pass
In [19]:
from time import time

def time_out(function):
    def wrapper():
        start = time()
        function()
        end = time()
        print('time costs: %.6f' % (end - start))
    return wrapper

@time_out  # 就像加了一个挂件一样
def line_split():
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '-'
    print(res)
    
@time_out
def plus_split():
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '+'
    print(res)

@time_out
def multiple_split():
    res = ''
    for i in range(1000000):
        if i % 100000 == 0:
            res += '*'
    print(res)
In [20]:
line_split()
----------
time costs: 0.170900
In [21]:
multiple_split()
**********
time costs: 0.090946
In [22]:
plus_split()
++++++++++
time costs: 0.168902

通过装饰器我们既扩展了功能,同时调用方式也没有发生变化。语法糖保证了易读性。

但还有一些疏漏,这些分隔符函数不用接收参数。当原函数参数列表不为空时,装饰器通过给wrapper传入可变参数列表*args进行匹配

In [30]:
from time import time

def time_out(function):
    def wrapper(*args):  # 接收可变参数
        start = time()
        function(*args)
        end = time()
        print('time costs: %.6f' % (end - start))
    return wrapper

@time_out  # 不用因参数个数不同而重新定义新的装饰器
def char_split(char):
    res = ''+char
    for i in range(1000000):
        if i % 100000 == 0:
            res += char
    print(res)
    
@time_out
def chars_split(char_one, char_two):
    res = ''
    for i in range(1000000):
        if i % 200000 == 0:
            res += char_one
        elif i % 100000 == 0:
            res += char_two
    print(res)
In [31]:
char_split('a')
aaaaaaaaaaa
time costs: 0.102925
In [33]:
chars_split('a', 'b')
ababababab
time costs: 0.167900

再进一步,如果不只是参数数量不定,可能还有关键字参数,于是得到了最常见的装饰器形态

def decorator(function):
    def wrapper(*args, **kw):
        function(*args, **kw)
    return wrapper
In [34]:
from time import time

def time_out(function):
    def wrapper(*args, **kw):  # 接收可变参数,关键字参数,可以应对任意形式的参数列表
        start = time()
        function(*args, **kw)
        end = time()
        print('time costs: %.6f' % (end - start))
    return wrapper
In [37]:
@time_out
def note_char_split(char_one, char_two, **kw): 
    res = ''
    for i in range(1000000):
        if i % 200000 == 0:
            res += char_one
        elif i % 100000 == 0:
            res += char_two
    res += '\n' + str(kw)
    print(res)
In [38]:
note_char_split('-','|', chars_type='str', chars_num=2)
-|-|-|-|-|
{'chars_type': 'str', 'chars_num': 2}
time costs: 0.168899

此外,每个函数不止可以接受一个装饰器

@decorator_1
@decorator_2
def function():
    pass