Python的函数式编程思想

这篇博客中每一行代码都值得反复学习。

函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

参考连接

全文整理自廖雪峰大佬的函数式编程教程

何为函数式编程

函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

函数式编程与命令式编程最大的不同其实在于: 函数式编程关心数据的映射,命令式编程关心解决问题的步骤 这里的映射就是数学上「函数」的概念,即一种东西和另一种东西之间的对应关系。 这也是为什么「函数式编程」叫做「函数」式编程,核心思想就是要代码体现这种映射关系。

举个例子

假如,现在你来面试,面试官让你把二叉树镜像反转一下?

命令式编程:描述「从旧树得到新树应该怎样做」来实现

1
2
3
4
5
def invertTree(root):
if root is None:
return None
root.left, root.right = invertTree(root.right), invertTree(root.left)
return root

好了,现在停下来看看这段代码究竟代表着什么—— 它的含义是:首先判断节点是否为空;然后翻转左树;然后翻转右树;最后左右互换。 这就是命令式编程——你要做什么事情,你得把达到目的的步骤详细的描述出来,然后交给机器去运行。 这也正是命令式编程的理论模型——图灵机的特点。一条写满数据的纸带,一条根据纸带内容运动的机器,机器每动一步都需要纸带上写着如何达到。

函数式编程:通过描述一个 旧树->新树 的映射来实现

1
2
3
4
5
def invert(node):
if node is None:
return None
else
return Tree(node.value, invert(node.right), invert(node.left))

所谓“翻转二叉树”,可以看做是要得到一颗和原来二叉树对称的新二叉树。这颗新二叉树的特点是每一个节点都递归地和原树相反。这段代码体现的思维,就是旧树到新树的映射——对一颗二叉树而言,它的镜像树就是左右节点递归镜像的树。 Haskell(发音为/ˈhæskəl/)是一种标准化的,通用的纯函数编程语言,有非限定性语义和强静态类型。它的命名源自美国逻辑学家哈斯凯尔·加里,他在数理逻辑方面上的工作使得函数式编程语言有了广泛的基础。在Haskell中,“函数是第一类对象”。作为一门函数编程语言,主要控制结构是函数。Haskell语言是1990年在编程语言Miranda的基础上标准化的,并且\(\lambda\) 演算为基础发展而来。这也是为什么Haskell语言以希腊字母 \(\lambda\)(Lambda)作为自己的标志。Haskell具有 "证明即程序、命题为类型" 的特征。

那么这样有什么好处呢?

  • 首先,最直观的角度来说,函数式风格的代码可以写得很精简,大大减少了键盘的损耗???
  • 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
  • 其次,函数式的代码是“对映射的描述”,它不仅可以描述二叉树这样的数据结构之间的对应关系,任何能在计算机中体现的东西之间的对应关系都可以描述——比如函数和函数之间的映射(比如 functor);比如外部操作到 GUI 之间的映射(就是现在前端热炒的所谓 FRP)。它的抽象程度可以很高,这就意味着函数式的代码可以更方便的复用。 抽象程度越高越容易被复用,离计算机硬件越近,离人类越远。
  • 另外还有其他答主提到的,可以方便的并行
  • 同时,将代码写成这种样子可以方便用数学的方法进行研究(不能理解 monad 就是自函子范畴上的一个幺半群你还想用 Haskell 写出 Hello world ?)
  • 至于什么科里化、什么数据不可变,都只是外延体现而已。

那么这样处理有什么坏处?

  • 在处理I/O时,要么引入可变变量,要么通过Monad来进行封装(如State Monad和IO Monad)。单纯依靠函数式编程是不行的。
  • Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。

来源:知乎 nameoverflow

Python中的高阶函数

map/reduce

相传Google Search的技术栈有三驾马车,分别是Google File System、BigTable、MapReduce[1],这三种技术共同支撑起了Google search business的江山,MapReduce实际上就是一个分布式的计算框架。

[1] Dean J , Ghemawat S . MapReduce: Simplified Data Processing on Large Clusters[C]// Sixth Symposium on Operating System Design & Implementation. USENIX Association, 2004.

map函数:接受一个键值对(key-value pair)(文本名:文本内容),产生一组中间键值对(单词:出现次数)。MapReduce框架会将map函数产生的中间键值对里键相同的值传递给一个reduce函数。

reduce函数:接受一个键key,以及相关的一组值(value list)(单词A:(1,2,3)),将这组值进行合并产生一组规模更小的值(通常只有一个或零个值)(单词A:6)。

