廖雪峰Python教程阅读笔记——4. 函数式编程

来源:互联网 发布:java cms管理系统 编辑:程序博客网 时间:2024/06/03 15:01

4函数式编程

函数是面向过程的程序设计的基本基元。而函数式编程-Function Programming,虽然其接近于面向过程的编程,但是更多的是偏于数学计算。

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

函数式编程的一个特点就是:允许把函数作为一个参数传入另外一个函数,还允许返回一个函数

Python对函数式编程提供部分支持,由于Python允许使用变量,因此,Python不是纯粹的函数式编程。

4.1高阶函数

高阶函数-Higher-order function。

  • 变量可以指向函数**

    以Python的绝对值函数abs()为例:

    >>> abs(-10)10

    但是如果写成:

    >>> abs<built-in function abs>

    可见abs(-10)是函数调用,而abs是函数本身。

    要获得函数的调用,我们可以把结果赋值给变量:

    >>> x = abs(-10)>>> x10

    但是如果将函数赋值给一个变量:

    >>> f=abs>>> f(-10)10

    结论:函数本身可以赋值给变量,即:变量可以指向函数。

    • 函数名也是变量

    函数名其实就是名副其实的指向函数的变量。当把函数名abs指向其他对象:

    >>> 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)调用该函数了。

    注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其他模块生效,要用import builtins;builtins.abs = 10

    • 传入函数

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数

def add(x,y,f):   return f(x) + f(y)

例如,调用add(-10,3,abs)

>>>add(-10,3,abs)13

编写高阶函数就是让函数能接收函数作为参数。

小结:把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

4.1.1 map/reduce

Python内建了map()reduce()函数。

  • map()函数

    map()函数接收两个参数,一个参数是函数,一个参数是Iterable,map()将传入的函数依次作用于Iterable对象的每个元素,结果返回一个Iterator。例如:

>>> def f(x):  ... return x*x>>> r = map(fun,[1,2,3,4])>>> r<map object at 0x00000222A759E780>  ##注意,r是一个惰性的序列,无法一次性输出,必须转换成list类型>>> next(r)1>>> next(r)4>>> next(r)9>>> list(r) ##r的指向改变了,当使用多次next()函数,r指向的不再是原来的Iterator对象。而是类似于栈帧发生位移[16]>>> list(r)[]>>> list(r)[]>>> r = map(fun,[1,2,3,4])>>> list(r)[1, 4, 9, 16]>>>

map()函数作为高阶函数,可以计算任意复杂的函数,比如把list转换为一个字符串:

>>> r1 = map(str,[1,2,3,4,5])>>> list(r1)['1', '2', '3', '4', '5']
  • reduce()函数

reduce()接收两个参数,一个是*函数*f,一个是Iterable对象。函数必须接受两个参数,reduce函数每次会把上次函数允许的结果和Iterable对象的下一个元素传入到函数f中。直到Iterable中所有的元素都参与过运算。

>>>from functools import reduce>>>def testadd(x,y):    return x+y>>> reduce(testadd,[1,3,5])9

再来看一个复杂的例子:把字符串”13579”转换成整数:

def str2num(s):    def fn(x,y):        return x*10+y    def char2int(s):        return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'0':0}[s]    return reduce(fn,map(char2int,s))

运行结果:

>>> str2num('222')222

可以使用lambda函数进一步简化:

def str2num2(s):    def char2int(s):        return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9,'0':0}[s]    return reduce(lambda x,y:x*10+y,map(char2ints,s))

4.1.2filter

Python内建了filter函数用于过滤序列。

map类似,filter函数也可以接收一个函数和一个序列。和map()不同的是filter()函数把函数参数作用于序列的每一个元素,判断该元素是否满足函数参数,根据函数参数返回的TrueFalse来决定该元素的去和留

例如:过滤掉序列中的空字符串

>>>def Not_empty(s):...  return s and s.strip()>>>list(filter(Not_empty,['A','B','SSSS','    ','AAAAA',None]))['A','B','AAAAA']

filter()的作用是从一个序列中筛出符合条件元素。使用了惰性计算,所以只有在取filter()结果的时候,才会真正的筛选并每次返回下一个筛除的元素。

4.1.3 sorted

  • 排序算法

    Python内置的函数sorted()就可以对list进行排序。

    >>> sorted([10,-9,3,93])[-9, 3, 10, 93]

sorted()函数也是一个高阶函数,他可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

>>> sorted([10,-9,3,93],key=abs)[3, -9, 10, 93]

key函数作用于序列的每一个元素,并将结果排序。

对于字符串排序,是按照ASCII的大小进行排序。由于’Z’<’a’,如果我们忽略大小写,对字符串进行排序。如果使用key函数,把字符串映射为忽略大小写排序即可。

忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较:

>>> sorted(['Asss','ass','ZXZ','bs'])   ##字符串排序['Asss', 'ZXZ', 'ass', 'bs']>>> sorted(['Asss','ass','ZXZ','bs'],key=str.lower)   ##忽略大小写,排序['ass', 'Asss', 'bs', 'ZXZ']

4.2返回函数

函数作为返回值

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

例如,实现一个可变参数的求和

def lazy_sum(*args):    def sum():        ax = 0        for n in args:            ax = ax + n        return ax    return sum

