Haste makes waste

Python-Liao-03-【重要】高阶函数-Map/Reduce-闭包-匿名函数-装饰器-偏函数

Posted on By lijun

[Python-Liao-XX…]系列,系列根据廖雪峰的python3初级教程学习整理。

notebook

24. 函数式编程

函数式编程的一个特点,就是函数可以作为变量参数,传递给另外的函数,也可以作为函数的返回值,传递到调用它的函数。

abs(-10)
输出:10
abs
输出:<function abs>
x = abs
x(-10)
输出:10
x
输出:<function abs>

从上面的代码可以看到,函数名就像一个变量,可以赋值给其他变量,然后其他变量就具有了函数相同的功能,该变量最后指向了函数。

所以,完全可以将函数名看成是变量,该变量来指向那个函数体对象。

如果将abs这个预定义的函数名指向其他值,比如 abs = sqrt,那么abs的功能就会被改变。所以不能用预定义的关键字作为变量名。

import math
abs = math.sqrt
abs(4)
输出:2.0

要恢复上面的abs函数功能,只能重启Python的交互环境。

25. 高阶函数

f = abs
f(-1)
输出:1
f is abs
输出:True

结论: 函数本身也可以赋值给变量,变量可以指向函数。 从上面的代码可以看到,函数名就像一个变量,可以赋值给其他变量,然后其他变量就具有了函数相同的功能,该变量最后指向了函数。 所以,完全可以将函数名看成是变量,该变量指向那个函数体对象。

25.1. 传入函数

既然函数名能作为变量,那么函数名A,也就可以作为另外一个函数B的参数,函数B就叫做高阶函数

def myfunc(a,b,f):
    return f(a,b)
def add(a,b):
    return a + b
def plus(a,b):
    return a*b
myfunc(2,3,add)
输出:5
myfunc(3,4,plus)
输出:12

编写高阶函数,就是让函数的参数能够接收别的函数。 把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

26. Map/Reduce

通过这种方式,将操作函数当作参数,就可以实现同一个接口函数,接收不同的函数名,实现不同的操作,增加了函数的灵活性。

Map/Reduce都是高阶函数,能够在参数中接收其他函数名。

26.1 map

map函数接收两个参数,第一个是函数名,第二个是迭代器,比如 f(x) = x**2, lista = [1,2,3,4,5,6] r = map(f,lista)

def f(x):
    return x**2
    
lista = [1,2,3,4,5,6]
r = map(f,lista)
list(r)
输出:[1, 4, 9, 16, 25, 36]

map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x**2 ,还可以计算任意复杂的函数,比如,把这个 list 所有数字转为字符串:

list(map(str,[1,2,3,4,5,6,7]))
输出:['1', '2', '3', '4', '5', '6', '7']

26.2 reduce

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

其效果就是: reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

from functools import reduce
def add(a,b):
    return a+b
reduce(add,[1,2,3,4,5,6])
输出:21

1+2,和为3,3+3,和为6,6+4,和为10…,最后为21.

如果要把序列[1, 3, 5, 7, 9]变换成整数 13579,reduce 就可以派上 用场:

def f1(a,b):
    return 10*a+b
