Python学习笔记(四)函数式编程

时间:2022-06-16 03:21:20

函数式编程

高阶函数

将函数作为参数传入另一个函数

map/reduce

map

map(函数名,Iterable对象)将函数作用在Iterable对象的每一个元素,返回Iterator。Iterator是惰性的,因此如果要浏览结果需要list(map(…))转化。使用map而非for循环,代码逻辑更清晰,抽象程度更高,可读性强

reduce

结构和map相同,但它是把结果和迭代对象的下一个元素做累计计算,即reduce(f, [x1,x2,x3]) = f(f(x1,x2),x3)。对于形式较为简单的函数,可以使用lambda来简化代码,可以省去def f的部分(见练习3的解答)。

lambda函数是个好东西,C++11新标准也引入了lambda函数。

练习1:规范化英文名,将一个英文名List中的各个元素首字母大写其他字母小写

def normalize(name):
return name.capitalize()#Python中有直接实现str规范化的函数

L = ['lia', 'wR', 'BAR', 'Ra']
print(list(map(normalize, L)))

练习2:求积

from functools import reduce

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

print(prod([1,2,3,4]))

练习3:字符串转化为浮点数

from functools import reduce

def char2num(c):
return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}[c]

def str2float(s):
s1,s2 = s.split('.')#Python提供str的split分割函数实现根据分隔符切片
return reduce(lambda x,y: x*10+y, map(char2num, s1))+reduce(lambda x,y: x/10+y, map(char2num, s2[::-1]))/10

print(str2float('123.456'))

filter

filter()map()类似,也将一个函数作用于一个序列,但会根据返回值剔除元素 。同样地,filter也返回惰性序列,完成计算结果需要list(fitler())

使用埃氏筛选法构造素数生成器:

#先构造从3开始的奇数生成器
def _odd_iter():
n = 1
while True:
n += 2
yield n

#筛选函数
def _not_divisible(n):
return lambda x: x%n>0

#构造素数生成器
def primes():
yield 2
it = _odd_iter() #从3开始的奇数序列
while True:
n = next(it) #序列的第一个数
yield n
it = filter(_not_divisible(n), it) #过滤掉n的倍数,剩下的数组成新的序列

#生成素数
for n in primes():
if n < 1000:
print(n)
else:
break

练习:回数生成器

def is_palindrome(n):
return str(n) == str(n)[::-1] #[::-1]的使用获得了非常简洁的代码,一行搞定
output = filter(is_palindrome, range(1,1000))
print(list(output))

sorted

排序函数sorted(序列, key = 函数, reverse = False),将函数作用于序列之后,根据结果序列,从小到大重排序列。如果reverse = True则反向排列

返回函数

函数也可以作为返回值。

def f1(...):
...
def f2():
...
return f2

f = f1(...)
g = f2(...)

函数返回时并未执行,调用f()时才会计算结果,这样可以在需要计算的时候才计算结果,实现延迟计算。

f2作为f1的内部函数,可以引用外部函数f1的参数和局部变量。f1返回f2时,参数和变量都保存在f2中,这种结构成为闭包Closure。注意每次返回的f2虽然形式相同,即使参数相同也是不同的函数。以上的代码中f和g是不同的。

使用闭包要注意,返回函数不要饮用任何循环变量或者后续会发生变化的变量,否则因为函数返回时并未执行,计算结果都是根据变化变量最新的值计算。如果一定要用到的话,就要再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。这样程序嵌套就变多了,显得臃肿

匿名函数

lambda 变量:表达式实现匿名函数。可以用它赋值给变量来定义函数f = lambda ...,可以作为返回值,可以作为参数传入函数。不过表达式只能有一个,因此支持情形的比较简单

装饰器

使用装饰器可以在不修改函数定义的情况下给函数增加功能。比如下面这个装饰器让函数调用时先打印hello再执行函数:

def log(f):
def wrapper(*args, **kw):
print('hello')
return f(*args, **kw)
return wrapper

@log
def now():
print('now')

now()
  • log(f)返回的是一个函数。

  • @log称为语法糖,出现在函数定义前相当于执行了语句f = log(f),将函数重定向到log(f),从而扩展了f的功能。

  • log中的wrapper引用了log中的变量,因此是个闭包。为什么使用闭包很多人表示疑惑,我也进行了自己的思考和查阅资料。如果不用闭包而使用以下形式:

def log(f):
print('begin')
f()
print('end')
return f

@log
def f1():
...

注意上面的代码log返回的是f本身,则第一次调用f1之后,执行完了log的内容,f1又变成了原来的f1,并不能起到扩展功能的装饰效果。使用闭包以后每次调用f1实际上都在调用wrapper,修饰的内容才算加上去了

还可以使用带参数的装饰器,则要采取以下的形式:

def log(arg):
def dec(f):
def wrap():
...
return f
return warp
return dec

@log(a1)
def f1():
...

此时的log(a1)相当于f1 = log(a1)(f1)log(a1)返回dec函数,因此相当于f1 = dec(f1),不过a1作为参数已经传递进闭包了。

f.__name__可以拿到函数名字。但函数在经过修饰之后名字会变。对于依赖函数签名的代码则需要在构造装饰器的时候传入函数名。这时候需要使用functools模块(用法见下列代码)。这一节的练习题是编写一个既支持有参数(以字符串为例)装饰器又支持无参数装饰器,还是很有难度的,交作业的网友们没有一个的代码可以跑通,在网上搜相关的内容也没有可行的。对这道题我思考了很长的时间,这里给出两个版本的解法,第二版是更符合要求的,但第一版也贴出来进行一些辨析,以加深对装饰器的理解。

#版本1
import functools

def log(text = None):
if isinstance(text, str):
@functools.wraps(text)
def dec(f):
def wrapper():
print('begin')
print('%s %s' %(text, f.__name__))
f()
print('end')
return f
return wrapper
return dec
else:
def d(f):
def wrapper():
print('begin')
f()
print('end')
return f
return wrapper
return d

@log('excute')
def f2():
print('f2 is running')

@log() #这里不符合要求
def f1():
print('f1 is running')

f1()
f1()
f2()
f2()

仔细一看修饰f1的时候log不是标准形式的无参@log,而是@log()。如果使用后者会报错,因为f1 = log()(f1)相当于f1 = d(f1),而@log则是f1 = log(f1),进入else之后变成了f1 = d,没有参数传入d了,因此会报错。显然这一版本是不太符合要求的。

#版本2
import functools

def log(text):
if isinstance(text, str):
@functools.wraps(text)
def dec(f):
def wrapper():
print('begin')
print('%s %s' %(text, f.__name__))
f()
print('end')
return f
return wrapper
return dec
else:
def wrapper():
print('begin')
text()
print('end')
return text
return wrapper

@log
def f1():
print('f1 is running')

@log('excute')
def f2():
print('f2 is running')

f1()
f1()
f2()
f2()

看完版本2之后觉得思路还是比较清晰的,有参的log需要三层嵌套,无参只需要两层嵌套。有参数的部分仍然和版本1一致,无参部分的处理实际上和最开始的例子一样,无非把f换成了text。现在看来真是非常简单,基本上变都不用变,两个组合起来加上判断就行了。之前想了那么长时间,看来还是没有理解装饰器的本质。

偏函数

ff = functools.partial(f, arg = n)将函数f的某个参数arg设为固定值n,构造一个新的函数ff,就是偏函数