map

map 到底在干啥?\(map(func, (1, 2, 3)) = (func(1), func(2), func(3))\)

map()函数接收两个参数,一个是函数,一个是Iterablemap 将传入的函数依次作用到序列的每个元素,并把结果作为新的 Iterator 返回A。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 基于map函数生成 生成器
print("基于map函数生成 生成器")
def f(x):
return x * x
iterabel = [1, 2, 3]
_generator = map(f, iterabel)
print(_generator)
print("可迭代:{}".format('__iter__' in dir(_generator)))
print("可生成:{}".format('__next__' in dir(_generator)))
print(next(_generator))
for i in _generator:
print(i)


# 基于()生成 生成器
print("基于()生成 生成器")
_generator2 = (x*x for x in [1, 2, 3])
print(_generator2)
print("可迭代:{}".format('__iter__' in dir(_generator2)))
print("可生成:{}".format('__next__' in dir(_generator2)))
print(next(_generator2))
for i in _generator2:
print(i)


# 基于yiled生成 生成器
print("基于yiled生成 生成器")
def fun(nums):
index = 0
while index < len(nums):
x = nums[index]
yield x * x
index += 1 # 下次迭代进来才会执行这一句
_generator3 = fun([1, 2, 3])
print(_generator3)
print("可迭代:{}".format('__iter__' in dir(_generator3)))
print("可生成:{}".format('__next__' in dir(_generator3)))
print(next(_generator3))
for i in _generator3:
print(i)

# Output
基于map函数生成 生成器
<map object at 0x000001C7AFF25520>
可迭代:True
可生成:True
1
4
9
基于()生成 生成器
<generator object <genexpr> at 0x000001C7AFEA92E0>
可迭代:True
可生成:True
1
4
9
基于yiled生成 生成器
<generator object fun at 0x000001C7AFEA9350>
可迭代:True
可生成:True
1
4
9

map()传入的第一个参数是f,即函数对象本身。由于结果r是一个IteratorIterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个 list

可以发现,通过 map 函数返回得到的是一个生成器和通过 () 生成一个生成器、基于yiled生成一个生成器的效果是一样的。

map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的 \(f(x)=x^2\),还可以计算任意复杂的函数,比如,把这个 list 所有数字转为字符串:

1
2
3
4
>>> list(map(str, [1, 2, 3, 4, 5, 6]))
['1', '2', '3', '4', '5', '6']
>>> [str(x) for x in [1, 2, 3, 4, 5, 6]]
['1', '2', '3', '4', '5', '6']

可以发现map 也可以完成列表解析式的工作。

reduce

reduce 到底在干啥?\(reduce(func, (1, 2, 3, 4, 5))=func(func(func(func(1, 2), 3), 4), 5)\)

reduce函数和map函数一样,也接收两个参数,一个是函数,一个是Iterablereduce函数把一个函数func作用在一个序列[x1, x2, x3, ...]上,这个 func 函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

1
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

一个简单的例子,把序列[1, 3, 5, 7, 9]变换成整数13579reduce就可以派上用场:

1
2
3
4
5
from functools import reduce
def list2num(x, y):
return x * 10 + y

print(reduce(list2num, [1, 2, 3]))

注意,装饰器中的 wraps 函数也在 functools 模块中,wraps 函数负责将原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

reducemap 结合,将str转换成int,map将str转换成list,reduce将list转换成int。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from functools import reduce

def str2list(x):
digit = {'0':0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9}
return digit[x]

def list2num(x, y):
return x * 10 + y

# map/reduce 先map后rudece
step1 = map(str2list, "123") # <class 'map'>
res = reduce(list2num, step1) # <class 'int'>

# map&reduce 一次搞定
res = reduce(list2num, map(str2list, "123")) # <class 'int'>

# one fuction 定义为函数
def str2int(_str):
def list2num(x, y):
return x * 10 + y
def str2list(x):
digit = {'0':0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9}
return digit[x]
return reduce(list2num, map(str2list, _str))

res = str2int("12345")
print(type(res))
print(res)

practice

利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT'],输出:['Adam', 'Lisa', 'Bart']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def format(_name):
# str是不可变对象 先调整成list 再操作
_name = list(_name)
# 首字母 需大写
if ord(_name[0]) > 90: # ord('Z')=90 若首字母为小写 则需要更正 将小写转大写
_name[0] = chr(ord(_name[0])-32)
# 非首字母 需小写
for i in range(1, len(_name)):
if ord(_name[i]) < 97: # ord('a')=97 若非首字母为大写 则需要更正 将大写转小写
_name[i] = chr(ord(_name[i])+32)
return ''.join(_name) # 最后再将结果拼接成str

