函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。
而函数式编程(请注意多了一个“式”字)——Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。
我们首先要搞明白计算机(Computer)和计算(Compute)的概念。
在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。
而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。
函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python对函数式编程提供部分支持。由于Python允许使用变量,因此,Python不是纯函数式编程语言。
高阶函数
map()
接受两个参数:
- 函数
- Iterable
map()
将传入的函数依次作用于可迭代对象Iterable
的每个元素中,将结果作为新的迭代器Iterator
返回
1 2 3 4 5 6 7 8 9
| 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]
list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
|
map()
规范化英文名字(首字母大写,其余小写)
1 2 3 4 5 6 7
| def normalize(name): return name[0].upper() + name[1:].lower()
L1 = ['adam', 'LISA', 'barT'] L2 = list(map(normalize, L1)) print(L2) > ['Adam', 'Lisa', 'Bart']
|
reduce()
接受两个参数:
- 函数
- Iterable
reduce()
也是把一个函数作用在一个序列上,但不一样的的是reduce的函数必须接受两个参数,然后reduce会把结果继续和序列的下一个元素做累积计算
1 2 3 4 5 6 7
| >>> from functools import reduce >>> def add(x, y): return x + y
>>> reduce(add, [1, 3, 5, 7, 9]) 25
|
上面的例子完全可以用sum()
代替,下面再举些实际点的例子
1 2 3 4 5 6 7
| >>> from functools import reduce >>> def fn(x, y): return x * 10 + y
>>> reduce(fn, [1, 3, 5, 7, 9]) 13579
|
字符串str
也是一个序列,配合map
,我们可以自己写一个int()
函数,将字符串转为整形
1 2 3 4 5 6 7 8 9 10
| from functools import reduce def fn(x, y): return x * 10 + y def char2num(s): digits = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9} return digits[s]
>>> reduce(fn, map(char2num, '13679')) 13479
|
整理成一个str2int
函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def str2int(s): def fn(x, y): return x * 10 + y def char2num(s): return DIGITS[s] return reduce(fn, map(char2num, s))
>>> str2int('13579') 13579
|
通过lambda进一步简化
1 2 3 4 5 6 7 8 9
| from functools import reduce
DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}
def char2num(s): return DIGITS[s]
def str2int(s): return reduce(lambda x, y: x * 10 + y, map(char2num, s))
|
如此就能自己实现一个int()
函数
写一个prod函数,可以接受一个list,并计算里面元素的累乘积
1 2 3
| from functools import reduce def prod(L): return reduc(lambda x, y: xy * y, L)
|
map+reduce
使用map和reduce,写一个str2float
函数,将字符串’123.456’转换成浮点型123.456
1 2 3 4 5 6 7 8 9
| from functools import reduce
def str2float(s): DIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9} s_list = s.split('.') n_1 = reduce(lambda x, y: x * 10 + y, map(lambda x: DIGITS[x], s_list[0])) length = len(s_list[1]) n_2 = reduce(lambda x, y: x * 10 + y, map(lambda x: DIGITS[x], s_list[1])) * (10 ** (-length)) return n_1 + n_2
|
filter()
接受一个函数和一个序列,将函数依次作用于序列的每个元素,根据返回值是True
还是False
决定保留还是丢弃该元素.
同样返回生成器Iterator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def is_odd(n): return n % 2 == 1
list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
> [1, 5, 9, 15]
def not_empty(s): return s and s.strip()
list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))
> ['A', 'B', 'C']
|
sorted()
排序的核心是比较两个元素的大小,如果是数字就直接比较,如果是str或dict,比较过程则通过函数抽象出来.
sorted()
可以接收一个key
函数实现自定义排序
1 2 3 4 5 6
| sorted([36, 5, -12, 9, -21]) > [-21, -12, 5, 9, 36]
sorted([36, 5, -12, 9, -21], key=abs) > [5, 9, -12, -21, 36]
|
仔细看可以发现,最终返回的是原始list
的元素,它实际上是按照key
函数的规则先对原始数列排序,然后再对应原始数列输出原始数列的元素.
1 2 3
| keys排序结果 => [5, 9, 12, 21, 36] | | | | | 最终结果 => [5, 9, -12, -21, 36]
|
字符串排序默认情况下是按照ASCII
码来比较大小(大写Z
<小写a
),因此我们很多时候要忽略大小写去比较.要实现这个功能,只需要通过key
将源字符串全部转为大写/小写即可.
1 2
| sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower) > ['about', 'bob', 'Credit', 'Zoo']
|
如果要反向排序
,则要用到第三个参数reverse
1 2
| sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True) > ['Zoo', 'Credit', 'bob', 'about']
|
小结: sorted
实际上是先排序,再映射.
返回函数
高阶函数
除了可以接受函数作为参数,还是把函数作为返回值
返回.
1 2 3 4 5 6
| def calc_sum(*args): ax = 0 for n in args: ax = ax + n return ax
|
如果不需要立即求和,而是在后面的代码中根据需要再计算,此时可以不返回求和结果
,只是返回求和函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def lazy_sum(*args): def sum(): ax = 0 for n in args: ax = ax + n return ax return sum
f = lazy_sum(1, 3, 5, 7, 9) f > <function lazy_sum.<locals>.sum at 0x101c6ed90>
f() > 25
|
注意: 每次调用,返回的都是全新的函数
1 2 3 4
| f1 = lazy_sum(1, 3, 5, 7, 9) f2 = lazy_sum(1, 3, 5, 7, 9) f1==f2 > False
|
返回的函数并非立刻执行
1 2 3 4 5 6 7 8 9 10
| def count(): fs = [] for i in range(1, 4): def f(): return i * i fs.append(f) return fs
f1, f2, f3 = count()
|
当你调用f1-3时,你以为它会返回1,4,9; 但实际上它的返回如下:
1 2 3 4 5 6 7 8
| f1() > 9
f2() > 9
f3() > 9
|
原因在于返回的函数引用了变量i
,但它不是立即执行
.等到三个函数都返回时, i
已经变成3
,因此结果都是9
.
那如果一定要引用循环变量呢: 可以再定义一个函数,用该函数的参数绑定循环变量当前的值
.
1 2 3 4 5 6 7 8 9
| def count(): def f(j): def g(): return j * j return g fs = [] for i in range(1, 4): fs.append(f(i)) return fs
|
此时结果就是预料中的1
,4
,9
.
闭包
lazy_sum
的例子中,返回的函数在其内部定义了局部变量args
,这种称之为闭包
.
就是内层函数引用了外层函数的局部变量
.
如果只是读外层函数的值是没问题的:
1 2 3 4 5 6 7 8 9
| def inc(): x = 0 def fn(): return x + 1 return fn
f = inc() print(f()) > 1
|
但是如果对外层变量赋值就会报错:
1 2 3 4 5 6 7 8
| def inc(): x = 0 def fn(): x = x + 1 return x return fn
> UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
|
因为python解释器会把x
当作函数fn()
的局部变量,而x
作为局部变量没有初始化就直接计算x+1
是不行的,所以我们需要在fn()
内部加上nonlocal x
的声明,声明x
不是fn()
的局部变量,解释器就会把fn()
里面的x
看作外层函数的局部变量,这样才能计算成功.
1 2 3 4 5 6 7
| def inc(): x = 0 def fn(): nonlocal x x = x + 1 return x return fn
|
小结
- 一个函数可以返回一个计算结果, 也可以返回一个函数
- 返回一个函数时,该函数并非立即执行,返回函数中不要引用任何可能变化的变量
匿名函数
就是lambda
, 比如
1 2 3 4
| lambda x: x * x
def f(x): return x * x
|
好处:
- 没有名字,不必当心函数名冲突
- lambda也是一个函数对象,可以赋值给一个变量,再用该变量调用函数
1 2 3 4 5
| f = lambda x: x * x f <function <lambda> at 0x101c6ef28> f(5) > 25
|
- 一些简单的,一次性的函数用lambda可以缩短/简洁代码
装饰器
函数也是一个对象
,函数对象有一个__name__
属性,该属性就是函数的名字
假设现在我们想增加函数的功能,比如,在函数调用前后自动打印日志,但又不想修改原函数的定义:
这种在代码运行期间动态增加功能
的方式,就是装饰器(Decorator)
装饰器的本质就是一个返回函数的高阶函数
1 2 3 4 5 6
| def log(func): def wrapper(*args, **kw): print(f'call {func.__name__}:') return func(*args, **kw) return wrapper
|
这个装饰器decorator
接受一个函数作为参数,并返回一个函数
使用@
调用
1 2 3 4 5 6 7 8 9 10 11
| @log def now(): print('2024-1-9') now() > call now(): > 2024-1-9
now = log(now) now()
|
由于log()
是一个decorator,返回一个函数,所以,原来的now()
函数仍然存在,只是现在同名的now
变量指向了新的函数,于是调用now()
将执行新函数,即在log()
函数中返回的wrapper()
函数。
wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper()
函数内,首先打印日志,再紧接着调用原始函数。
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def log(text): def decorator(func): def wrapper(*args, **kw): print(f"{text}, {func.__name__}():") return func(*args, **kw) return wrapper return decorator
@log('execute') def now(): print('2024-1-9') now() > execute now(): > 2024-1-9
now = log('execute')(now) now()
|
首先执行log('execute')
返回了decorator()
,然后再调用返回的decorator()
,参数是now()
,最终返回wrapper()
调用wrapper()
会打印日志并把源函数now()
返回出来,并调用
__name__
的变动
被decortator
装饰过的函数,__name__
属性都会发生改变,因为最终返回的函数名变了,以上面两个例子为例,最终的__name__
属性都会变为wrapper
1 2
| now.__name__ > 'wrapper'
|
因为最终返回的函数就是wrapper
,所以我们在写装饰器时,还要把原始函数的__name__
复制到wrapper()
中,避免其他依赖于这个函数的__name__
属性的代码报错:
1
| wrapper.__name__ = func.__name__
|
但实际上我们不需要手动这样做,用内置模块functools
就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import functools
def log(func): @functools.wraps(func) def wrapper(*args, **kw): print(f"call {func.__name__}():") return func(*args, **kw) return wrapper
def log(text): def decorator(func): @functools.wraps(func) def wrapper(*args, **kw): print(f"{text} {func.__name__}():") return func(*args, **kw) return wrapper return decorator
|
例子
- 写一个装饰器,打印源函数执行时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import time import functools
def metric(fn): @functools.wraps(fn) def wrapper(*args, **kw): start_time = time.time() _ = fn(*args, **kw) end_time = time.time() time_cost = end_time - start_time print(f"{fn.__name__} tooks {time_cost}ms") return fn(*args, **kw) return wrapper
@metric def func1(x, y): time.sleep(0.0012) return x + y
|
- 写一个
@log
,既支持有参数也支持无参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import functools
def log(arg=None): if callable(arg): @functools.wraps(arg) def wrapper(*args, **kw): print(f"running {arg.__name__}()") return arg(*args, **kw) return wrapper else: def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kw): print(f"{arg} {fn.__name__}()") return fn(*args, **kw) return wrapper return decorator @log def func1(x, y): return x + y @log('execute') def func1(x, y): return x + y
|
- 写一个装饰器,在函数调用前后分别输出
begin_call
和end_call
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import functools
def log_decorator(fn): @functools.wraps(fn) def wrapper(*args, **kw): print('begin_call') result = fn(*args, **kw) print('end_call') return result return wrapper
@log_decorator def func1(x, y): print(x + y)
|
小结
- python的decorator支持函数实现也支持类实现
- 在OOP(面向对象)中, decorator也称为装饰模式,通过继承和组合实现
- 装饰器用于增强函数功能,定义复杂,使用方便
偏函数
偏函数是python的自带模块functools
提供的功能(partial
).
偏函数就是把特定函数的某些参数固定
(默认值),并返回一个新函数.
比如它可以将给一个函数加上一个默认参数:
1 2 3 4 5 6 7 8
|
def int2(x, base=2): return int(x, base)
import functools int2 = functools.partial(int, base=2)
|
另外,创建偏函数时,实际上接受了函数对象
,*args
和**kw
3个参数
1 2 3 4 5 6 7 8 9 10 11 12 13
| int2 = functools.partial(int, base=2) int2('10010')
kw = {'base': 2} int2 = int('10010', **kw)
max2 = functools.partial(max, 10) max2 = (5, 6, 7)
args = (10, 5, 6, 7) max2 = max(*args)
|
总结: 偏函数主要是用来固定参数以及简化代码