当我们调用lazy_sum()函数时,返回的并不是求和的结果,而是求和函数。调用返回值时才算是真正的求和。

>>> from test1 import lazy_sum>>> f = lazy_sum(1,2,3,4)>>> f<function lazy_sum.<locals>.sum at 0x000001B6A3BD3E18>>>> f()10

在这个例子中,我们在函数lazy_sum()中定义了一个函数sum()内部函数可以引用外部函数lazy_sum()参数局部变量,当lazy_sum()函数返回函数sum时,相关参数变量都保存在返回的函数中,这种称为:闭包(Closure)的程序结构拥有极大的威力

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

>>> f2 = lazy_sum(1,2,3,4)>>> f3 = lazy_sum(1,2,3,4)>>> f2==f3False
  • 闭包

    注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回一个函数后,其内部的局部变量还被函数引用。所以,闭包使用起来方便,实现起来可不容易。
    返回闭包时要牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

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

4.3 匿名函数

当我们传入函数时,有些时候,不需要显示的定义函数,只需要传入匿名函数更方便。在Python中,对匿名函数提供了有限支持。
map()函数为例,计算f(x) = x^2。我们使用匿名函数:

>>> list(map(lambda x:x*x,[1,2,3,4,5,6,7,8]))[1, 4, 9, 16, 25, 36, 49, 64]

关键字lambda表示匿名函数,冒号前面的x表示函数参数。
匿名函数有个限制,就是只能有一个表达式,不用写return语句,返回值就是该表达式的结果。
匿名函数也是一个函数对象,可以把匿名函数赋值给一个变量,再利用该变量来调用函数。

>>> f=lambda x:x*x>>> f(3)9

4.4 装饰器

函数对象有个_name_属性,可以拿到函数的名字。
假如有一个函数,打印当前时间:

import time  ##yindef now():  print(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())

现在假如我们要增强now()函数的功能,在函数调用前后都自动打印日志,但又不希望更改函数now()的定义,这种在代码运行时动态增加功能的方式,称之为“装饰器”(Decorator)

本质上,Decorator就是一个返回函数的高阶函数。我们如果要定义一个可以打印日志的“装饰器”:

def log(func):    def wrapper(*args,**kw):        print("call %s():" % func._name_)        return func(*args,**kw)    return wrapper

log()函数,是一个decorator,可以接收一个函数作为参数。我们可以借助Python的@语法,把decorator置于函数的定义处:

@logdef now():  print(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())

调用now()函数,不仅会允许now()函数本身,还会在运行now()函数前打印一行日志。
由于log()函数,是一个decorator,返回一个函数,因此原来now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()函数将执行新的函数,即log()函数中返回wrapper(*args,**kw)函数。
wrapper(*args,**kw)函数的参数定义是(*args,**kw),因此,该函数可以接受任意参数的调用。在wrapper(*args,**kw)函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那么就需要编写一个返回decorator的高阶函数,写出来会更复杂:

def log(text):  def decorator(func):    def wrapper(*args,**kw):        print("call %s():" % func._name_)        return func(*args,**kw)    return wrapper  return decorator

这个三层嵌套的decorator用法如下:

@log('execute')def now():  print(time.strftime("%Y-%m-%d %H:%M:%S",time.localtime())

执行结果如下:

>>>now()execute now():2017-8-25

小结:
在面向对象(OOP)的设计模式中,decorator被称为装饰模式,OOP的装饰模式需要通过继承组合来实现,而Python除了支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数来实现,也可以用类来实现。
decorator可以增强函数的功能,定义起来稍微有些复杂,但是用的时候非常灵活方便。

4.5偏函数

Python中的functools模块,提供了许多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学意义上的不一样。
Python中的int()函数可以将字符串转换为整数。默认是十进制的整数,int()函数还提供额外的base参数,默认为10:

>>> int('123')                                                                                                                              123                                                                                                                                         >>> int('123',base=8)                                                                                                                       83                                                                                                                                          >>> int('123',base=2)                                                                                                                       Traceback (most recent call last):                                                                                                            File "<stdin>", line 1, in <module>                                                                                                       ValueError: invalid literal for int() with base 2: '123'                                                                                    >>> int('100100',base=2)                                                                                                                    36

假设要转换大量的二进制函数,每次都要传入int('123',base=2),非常麻烦。可以定义一个默认参数的函数:

def int2(_str,base=2):    return int(_str,base)

如此,我们转换二进制参数就非常方便了,但是代码量还是非常不方便。我们使用functools.partial,用来创建一个偏函数:

>>> import functools                                                                                                                        >>> int2 = functools.partial(int,base=2)                                                                                                    >>> int2('10000100')                                                                                                                        132

只需要一行代码,无需重新定义一个函数,上面的代码int2是偏函数名。functools.partial()的第一个参数是函数,第二个参数是函数的默认参数。返回值为一个新函数。
该函数仅仅将base=2作为默认函数,也可以使用其他值:

>>>int2('2221111',base=10)

偏函数可以接收函数对象、*args**kw这三个参数。
小结:
当函数参数过多,需要简化时,使用functools.partial()可以创建一个新函数,这个新函数就可以固定原函数的部分参数,从而在调用时更简单。

原创粉丝点击