reduce(f1,[1,2,3,4,5])
输出:12345
def char2Num(a):
    return {"0":0,"1":1,"2":2,"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9}[a]
reduce(f1,map(char2Num,"15678945"))
输出:15678945

整理成一个函数就是:

def str2int(s):
    def f1(a,b):
        return 10*a+b
    def char2Num(a):
        return {"0":0,"1":1,"2":2,"3":3,"4":4,"5":5,"6":6,"7":7,"8":8,"9":9}[a]
    return reduce(f1,map(char2Num,s))
str2int("1234563443")
输出:1234563443

通过lambda表达式,使代码更加简洁。这里lambda表达式就是一个没有函数名的函数,之前传入的函数名,实际也是指向一个函数对象的。

def str2int2(s): 
    return reduce(lambda x, y: x * 10 + y, map(char2Num, s))
str2int2("89765")
输出:89765

26.3 练习

  • 将名字的第一个字母大写,后续全部小写
def normalize(name): 
    str_len = len(name)
    return name[0].upper()+name[1:str_len].lower()
L1 = ['adam', 'LISA', 'barT'] 
L2 = list(map(normalize, L1))
print(L2) 
['Adam', 'Lisa', 'Bart']
  • 求几个数的积
def prod(L):
    return reduce(lambda a,b:a*b,L)
    pass
print("积:", prod([1, 2, 3, 4])) 
积: 24
  • 利用 map 和 reduce 编写一个 str2float 函数,把字符串’123.456’转换成 浮点数 123.456:
def str2float(s): 
    pass
print('str2float(\'123.456\') =', str2float('123.456')) 
输出:str2float('123.456') = None

27. filter

Python 内建的 filter()函数用于过滤序列。

与map类似,也是接收两个参数,前者函数名,后数数列,但是与map不同的是,根据函数的返回值是True还是Flase,确定是否丢弃该元素。与map一样,都是独立作用与单个元素。

在R中以及MongoDB中都有类似的概念,将一个规则作用域一个数据集,然后将过滤后的数据集返回。

比如R中的 subset(statesInfo,state.region == 1)

def is_odd(a):
    return a%2== 1
list(filter(is_odd,[1,2,3,4,4,6,7,7,8]))
输出:[1, 3, 7, 7]
def not_empty(s): 
    return s and s.strip()

image

# and - 如果前面是True,则需要进一步计算,即将and 后面的返回;如果前面是false,则直接返回
print( True and "999")
print( False and "999")

# or - 如果前面是True,则直接返回前面,如果前面是false,则需要进一步计算,即将or后面的返回
print( True or "999")
print( False or "999")
输出:999
	False
	True
	999
# 需要经过and 后面的strip处理
print(not_empty(' A'))
输出:A
list(filter(not_empty, [' A', '', 'B', None, 'C', '  '])) 
输出:[' A', 'B', 'C']

27.1 用filter求素数

  • 定义一个序列生成器,无限的
def _odd_iter():
    n = 1
    while True:
        n = n + 2
        yield n
  • 定义一个筛选函数
def _not_divisible(n):
    return lambda x:x % n > 0
  • 定义一个生成器,不断返回下一个素数
def primes():
    yield 2 # 默认返回2这个素数
    yield 4
    yield 6
    yield 8
    it = _odd_iter()
    while True:
        n = next(it)
        yield n
        it = filter(_not_divisible(n), it) # 构造新序列 

由于 primes()也是一个无限序列,所以调用时需要设置一个退出循环的

# 打印 30 以内的素数: 
for n in primes(): 
    if n < 30: 
        print(n) 
    else: 
        break 
输出:2
    4
    6
    8
    3
    5
    7
    11
    13
    17
    19
    23
    29

27.2 练习 - 打印回数(左→右 与 右→左 相同)

# and的用法,如果是true则返回and后面的,否则返回前面的false
def is_palindrome(n): 
    strN = str(n)
    strN_right = ""
    for i in range(len(strN)):
        strN_right += strN[len(strN)-i-1]
    return strN_right == strN and strN_right

# 测试: 
output = filter(is_palindrome, range(1, 100)) 
print(list(output)) 
输出:[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99]

28. Sorted 排序算法

sorted([1,2,3,4,-342,3,3,2,4])
输出:[-342, 1, 2, 2, 3, 3, 3, 4, 4]

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

sorted([1,2,3,4,-342,3,3,2,4],key=abs)
输出:[1, 2, 2, 3, 3, 3, 4, 4, -342]

按照ASCII码排序

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()也是一个高阶函数。用 sorted()排序的关键在于实现一个映射函数。

29. 返回函数

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

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

f1 = lazy_sum(3,4,5,6)
f1
输出:<function __main__.lazy_sum.<locals>.sum>

调用函数 f 时,才真正计算求和的结果:

f1()
输出:18

我们在函数lazy_sum中,再次定义了函数sum,这个内部的函数sum可以引用外部函数内部的变量,以及参数,相关参数和变量都保存在返回的函数中。

这种成为闭包的程序结构有极大的威力。

29.1 闭包

def count(): 
    fs = [] 
    print("count()执行")
    for i in range(1, 4): 
        def f(): 
            print("i执行:{}".format(i))
            return i*i 
        fs.append(f)
    return fs 

f1, f2, f3 = count() 
count()执行

fs,即函数count()的返回值,是一个包含指向函数f的变量的list。

print("result:{}".format(f1()))
print("result:{}".format(f2()))
print("result:{}".format(f3()))
i执行:3
result:9
i执行:3
result:9
i执行:3
result:9

可以看到上面,f1/f2/f3的执行结果都是相同的,原因就在于返回值引用了变量,但函数f()并非立刻执行,但这时i已经变成3,因此最终结果为9. 所以要特别注意:”返回函数f()不要带任何变量”。

如果一定要引用循环变量怎么办?方法是再创建一个函数, 用该函数的 参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到 函数参数的值不变:

def count(): 
    print("count()执行")
    def f(j): 
        def g(): 
            return j*j 
        return g 
    fs = [] 
    for i in range(1, 4): 
        print("i执行:{}".format(i))
        fs.append(f(i)) # f(i)立刻被执行,因此 i 的当前值被传入 f() 
    return fs
        
f1, f2, f3 = count() 
count()执行
i执行:1
i执行:2
i执行:3
print("result:{}".format(f1()))
print("result:{}".format(f2()))
print("result:{}".format(f3()))
result:1
result:4
result:9
  • 关于变量的作用域
if 1 == 1:
    name = "lzl" 
print(name)
 
 
for i in range(10):
    age = i
 
print(age)
输出:lzl
		9

这.....太诡异了吧,python中的函数块中的变量,在结束了函数块之后,居然还能访问,没有被释放!!! 参考下下面的文章!!

代码执行成功,没有问题;在Java/C#中,执行上面的代码会提示name,age没有定义,而在Python中可以执行成功,这是因为在Python中是没有块级作用域的,代码块里的变量,外部可以调用,所以可运行成功;

几个概念:

  1. python能够改变变量作用域的代码段是def、class、lamda.
  2. if/elif/else、try/except/finally、for/while 并不能涉及变量作用域的更改,也就是说他们的代码块中的变量,在外部也是可以访问的
  3. 变量搜索路径是:本地变量->全局变量
li = [lambda :x for x in range(10)]

30. 匿名函数

有时我们将一个函数作为参数,传入另一个函数时,有时候不需要显式的定义一个函数,直接传入一个匿名函数更加方便。如下的两种方式:

  • 传统的函数定义方式
def myfunc(x):
    return x*x

list(map(myfunc,[1,2,3,4,5]))
输出:[1, 4, 9, 16, 25]
  • 直接使用lambda表达式
list(map(lambda x: x*x,[1,2,3,4,5]))
输出:[1, 4, 9, 16, 25]

lambda表达式有个限制就是只能返回一个表达式,但是因为没有函数名,所以没有函数名冲突的问题存在,而且非常简洁。

也可以直接将lambda表达式作为一个函数对象,将其赋值给其他变量,比如:

f = lambda x:x*x
f
输出:<function __main__.<lambda>>
f(5)
输出:25

也可以把lambda表达式作为返回值返回:

def build(x,y):
    return lambda x,y:x*x + y*y
f = build(0,0)
f(4,7)
输出:65

31. 装饰器

关于装饰器更深的理解,参考如下链接

简单地理解Python的装饰器

如何理解Python装饰器?

PYTHON修饰器的函数式编程

装饰器

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志,性能检测,事务处理,缓存,权限校验等场景。 装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能无关的雷同代码并继续重用。 概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

比如下面的代码,有一个专门打印log的装饰器,在一般函数前添加@log,即可以给该函数添加一个装饰器。

如果下面的代码,用通常的方法去实现的话,可能要在一般函数中添加打印log的代码了,这样即会导致大量的代码重复,也使得函数功能划分不够清晰。

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

    return wrapper

@log
def now():
  print(time.time())
now()
输出:call now():
1509841556.247622

上面将@log放在now()的前面,与下面now = log(now)效果一样,都是将now函数变量指向了一个新的函数块。

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

    return wrapper

def now():
  print(time.time())
now = log(now)
now()
输出:call now():
1509841556.267676

自己也总结了一个关于装饰器的专题,参考here

32. 偏函数

Python中确实有非常多贴心的功能,不掌握这些用法就太亏了,下面的偏函数就是其中一种。

通过设定函数参数的默认值,可以降低调用函数的难度,比如

int(“12345”) , int函数其实也存在默认值,该函数等同于 int(“12345”,base=10),即默认值为转换为10进制。

int("12345",base=8)
输出:5349
int("12345",base=16)
输出:74565

但是如果需要大量调用上面的将字符串型数字,转换为16进制数字的话,每次传入参数很繁琐。于是考虑自定义函数,比如

def int16(s):
    return int(s,base=16)

int16("12345")
输出:74565
import functools
int2 = functools.partial(int, base=2)
int2('1000000')
输出:64

所以,简单总结 functools.partial 的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数 会更简单。

注意到上面的新的 int2 函数,仅仅是把 base 参数重新设定默认值为 2, 但也可以在函数调用时传入其他值: int2(‘1000000’, base=10) 1000000

最后,创建偏函数时,实际上可以接收函数对象、*args 和**kw 这 3 个 参数,当传入: int2 = functools.partial(int, base=2)

实际上固定了 int()函数的关键字参数 base,也就是: int2(‘10010’) 相当于: kw = { ‘base’: 2 } int(‘10010’, **kw)

当传入: max2 = functools.partial(max, 10) 实际上会把 10 作为*args 的一部分自动加到左边,也就是:

max2(5, 6, 7) 相当于: args = (10, 5, 6, 7) max(*args) 结果为 10。

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

33. 模块

在 Python 中,一个.py文件就称之为一个模块(Module)。使用模块有如下好处:

  1. 提供代码重用性
  2. 避免函数名和变量名冲突

在模块之上有包即package名,比如有abc和xyz两个模块,都在包 mycompany中,那么模块的引用就变成了mycompany.abc。 注意,每一个包目录下面都会有一个__init__.py文件,这个文件必须存在,否则Python就把这个目录当成一个普通目录,而不是一个包。__init__.py可以是一个空文件,也可以有代码。

注意自己创建的模块名,不要与系统自带的模块名重名。

34. 使用模块

python内置了很多模块,可以直接使用,以下面的代码为例,说明一个标准代码块的写法:

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

' a test module '
__author__ = 'Michael Liao' 

import sys

def test():
    args = sys.argv
    if len(args) == 1:
        print("Hello,world!")
    elif len(args) == 2:
        print("Hello,%s!" % args[1])
    else:
        print("Too many arguments!")

if __name__ == "__main__":
    test()
Too many arguments!
  1. 第1,2行注释是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身是UTF-8编码

  2. 第 4 行是一个字符串,表示模块的文档注释,任何模块代码的第一个字 符串都被视为模块的文档注释。

  3. 第 6 行使用__author__变量把作者写进去

  4. import sys,导入 sys 模块后,我们就有了变量 sys 指向该模块,利用 sys 这个变量, 就可以访问 sys 模块的所有功能。

  5. sys 模块有一个 argv 变量,用 list 存储了命令行的所有参数。argv 至少 有一个元素,因为第一个参数永远是该.py 文件的名称,比如运行 python3 hello.py Michael 获得的 sys.argv 就是[‘hello.py’, ‘Michael]。

  6. if __name__=='__main__': 当我们在命令行运行 hello 模块文件时,Python解释器把一个特殊变量 name__置为__main,而如果在其他地方导入该 hello 模块时,if 判断将失败,即不会执行if内的代码。 因此,这种if测试可以让一个模块通过命令行运行时执行一 些额外的代码,最常见的就是运行测试。

1. 作用域

在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的希望内部使用。 在python中,通过_前缀来实现。

  1. 正常的函数名和变量名都是public,可以直接引用
  2. __xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__name__,hello模块定义的文档 注释也可以用特殊变量__doc__访问, 我们自己的变量一般不要用这种变 量名;
  3. 类似_xxx__xxx 这样的函数或变量就是非公开的(private),不应该被 直接引用,比如_abc,__abc 等;

上面说的是不应该被直接引用,而不是不能,因为python中并没有强制使该变量为private,只是说从编程习惯上说不应该直接使用private变量。 主要是为了后面面向对象类中的变量定义,外部不需要引用的函数全部定义成 private, 只有外部需要引用的函数才定义为 public。

35. 安装第三方模块

在 Python 中,安装第三方模块,是通过包管理工具 pip 完成的。

99. 【重要】下面的and和or

'123' or '456'  -> '123'  # bool('123') がTrueなので、 左辺値'123'をリターン
'123' or ''     -> '123'  # bool('123') がTrueなので、 左辺値'123'をリターン
''    or '456'  -> '456'  # bool('')    がFalseなので、右辺値'456'をリターン
''    or ''     -> ''     # bool('')    がFalseなので、右辺値''   をリターン
'123' and '456' -> '456'  # bool('123') がTrueなので、 右辺値'456'をリターン
'123' and ''    -> ''     # bool('123') がTrueなので、 右辺値''   をリターン
''    and '456' -> ''     # bool('')    がFalseなので、左辺値''   をリターン
''    and ''    -> ''     # bool('')    がFalseなので、左辺値''   をリターン