names = ['adam', 'LISA', 'barT']
a = map(format, names)
print(list(a))

# output
['Adam', 'Lisa', 'Bart']

Python提供的sum()函数可以接受一个list并求和,请编写一个prod()函数,可以接受一个list并利用reduce()求积:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import reduce

def prod(nums):
def product(x, y):
return x * y
return reduce(product, nums)

print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))
if prod([3, 5, 7, 9]) == 945:
print('测试成功!')
else:
print('测试失败!')

# output
3 * 5 * 7 * 9 = 945
测试成功!

利用mapreduce编写一个str2float函数,把字符串'123.456'转换成浮点数123.456

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from functools import reduce

# 全局变量
is_decimal = False
carry = 1

def numstr2list(x):
digit = {'0':0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, ".": "."}
return digit[x]

def list2float(x, y):
if y == '.': # 切换成小数模式 然后直接返回
global is_decimal
is_decimal = True
return x
if is_decimal: # 小数模式
global carry
carry /= 10
return x + y * carry
else: # 整数模式
return x * 10 + y

def str2float(_nums):
return reduce(list2float, list(map(numstr2list, '123.456')))

res = str2float('123.456')
print('str2float(\'123.456\') =', res)
if abs(res - 123.456) < 0.00001:
print('测试成功!')
else:
print('测试失败!')

# output
str2float('123.456') = 123.456
测试成功!

filter

基本原理

filter()函数用于过滤序列。\(filter(func, (1, 2, 3))=(1\ if\ func(1),\ 2\ if\ func(2),\ 3\ if\ func(3))\)

map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

例如,在一个list中,删掉偶数,只保留奇数,可以这么写:

1
2
3
4
5
6
nums = [1, 2, 3, 4, 5]
res = [x for x in nums if x%2 != 0]
print(res)

# output
[1, 3, 5]

这是列表解析器的写法,使用 filter() 函数,可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def is_odd(x):
return x % 2 != 0

def is_enve(x):
return x % 2 == 0

nums = [1, 2, 3, 4, 5]
res = filter(is_odd, nums) # 返回的<class 'filter'>也是一个generator
print(type(res))
print('__next__' in dir(res))
print(res)

# output
<class 'filter'>
True
[1, 3, 5]

practice

只用 filter 求素数

计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单:

首先,列出从2开始的所有自然数,构造一个序列: 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉: 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉: 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 取新序列的第一个数5,然后用5把序列的5的倍数筛掉: 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ... 不断筛下去,就可以得到所有的素数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 使用filter形式
# 先构造一个从3开始的奇数序列,这是一个generator,且产生无限序列。
def _odd_iter():
n = 1
while True:
n = n + 2
yield n

# 定义一个筛选函数
def _not_divisible(n):
# lambda 中的 x 是迭代器 it 里遍历的各个值
# 如果x%n大于0为True,代表该数不是n的倍数,保留。
# 如果x%n等于0为False,代表该数是n的倍数,舍弃。
return lambda x: x % n > 0

# 定义一个素数生成器,不断返回下一个素数
def primes():
yield 2 # 第一次必须返回2
it = _odd_iter() # 从3开始的奇数序列
while True:
n = next(it)
yield n
it = filter(_not_divisible(n), it) # 用n把序列it的n的倍数筛掉

_max = 100
res = []
for n in primes():
if n < _max:
res += [n]
else:
break
print(" ".join([str(x) for x in res]))


# output
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

同时用filtermaplambda求100以内的素数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = [item for item in 
filter(
lambda x:all( # all(): Return True if bool(x) is True for all values x in the iterable. If the iterable is empty, return True.
map(
lambda p: x % p !=0,
range(2, x)
)
),
range(2, 101) # 1不是素数 从2开始
)
]
a

# output
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

回数是指从左向右读和从右向左读都是一样的数,例如12321909。请利用filter()筛选出回数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from functools import reduce

def int2list(_int):
res = []
while _int:
res.insert(0, _int % 10)
_int //= 10
return res

def is_palindrome(nums):
nums = int2list(nums)
if len(nums) == 1: return True
if len(nums) == 2 and nums[0] == nums[1]: return True
# 三位以上 将list reverse 之后再做对比
copy_nums = nums[:]
copy_nums.reverse()
return copy_nums == nums

