python学习笔记函数式编程部分
函数式
函数式编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数是没有变量的,因此,任意一个函数,只要输入确定,输出就是确定的。这种纯函数我们称为没有副作用
为什么说纯函数只要输入确定,输出就能确定呢,因为允许变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入可能得到不同的输出,因此这种函数是有副作用的。
函数式编程的一个特点:
允许函数本身作为参数传入另一个函数,且还允许返回一个函数
需要注意的是,python允许使用变量,因此python不是纯函数式编程语言
高阶函数
python中函数名可以理解为指向函数体的变量,因此,可以将函数名赋值给变量,也可以对函数名重新赋值,例如:
1 | >>> abs |
可见将由于abs = 10
这条语句,导致abs(-10)
不再具有绝对值的功能,此时必须重启python交互环境,abs
才能恢复原有指向。
由此引入高阶函数的概念:
可以接受另一个函数作为参数的函数称为高阶函数
一个简单的高阶函数如下:
1 | def add(x, y, f): |
map/reduce
map
map()
函数接收两个参数,一个是函数,一个是Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator
返回。
例如:
1 | def f(x): |
由于map函数将返回惰性加载的Interator
,因此此处使用list()
将其转化为list
虽然如上简单操作看起来并没有什么意义,但我们还能使用map完成如下有实际意义的操作:
利用map()
函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:['adam', 'LISA', 'barT']
,输出:['Adam', 'Lisa', 'Bart']
:
1 | # -*- coding: utf-8 -*- |
reduce
reduce
把一个函数作用在一个序列`[x1
效果类似:
reduce(f
使用reduce弄够完成如下事件:
1 | # -*- coding: utf-8 -*- |
使用map与reduce的组合,可以完成如下复杂的操作:
利用map
和reduce
编写一个str2float
函数,把字符串'123.456'
转换成浮点数123.456
:
1 | # -*- coding: utf-8 -*- |
filter
filter
机制与map
有些相似,都是将将传入的函数作用在传入可便利对象中的每个元素上,区别在于,fliter
是根据再用在该元素上的函数返回值是true
还是false
,从而决定该函数是否保留,最后返回所有被保留的元素组成的list
例如一个筛出奇数的选择器:
1 | def is_odd(n): |
filter
与map
一样,但会的是一个惰性加载的Iterator
,需要使用list()
filter应用
接下来我们利用Iterator
、filter
实现埃氏筛法:
首先对该问题进行分解:
- 除了2以外的偶数都是和数,因此我们只需要考虑奇数,为此,我们需要先创建一个能够不断产生奇数的
Iterator
- 创建一个用来筛选的函数
- 接下来我们需要再创建一个函数,利用
filter
筛去1中的一些奇和数。 - 最后打印
产生奇数
1 | def _odd_iterator(): |
构造一个能不断产生奇数的Iterator
筛选函数
1 | def primes_filter(n): |
使用n筛去n的倍数,由于filter
的第一个参数需要接受一个函数,我们可以再此处返回一个lambda
函数,使这个函数既可以接受参数又可以作为filter
的第一个参数
素数生成器
1 | # -*- coding: utf-8 -*- |
sorted
类似C++中的sort
函数,包含三个参数:
1 | sorted(list,key,reverse) |
- 其中list表示需要排序的序列
- key代表排序过程中作用在list中元素上的函数
- reverse表示从大到小还是从小到大
比如按绝对值大小排序:
1 | >>> sorted([36, 5, -12, 9, -21], key=abs) |
上述操作是先对list中的每个元素做了abs操作,再按abs操作后的值排序,再将原本的值放入相应位置。
1 | keys排序结果 => [5, 9, 12, 21, 36] |
返回函数
python中允许将另一个函数作为某一函数的返回值,被返回的函数将不会立即进行运算,例如:
1 | def lazy_sum(*args): |
此时分别调用f
与f()
:
1 | >>> f |
此处内部函数sum
可以引用外部函数lazy_sum
中的参数和局部变量,当sum
被lazy_sum
返回时,相关参数和变量都保存在返回的函数(sum)中,这种程序结构被成为闭包
需要注意的是lazy_sum
每次都会返回一个新的函数,即:
1 | >>> f1 = lazy_sum(1, 3, 5, 7, 9) |
f1()
和f2()
的调用结果互不影响。
闭包
由闭包的定义,我们来看如下的一个操作:
1 | def count(): |
此时乍一看f1()
应该返回1,f2()
返回4,f3()
返回9
但是实际上结果如下:
1 | >>> f1, f2, f3 = count() |
原因在于,返回函数将会把函数内部的变量封装,而python中的变量均为引用变量,即存储数据的地址,而变量i
所存储的地址中的值一直在变化,等到返回第个函数时,i
中存储的地址中的值已变为3,此时无论调用哪个返回函数,其中i
存储的地址中的值均为3
因此返回闭包时,有如下注意事项:
返回函数不要引用任何循环变量,或者后续会发生变化的变量。
但如上函数并不是没有解决的办法,只需要使用参数来保存循环值即可:
1 | def count(): |
使用闭包构造一个计数器:
1 | def createCounter(): |
番外——变量作用域
python中的变量作用域大致与C++相同,但python为动态语言,定义时无需给出变量的类型,导致定义语法与引用语法相同,因此,python中加入了两个指定变量的关键字global
和nonlocal
其中:
- global关键字用来在函数或其他局部作用域中使用全局变量。
- nonlocal声明的变量不是局部变量,也不是全局变量,而是外部嵌套函数内的变量。
如,当我们需要在函数中使用全局变量时:
1 | count = 0 |
当我们需要在内部函数中使用外部函数的变量时:
1 | def nonlocal_test(): |
匿名函数
在函数式变成时,我们经常需要传入函数或是返回函数,这样大量的使用函数将导致命名成为一个非常麻烦的问题,因此python为我们提供了一种匿名函数的机制,省去了为函数命名的麻烦:
1 | lambda x: x*x |
上面这段函数就相当于:
1 | def f(x): |
匿名函数的基本格式为:
lambda argument_list : expression
其中argument_list
是参数列表,具有如下特性:
- 参数需要在argument_list中有定义
- 表达式只能是单行
可以使用如下种种形式:
1 | 1 |
而expression
只允许使用一个表达式,且该表达式的值即时该函数的返回值。
lambda函数既可以作为变量赋值给一个变量,也可以作为返回值:
1 | >>> f = lambda x: x * x |
1 | def build(x, y): |
关于lambda函数的使用一直存在争议,有些人认为使用lambda函数能使代码更加的pythonic,而有些人认为只能使用一条语句的lambda函数有时反而会降低代码的可读性。
装饰器
首先要明确python中函数作为对象,也具有一些属性,比如:
1 | def now(): |
此时,如果我们希望丰富now的功能,但又不希望修改now的定义,我们该怎么办呢?学习过java注解
和AOP
就知道,这就是AOP的编程思维。
在java中这样的操作并不难,通过为函数添加注解,再为该注解编写一些逻辑就能首先。而python中提供了一种使用函数实现AOP的方式:装饰器(Decorator)
本质上,decorator
是一个高阶函数
。如果我们想要定义一个能打印日志的decorate,可以通过如下方式:
1 | def log(func): |
然后只需要使用python提供的@语法糖
,把decorate置于函数的定义处:
1 |
|
该语法糖相当于执行了:
1 | now = log(now) |
还能定义需要参数的decorate
,实现方式是再如上双重函数嵌套的基础上再增加一层用来接受参数:
1 | def log(text): |
该语法糖相当于执行了:
1 | now = log('execute')(now) |
其中log(‘execute’)
返回decorator
,接着将decorator(now)
赋值给now
但由于为now
重新赋值,单纯的该过程将导致如下问题:
1 | now.__name__ |
我们发现now
中的值变了,导致now
的__name__
属性变了,因此我们需要把原始函数的__name__
等属性赋值到wrapper()
函数中去,否则,有些依赖函数签名的代码执行会出错。
Python的functools
模块中也为我们提供了一个用于做这些事情的decorator
: functools.wraps
1 | import functools |
对于有参数的decorator
:
1 | import functools |
偏函数
Python的functools
模块提供了很多有用的功能,其中一个就是偏函数(Partial function)
可以通过偏函数来固定一个函数中的某个参数值,并返回一个新函数,但新函数仍然允许对固定的参数传入其他值。例如int
函数:
1 | import functools |
创建偏函数时,实际上可以接收函数对象、*args
和**kw
这3个参数,当传入:
1 | int2 = functools.partial(int, base=2) |
实际上固定了int()
函数的关键字参数base
,也就是:
1 | kw = { 'base': 2 } |
再看另一个例子:
1 | max2 = functools.partial(max, 10) |
相当于:
1 | args = (10, 5, 6, 7) |
结合python入门中函数章节提到的,任何函数都能通过:
1 | func(*args, **kw) |
来调用,我们可以大胆猜测一下偏函数的参数调用规则:
- 偏函数先将默认值中的
位置参数
和调用时传入的位置参数
组合为一个*args
,且默认位置参数在前。 - 再将默认
关键字参数
和调用时传入的关键字参数
组合为一个**kw
,同名关键字将用传入值覆盖默认值。 - 最后通过
func(*args, **kw)
去调用
通过如下例子可以验证:
1 | import functools |