廖雪峰Python的研读笔记(二) 函数式编程

前言

以前知道廖前辈的这个网站,但是今天是第一次拜读,我是通过《Python核心编程(第三版)》这本书入门Python的,感觉廖前辈的教程更加容易,总结的也很到位。下面记录的是此番学习Python时带给我的领悟,以及一些值得关注的东西。我将它们写入博客《廖雪峰Python的研读笔记》系列。

函数式编程

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

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

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

高阶函数

高阶函数英文叫Higher-order function。一个函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。

map/reduce

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

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

>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。

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

这里给出一个综合的例子,使用map/reduce实现类似int()函数的功能:

from functools import reduce

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

练习

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

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

def normalize(name):
    L = name[0].upper()
    L = L + name[1:].lower()
    return L


if __name__ == '__main__':

    L1 = ['adam', 'LISA', 'barT']
    L2 = list(map(normalize, L1))
    print(L2)
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

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

def prod(L):

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


if __name__ == '__main__':

    print('3 * 5 * 7 * 9 =', prod([3, 5, 7, 9]))
    print('3 * 5 =', prod([3, 5]))
    print('3 =', prod([3]))
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

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

def str2float(s):

    CH2INT = {
        '1' : 1,
        '2' : 2,
        '3' : 3,
        '4' : 4,
        '5' : 5,
        '6' : 6,
        '7' : 7,
        '8' : 8,
        '9' : 9,
        '0' : 0,
        '.' : -1,
    }

    str2float.point_factor = 0  # Python2中使用这样的方式实现nonlocal功能

    def to_float(a, b):

        if a == -1:         # 特殊处理以'.'开头的字符串
            str2float.point_factor = 0.1
            return b * str2float.point_factor

        if b == -1:         # 处理过程中遇到'.'的情况
            str2float.point_factor = 1
            return a

        if str2float.point_factor == 0:
            return a * 10 + b   # 整数部分处理方法
        else:
            str2float.point_factor = str2float.point_factor * 0.1
            return a + b * str2float.point_factor 

    return reduce(to_float, map(lambda x: CH2INT[x], s))


if __name__ == '__main__':

    TEST_LIST = [
        '0',
        '123.456',
        '123.456000',
        '0.1234',
        '.1234',
        '120.0034',
        ]

    for i in TEST_LIST:
        print 'str2float(\'%s\') = %s' % (i, str2float(i))

filter

Python内建的filter()函数用于过滤序列。filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

filter()函数返回的是一个Iterator,使用filter()这个高阶函数,关键在于正确实现一个“筛选”函数。

练习

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

'''
回数是指从左向右读和从右向左读都是一样的数,例如12321,909。请利用filter()滤掉非回数。
'''

def is_palindrome(n):

    s = str(n)
    l = len(s)

    i = 0
    while i < l / 2:
        if s[i] != s[-1 - i]:
            return False
        i = i + 1
    return True


if __name__ == '__main__':

    output = filter(is_palindrome, range(1, 10000))
    print(list(output))

sorted

Python内置的sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序。

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

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

高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

由此可见,用sorted()排序的关键在于实现一个映射函数。

练习

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

'''
假设我们用一组tuple表示学生名字和成绩:
L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]

请用sorted()对上述列表分别按名字排序,再按成绩从高到低排序。
'''

if __name__ == '__main__':

    L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
    print sorted(L, key = lambda x : x[0])
    print sorted(L, key = lambda x : x[1], reverse = True)

返回函数

函数作为返回值

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

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

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

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
.sum at 0x101c6ed90>

>>> f()
25

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

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

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

f1()f2()的调用结果互不影响。

闭包

注意到返回的函数在其定义内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来可不容易。

返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

小结

一个函数可以返回一个计算结果,也可以返回一个函数。

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

匿名函数

当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便,且不用担心函数名冲突。

关键字lambda表示匿名函数,冒号前面的部分表示函数参数,后面的部分表示计算公式,也就是函数体。

此外,lambda表达式返回一个函数,匿名函数也是函数对象,可以将其保存在变量中或作为返回值返回:

>>> f = lambda x: x * x
>>> f
lambda> at 0x101c6ef28>
>>> f(5)
25
def build(x, y):
    return lambda: x * x + y * y

小结

Python对匿名函数的支持有限,只有一些简单的情况下可以使用匿名函数。

装饰器

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

不带参数的装饰器

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

def log(func):
    def wrapper(*args, **kw):
        print('start calling %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

@log
def my_add(x, y):
    return x + y

@log放到my_add()函数的定义处,相当于执行了语句my_add = log(my_add)

由于log()是一个decorator,返回一个函数,所以,原来的my_add()函数仍然存在,只是现在同名的my_add变量指向了新的函数,于是调用my_add()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

带参数的装饰器

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

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

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

@log('start executing')
def my_add(x, y):
    return x + y

和两层嵌套的decorator相比,3层嵌套相当于my_add = log('start executing')(my_add)

剖析上面的语句,首先执行log('start executing'),返回的是decorator函数,再调用返回的函数,参数是my_add函数,返回值最终是wrapper函数。

执行结果:

>>> my_add(1, 2)
start executing my_add():
3

还原函数名称

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的’my_add’变成了’wrapper’:

>>> my_add.__name__
'wrapper'

因为返回的那个wrapper()函数名字就是’wrapper’,所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的。

所以,一个完整的decorator的写法如下:

import functools

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

或者针对带参数的decorator

import functools

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

小结

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。

练习

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

'''
请编写一个decorator,能在函数调用的前后打印出'begin call'和'end call'的日志,并计算函数执行时间。
'''

import time
from functools import wraps
import random 

def fn_timer(func):
    @wraps(func)
    def func_timer(*args, **kwargs):
        func_name = func.__name__
        print 'begin call %s()' % func_name
        t0 = time.time()
        result = func(*args, **kwargs)
        t1 = time.time()
        print 'end call %s()' % func_name
        print "Total time running %s: %s seconds" % (func_name, str(t1-t0))
        return result
    return func_timer


@fn_timer
def random_sort(n):
    return sorted([random.random() for i in range(n)])


if __name__ == "__main__":
    random_sort(2000000)

偏函数

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

functools.partial可以帮我们创建一个偏函数的,从而把一个函数的某些参数固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64
>>> int2('1010101')
85

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

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

对于可以接收*args参数的函数来说,比如max,当我们使用max2 = functools.partial(max, 10)定义新函数max2后,10会作为*args的一部分自动加到左边,这样当调用max2(5, 6, 7)时,函数将返回最大值10

小结

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

你可能感兴趣的:(Python)