def _is_palindrome(n):
m = int(str(n)[::-1]) # 字符串切片的表达式 s[开始:结束:步长] s[::-1]就是从尾到头输出s 效果和s逆序差不多
return m == n


# 测试:
output = filter(is_palindrome, range(1, 1000))
print('1~1000:', list(output))
if list(filter(is_palindrome, range(1, 200))) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]:
print('测试成功!')
else:
print('测试失败!')

# output
1~1000: [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 202, 212, 222, 232, 242, 252, 262, 272, 282, 292, 303, 313, 323, 333, 343, 353, 363, 373, 383, 393, 404, 414, 424, 434, 444, 454, 464, 474, 484, 494, 505, 515, 525, 535, 545, 555, 565, 575, 585, 595, 606, 616, 626, 636, 646, 656, 666, 676, 686, 696, 707, 717, 727, 737, 747, 757, 767, 777, 787, 797, 808, 818, 828, 838, 848, 858, 868, 878, 888, 898, 909, 919, 929, 939, 949, 959, 969, 979, 989, 999]
测试成功!

sorted

Python内置的sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。

1
2
3
4
>>> sorted([36, 5, -12, 9, -21])
[-21, -12, 5, 9, 36]
>>> sorted([36, 5, -12, 9, -21], reverse=True)
[36, 9, 5, -12, -21]

作为一个高阶函数,sorted可以接收一个key函数来实现自定义的排序,key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。对比原始的list和经过key=abs处理过的list,例如按绝对值大小排序:

1
2
>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

对比原始的 list 和经过key=abs处理过的 list:

1
2
3
4
>>> [abs(x) for x in _list]
[36, 5, 12, 9, 21]
>>> _list
[36, 5, -12, 9, -21]

字符串排序

1
2
>>> sorted(['bob', 'about', 'Zoo', 'Credit'])
['Credit', 'Zoo', 'about', 'bob']

默认情况下,对字符串排序,是按照ASCII的大小比较的,由于'Z' < 'a',结果,大写字母Z会排在小写字母a的前面。 str.lower() 方法将字符串中所有大写字符转换为小写字符str.upper()方法将字符串中所有小写字符转换为大写字符

1
2
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']

practice

假设我们用一组tuple表示学生名字和成绩:

1
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]

请用sorted()对上述列表分别按名字排序:

1
2
3
4
5
6
7
8
9
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def func(item):
return item[0]

res = sorted(L, key=func)
print(res)

# output
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]

再按成绩从高到低排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def by_score(t):
return -t[1] # 由于sorted默认是由小到大进行排序的,所以这里进行一次取反。
L2 = sorted(L, key=by_score)
print(L2)

# 或者
def by_score(t):
return t[1] # 由于sorted默认是由小到大进行排序的,所以这里进行一次取反。
L2 = sorted(L, key=by_score, reverse=True)
print(L2)

# output
[('Adam', 92), ('Lisa', 88), ('Bob', 75),
('Bart', 66)]

一个保存学生姓名和成绩的字典,根据成绩对学生进行排序:

1
2
3
4
5
6
7
8
9
10
d = {'Bob': 75, 'Adam': 92, 'Bart': 66, 'Lisa': 88}

def by_score(item):
return -item[1]

d2 = sorted(d.items(), key=by_score,)
print(d2)

