Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

python学习笔记函数式编程部分

函数式

函数式编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数是没有变量的,因此,任意一个函数,只要输入确定,输出就是确定的。这种纯函数我们称为没有副作用

为什么说纯函数只要输入确定,输出就能确定呢,因为允许变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入可能得到不同的输出,因此这种函数是有副作用的。

函数式编程的一个特点:

允许函数本身作为参数传入另一个函数,且还允许返回一个函数

需要注意的是,python允许使用变量,因此python不是纯函数式编程语言

高阶函数


python中函数名可以理解为指向函数体的变量,因此,可以将函数名赋值给变量,也可以对函数名重新赋值,例如:

1
2
3
4
5
6
7
8
9
10
11
12
>>> abs
<build-in function abs>
>>> f = abs
>>> f
<build-in function abs>
>>> abs = 10
>>> abs
10
>>> abs(-10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

可见将由于abs = 10这条语句,导致abs(-10)不再具有绝对值的功能,此时必须重启python交互环境,abs才能恢复原有指向。

由此引入高阶函数的概念:

可以接受另一个函数作为参数的函数称为高阶函数

一个简单的高阶函数如下:

1
2
3
4
5
def add(x, y, f):
return f(x) + f(y)

print(add(-5, 6, abs))
# 输出 11

map/reduce


map


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

例如:

1
2
3
4
5
6
7
def f(x):
return x*x


r = map(f,[1, 2, 3, 4, 5, 6, 7, 8, 9])
print(list(r))
# [1, 4, 9, 16, 25, 36, 49, 64, 81]

由于map函数将返回惰性加载的Interator,因此此处使用list()将其转化为list

虽然如上简单操作看起来并没有什么意义,但我们还能使用map完成如下有实际意义的操作:

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

1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
def normalize(name):
return name[:1].upper() + name[1:].lower()


L1 = ['adam', 'LISA', 'barT']
L2 = list(map(normalize, L1))
print(L2)
# ['Adam', 'Lisa', 'Bart']

reduce


reduce把一个函数作用在一个序列`[x1

效果类似:

reduce(f

使用reduce弄够完成如下事件:

1
2
3
4
5
6
7
8
# -*- coding: utf-8 -*-
from functools import reduce

def prod(L):
return reduce(lambda x,y: x * y,L)

print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))
# 3 * 5 * 7 * 9 = 945

使用map与reduce的组合,可以完成如下复杂的操作:

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

1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
from functools import reduce

def str2float(s):
a,b = s.split('.')
return reduce(lambda x,y: x*10+y, map(int,a)) + reduce(lambda x,y: x * 0.1 + y ,map(int,b[::-1])) * 0.1

print('str2float(\'123.456\') =', str2float('123.456'))

filter


filter机制与map有些相似,都是将将传入的函数作用在传入可便利对象中的每个元素上,区别在于,fliter是根据再用在该元素上的函数返回值是true还是false,从而决定该函数是否保留,最后返回所有被保留的元素组成的list

例如一个筛出奇数的选择器:

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


list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# 结果: [1, 5, 9, 15]

filtermap一样,但会的是一个惰性加载的Iterator,需要使用list()

filter应用


接下来我们利用Iteratorfilter实现埃氏筛法

首先对该问题进行分解:

  1. 除了2以外的偶数都是和数,因此我们只需要考虑奇数,为此,我们需要先创建一个能够不断产生奇数的Iterator
  2. 创建一个用来筛选的函数
  3. 接下来我们需要再创建一个函数,利用filter筛去1中的一些奇和数。
  4. 最后打印
产生奇数
1
2
3
4
5
def _odd_iterator():
n = 1
while True:
n += 2
yield n

构造一个能不断产生奇数的Iterator

筛选函数
1
2
def primes_filter(n):
return lambda x: x % n > 0

使用n筛去n的倍数,由于filter的第一个参数需要接受一个函数,我们可以再此处返回一个lambda函数,使这个函数既可以接受参数又可以作为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
35
# -*- coding: utf-8 -*-

def _odd_iterator():
n = 1
while True:
n += 2
yield n


def primes_filter(n):
return lambda x: x % n > 0


def primes():
yield 2
odd = _odd_iterator() # 初始化序列
while True:
n = next(odd) # 从奇数列中取d
yield n
odd = filter(primes_filter(n), odd) # 构造新序列


# 再写个生成器来取前max个素数
def print_prime(max):
p = primes()
n = next(p)
while n <= max:
yield n
n = next(p)


# 取出小于100的全部素数
for n in print_prime(100):
print(n)

sorted


类似C++中的sort函数,包含三个参数:

1
sorted(list,key,reverse)
  1. 其中list表示需要排序的序列
  2. key代表排序过程中作用在list中元素上的函数
  3. reverse表示从大到小还是从小到大

比如按绝对值大小排序:

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

上述操作是先对list中的每个元素做了abs操作,再按abs操作后的值排序,再将原本的值放入相应位置。

1
2
3
keys排序结果 => [5, 9,  12,  21, 36]
| | | | |
最终结果 => [5, 9, -12, -21, 36]

返回函数


python中允许将另一个函数作为某一函数的返回值,被返回的函数将不会立即进行运算,例如:

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

f = lazy_sum(1,3,5,7,9)

此时分别调用ff()

1
2
3
4
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>
>>> f()
25

此处内部函数sum可以引用外部函数lazy_sum中的参数和局部变量,当sumlazy_sum返回时,相关参数和变量都保存在返回的函数(sum)中,这种程序结构被成为闭包

需要注意的是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()的调用结果互不影响。

闭包


由闭包的定义,我们来看如下的一个操作:

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


f1,f2,f3 = count()

此时乍一看f1()应该返回1,f2()返回4,f3()返回9

但是实际上结果如下:

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

原因在于,返回函数将会把函数内部的变量封装,而python中的变量均为引用变量,即存储数据的地址,而变量i所存储的地址中的值一直在变化,等到返回第个函数时,i中存储的地址中的值已变为3,此时无论调用哪个返回函数,其中i存储的地址中的值均为3

因此返回闭包时,有如下注意事项:

返回函数不要引用任何循环变量,或者后续会发生变化的变量。

但如上函数并不是没有解决的办法,只需要使用参数来保存循环值即可:

1
2
3
4
5
6
7
8
9
10
def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1,4):
fs.append(f(i))
# 此处f()不是闭包,因此在调用时立即被执行
return fs

使用闭包构造一个计数器:

1
2
3
4
5
6
7
8
9
10
11
12
def createCounter():
i = 0
def counter():
nonlocal i
i += 1
return i
return counter


counterA = createCounter()
print(counterA(), counterA(), counterA(), counterA(), counterA())
# 1 2 3 4 5

番外——变量作用域


python中的变量作用域大致与C++相同,但python为动态语言,定义时无需给出变量的类型,导致定义语法与引用语法相同,因此,python中加入了两个指定变量的关键字globalnonlocal

其中:

  • global关键字用来在函数或其他局部作用域中使用全局变量。
  • nonlocal声明的变量不是局部变量,也不是全局变量,而是外部嵌套函数内的变量。

如,当我们需要在函数中使用全局变量时:

1
2
3
4
5
6
7
8
9
10
count = 0


def global_test():
global count
count += 1
print(count)


global_test() # 1

当我们需要在内部函数中使用外部函数的变量时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def nonlocal_test():
count = 0
def test2():
nonlocal count
count += 1
return count
return test2


val = nonlocal_test()
print(val())
print(val())
print(val())
# 1
# 2
# 3

匿名函数


在函数式变成时,我们经常需要传入函数或是返回函数,这样大量的使用函数将导致命名成为一个非常麻烦的问题,因此python为我们提供了一种匿名函数的机制,省去了为函数命名的麻烦:

1
lambda x: x*x

上面这段函数就相当于:

1
2
def f(x):
return x * x

匿名函数的基本格式为:

lambda argument_list : expression

其中argument_list是参数列表,具有如下特性:

  1. 参数需要在argument_list中有定义
  2. 表达式只能是单行

可以使用如下种种形式:

1
2
3
4
5
6
1
None
a+b
sum(a)
1 if a >10 else 0
......

expression只允许使用一个表达式,且该表达式的值即时该函数的返回值。

lambda函数既可以作为变量赋值给一个变量,也可以作为返回值:

1
2
3
4
5
>>> f = lambda x: x * x
>>> f
<function <lambda> at 0x101c6ef28>
>>> f(5)
25
1
2
def build(x, y):
return lambda: x * x + y * y

关于lambda函数的使用一直存在争议,有些人认为使用lambda函数能使代码更加的pythonic,而有些人认为只能使用一条语句的lambda函数有时反而会降低代码的可读性。

装饰器


首先要明确python中函数作为对象,也具有一些属性,比如:

1
2
3
4
5
6
7
def now():
print('2021-7-26')


f = now
print(f.__name__)
# now

此时,如果我们希望丰富now的功能,但又不希望修改now的定义,我们该怎么办呢?学习过java注解AOP就知道,这就是AOP的编程思维。

在java中这样的操作并不难,通过为函数添加注解,再为该注解编写一些逻辑就能首先。而python中提供了一种使用函数实现AOP的方式:装饰器(Decorator)

本质上,decorator是一个高阶函数。如果我们想要定义一个能打印日志的decorate,可以通过如下方式:

1
2
3
4
5
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

然后只需要使用python提供的@语法糖,把decorate置于函数的定义处:

1
2
3
4
5
6
7
8
@log
def now():
print('2021-7-26')
return None

now()
# call now():
# 2021-7-26

该语法糖相当于执行了:

1
now = log(now)

还能定义需要参数的decorate,实现方式是再如上双重函数嵌套的基础上再增加一层用来接受参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator


@log('execute')
def now():
print('2021-7-26')
return None


now()
# execute now():
# 2021-7-26

该语法糖相当于执行了:

1
now = log('execute')(now)

其中log(‘execute’)返回decorator,接着将decorator(now)赋值给now

但由于为now重新赋值,单纯的该过程将导致如下问题:

1
2
>>> now.__name__
'wrapper'

我们发现now中的值变了,导致now__name__属性变了,因此我们需要把原始函数的__name__等属性赋值到wrapper()函数中去,否则,有些依赖函数签名的代码执行会出错。

Python的functools模块中也为我们提供了一个用于做这些事情的decorator : functools.wraps

1
2
3
4
5
6
7
8
9
import functools


def log(func):
@functools.wraps(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

对于有参数的decorator

1
2
3
4
5
6
7
8
9
10
11
import functools


def log(text):
def decorator(func):
@functools.wraps(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
return decorator

偏函数


Python的functools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)

可以通过偏函数来固定一个函数中的某个参数值,并返回一个新函数,但新函数仍然允许对固定的参数传入其他值。例如int函数:

1
2
3
4
5
6
7
8
9
10
11
import functools


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


int('12345') # 12345
int('12345', 8) # 5349
int('12345', 16) # 74565
int2('1000000') # 64
int2('1000000', base=10) # 1000000

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

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

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

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

再看另一个例子:

1
2
3
4
max2 = functools.partial(max, 10)


max2(5, 6, 7) # 10

相当于:

1
2
args = (10, 5, 6, 7)
max(*args)

结合python入门中函数章节提到的,任何函数都能通过:

1
func(*args, **kw)

来调用,我们可以大胆猜测一下偏函数的参数调用规则:

  1. 偏函数先将默认值中的位置参数和调用时传入的位置参数组合为一个*args,且默认位置参数在前。
  2. 再将默认关键字参数和调用时传入的关键字参数组合为一个**kw,同名关键字将用传入值覆盖默认值。
  3. 最后通过func(*args, **kw)去调用

通过如下例子可以验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import functools


def f(*args, **kw):
print(args)
print(kw)
return None


f1 = functools.partial(f, 1, 2, a = 1, b = 2)


f1(3, 4, c = 3, d = 4)
# (1,2,3,4)
# {'a':1, 'b':2, 'c':3, 'd':4}

f1(a = 2, b = 3)
# (1,2)
# {'a':2, 'b': 3}

评论