# output
[('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]

从1-100中选出所有的素数

1
a = [str(item) for item in filter(lambda x: all(map(lambda p: x % p != 0, range(2, x))), range(2, 101))]  # 1不是素数

返回函数(闭包)

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

在外部函数outter_func中又定义了内部函数inner_func,并且,内部函数inner_func可以引用外部函数outter_func参数和局部变量,当outter_func返回inner_func时,相关参数和变量都保存在返回的函数中,这种程序结构被称为“闭包(Closure)”,拥有极大的威力。

举个栗子

通常情况下,求和的函数是这样定义的:

1
2
3
4
5
def calc_sum(*args):
ax = 0
for n in args:
ax = ax + n
return ax

如果想惰性求和,怎么办?

1
2
3
4
5
6
7
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数,执行求和函数时,才真正开始求和:

1
2
3
4
5
>>> f = lazy_sum(1, 2, 3, 4, 5, 6)
>>> f
<function lazy_sum.<locals>.sum at 0x0000021C61BB0160>
>>> f()
21

在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这就是典型的“闭包(Closure)”程序结构。

请再注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数:

1
2
3
4
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1==f2
False

f1()f2()的调用结果互不影响。 函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。返回的函数并没有立刻执行,而是直到调用了f()才执行。

1
2
3
4
5
6
7
8
9
def count():
fs = []
for i in range(1, 4):
def f():
return i * i
fs.append(f)
return fs

f1, f2, f3 = count()

在上面的例子中,每次循环,都创建了一个新的函数,然后,把创建的3个函数都返回了。 你可能认为调用f1()f2()f3()结果应该是149,但实际结果是:

1
2
3
4
5
6
>>> f1, f2, f3 = count()
>>> f1()
9
>>> f2()
9
>>> f3()

全部都是9!原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。 如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变:

1
2
3
4
5
6
7
8
9
10
11
def count():
def f(j): # j就是当前的i
def g():
return j * j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入f()
return fs

f1, f2, f3 = count()

再看看结果:

1
2
3
4
5
6
7
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

缺点是代码较长,可利用lambda函数缩短代码。

practice

利用闭包返回一个计数器函数,每次调用它返回递增整数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def createCounter():
i = [0] # 这里必须要用一个可变对象,之前用的int就不行,每次进这个createCounter就初始化一个新的int,不能在原来的基础上修改,但是用可变对象就不存在这个问题了。
def counter():
i[0] += 1
return i[0]
return counter

# 测试:
counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA()) # 1 2 3 4 5
counterB = createCounter()
if [counterB(), counterB(), counterB(), counterB()] == [1, 2, 3, 4]:
print('测试通过!')
else:
print('测试失败!')

# output
1 2 3 4 5
测试通过!

小结

一个函数可以返回一个计算结果,也可以返回一个函数。 返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。

匿名函数

当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。(Python3好像是不推荐使用了)

在Python中,对匿名函数提供了有限支持。还是以map()函数为例,计算\(f(x)=x^2\) 时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:

1
2
>>> list(map(lambda x: x*x, [1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]

关键字lambda表示匿名函数,冒号前面的x表示函数参数。

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

1
2
3
4
5
6
7
>>> f = lambda x: x*x
>>> f
<function <lambda> at 0x0000020778980160>
>>> type(f)
<class 'function'>
>>> list(map(f, [1, 2, 3, 4, 5]))
[1, 4, 9, 16, 25]

同样,也可以把匿名函数作为返回值返回,比如:

1
2
def build(x, y):
return lambda: x * x + y * y

practice

请用匿名函数改造下面的代码:

1
2
3
4
def is_odd(n):
return n % 2 == 1

L = list(filter(lambda x: x%2 == 1, range(1, 20)))

改造完成

1
2
3
4
5
6
7
def is_odd(n):
return n % 2 == 1

L = list(filter(lambda x: x%2 == 1, range(1, 20)))

# output
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19]

装饰器

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。 decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。 详解参看这篇文章

偏函数

简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的偏函数不一样。

在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。举例如下:

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换:

1
2
>>> int('12345')
12345

int() 函数还提供额外的 base 参数,默认值为 10。如果传入 base 参数,就可以做 N 进制的转换:

1
2
3
4
5
6
7
8
>>> int("12345", base=10)  # 把一个10进制字符串转换成10进制int
12345
>>> int("12345", base=8) # 把一个8进制字符串转换成10进制int
5349
>>> int("12345", base=16) # 把一个16进制字符串转换成10进制int
74565
>>> int("1011", base=2) # 把一个2进制字符串转换成10进制int
11

假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

1
2
def int2(x, base=2):
return int(x, base)

这样,我们转换二进制就非常方便了:

1
2
3
4
>>> int2('1000000')
64
>>> int2('1010101')
85

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2

1
2
3
4
>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1011')
11

所以,简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

注意到上面的新的int2函数,仅仅是把base参数重新设定默认值为2,但也可以在函数调用时传入其他值:

1
2
>>> int2('1000000', base=10)
1000000

最后,创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数,当传入:

1
int2 = functools.partial(int, base=2)

实际上固定了int()函数的关键字参数base,也就是:

1
int2('10010')

相当于:

1
2
kw = { 'base': 2 }
int('10010', **kw)

当传入:

1
min3 = functools.partial(min, 3)

实际上会把3作为*args的一部分自动加到左边,也就是:

1
min3(5, 6, 7)

相当于:

1
2
args = (3, 5, 6, 7)
min3(*args)

结果为3,这样就能控制返回的最小结果不小于 3 了。

小结

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!