Python技法3:匿名函数、回调函数和高阶函数
1、定义匿名或内联函数
如果我们想提供一个短小的回调函数供sort()这样的函数用,但不想用def这样的语句编写一个单行的函数,我们可以借助lambda表达式来编写“内联”式的函数。如下图所示:
add = lambda x, y: x + y
print(add(2, 3)) # 5
print(add("hello", "world!")) # helloworld
可以看到,这里用到的lambda表达式和普通的函数定义有着相同的功能。
lambda表达式常常做为回调函数使用,有在排序以及对数据进行预处理时有许多用武之地,如下所示:
names = [ 'David Beazley', 'Brian Jones', 'Reymond Hettinger', 'Ned Batchelder']
sorted_names = sorted(names, key=lambda name: name.split()[-1].lower())
print(sorted_names)
# ['Ned Batchelder', 'David Beazley', 'Reymond Hettinger', 'Brian Jones']
lambda虽然灵活易用,但是局限性也大,相当于其函数体中只能定义一条语句,不能执行条件分支、迭代、异常处理等操作。
2、在匿名函数中绑定变量的值
现在我们想在匿名函数定义时完成对特定变量(一般是常量)的绑定,以便后期使用。如果我们这样写:
x = 10
a = lambda y: x + y
x = 20
b = lambda y: x + y
然后计算a(10)和b(10)。你可能希望结果是20和30,然而实际程序的运行结果会出人意料:结果是30和30。
这个问题的关键在于lambda表达式中的x是个自由变量(未绑定到本地作用域的变量),在运行时绑定而不是定义的时候绑定(其实普通函数中使用自由变量同理),而这里执行a(10)的时候x已经变成了20,故最终a(10)的值为30。如果希望匿名函数在定义的时候绑定变量,而之后绑定值不再变化,那我们可以将想要绑定的变量做为默认参数,如下所示:
x = 10
a = lambda y, x=x: x + y
x = 20
b = lambda y, x=x: x + y
print(a(10)) # 20
print(b(10)) # 30
上面我们提到的这个陷阱常见于一些对lambda函数过于“聪明”的应用中。比如我们想用列表推导式来创建一个列表的lambda函数并期望lambda函数能记住迭代变量。
funcs = [lambda x: x + n for n in range(5)]
for f in funcs:
print(f(0))
# 4
# 4
# 4
# 4
# 4
可以看到与我们期望的不同,所有lambda函数都认为n是4。如上所述,我们修改成以下代码即可:
funcs = [lambda x, n=n: x + n for n in range(5)]
for f in funcs:
print(f(0))
# 0
# 1
# 2
# 3
# 4
2、让带有n个参数的可调用对象以较少的参数调用
假设我们现在有个n个参数的函数做为回调函数使用,但这个函数需要的参数过多,而回调函数只能有个参数。如果需要减少函数的参数数量,需要时用functools包。functools这个包内的函数全部为高阶函数。高阶函数即参数或(和)返回值为其他函数的函数。通常来说,此模块的功能适用于所有可调用对象。
比如functools.partial()就是一个高阶函数, 它的原型如下:
functools.partial(func, /, *args, **keywords)
它接受一个func函数做为参数,并且它会返回一个新的newfunc对象,这个新的newfunc对象已经附带了位置参数args和关键字参数keywords,之后在调用newfunc时就可以不用再传已经设定好的参数了。如下所示:
def spam(a, b, c, d):
print(a, b, c, d)
from functools import partial
s1 = partial(spam, 1) # 设定好a = 1(如果没指定参数名,默认按顺序设定)
s1(2, 3, 4) # 1 2 3 4
s2 = partial(spam, d=42) # 设定好d为42
s2(1, 2, 3) # 1 2 3 42
s3 = partial(spam, 1, 2, d=42) #设定好a = 1, b = 2, d = 42
s3(3) # 1 2 3 42
上面提到的技术常常用于将不兼容的代码“粘”起来,尤其是在你调用别人的轮子,而别人写好的函数不能修改的时候。比如我们有以下一组元组表示的点的坐标:
points = [(1, 2), (3, 4), (5, 6), (7, 8)]
有已知的一个distance()函数可供使用,假设这是别人造的轮子不能修改。
import math
def distance(p1, p2):
x1, y1 = p1
x2, y2 = p2
return math.hypot(x2 - x1, y2 - y1)
接下来我们想根据列表中这些点到一个定点pt=(4, 3)的距离来排序。我们知道列表的sort()方法
可以接受一个key参数(传入一个回调函数)来做自定义的排序处理。但传入的回调函数只能有一个参数,这里的distance()函数有两个参数,显然不能直接做为回调函数使用。下面我们用partical()来解决这个问题:
pt = (4, 3)
points.sort(key=partial(distance, pt)) # 先指定好一个参数为pt=(4,3)
print(points)
# [(3, 4), (1, 2), (5, 6), (7, 8)]
可以看到,排序正确运行。还有一种方法要臃肿些,那就是将回调函数distance嵌套进另一个只有一个参数的lambda函数中:
pt = (4, 3)
points.sort(key=lambda p: distance(p, pt))
print(points)
# [(3, 4), (1, 2), (5, 6), (7, 8)]
这种方法以来臃肿,二来仍然存在我们上面提到过的一个毛病,如果我们定义回调函数后对pt有所修改,就会发生我们上面所说的不愉快的事情:
pt = (4, 3)
func_key = lambda p: distance(p ,pt)
pt = (0, 0) # 像这样,后面pt变了就GG
points.sort(key=func_key)
print(points)
# [(1, 2), (3, 4), (5, 6), (7, 8)]
可以看到,最终排序的结果由于后面pt的改变而变得完全不同了。所以我们还是建议大家采用使用functools.partial()函数来达成目的。
下面这段代码也是用partial()函数来调整函数签名的例子。这段代码利用multiprocessing模块以异步方式计算某个结果,然后用一个回调函数来打印该结果,该回调函数可接受这个结果和一个事先指定好的日志参数。
# result:回调函数本身该接受的参数, log是我想使其扩展的参数
def output_result(result, log=None):
if log is not None:
log.debug('Got: %r', result)
def add(x, y):
return x + y
if __name__ == '__main__':
import logging
from multiprocessing import Pool
from functools import partial
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('test')
p = Pool()
p.apply_async(add, (3, 4), callback=partial(output_result, log=log))
p.close()
p.join()
# DEBUG:test:Got: 7
下面这个例子则源于一个在编写网络服务器中所面对的问题。比如我们在socketServer模块的基础上,编写了下面这个简单的echo服务程序:
from socketserver import StreamRequestHandler, TCPServer
class EchoHandler(StreamRequestHandler):
def handle(self):
for line in self.rfile:
self.wfile.write(b'GoT:' + line)
serv = TCPServer(('', 15000), EchoHandler)
serv.serve_forever()
现在,我们想在EchoHandler类中增加一个__init__()方法,它接受额外的一个配置参数,用于事先指定ack。即:
class EchoHandler(StreamRequestHandler):
def __init__(self, *args, ack, **kwargs):
self.ack = ack
super().__init__(*args, **kwargs)
def handle(self) -> None:
for line in self.rfile:
self.wfile.write(self.ack + line)
假如我们就这样直接改动,就会发现后面会提示__init__()函数缺少keyword-only参数ack(这里调用EchoHandler()初始化对象的时候会隐式调用__init__()函数)。 我们用partical()也能轻松解决这个问题,即为EchoHandler()事先提供好ack参数。
from functools import partial
serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED'))
serv.serve_forever()
3、在回调函数中携带额外的状态
我们知道,我们调用回调函数后,就会跳转到一个全新的环境,此时会丢失我们原本的环境状态。接下来我们讨论如何在回调函数中携带额外的状态以便在回调函数内部使用。
因为对回调函数的应用在与异步处理相关的库和框架中比较常见,我们下面的例子也多和异步处理相关。现在我们定义了一个异步处理函数,它会调用一个回调函数。
def apply_async(func, args, *, callback):
# 计算结果
result = func(*args)
# 将结果传给回调函数
callback(result)
下面展示上述代码如何使用:
# 要回调的函数
def print_result(result):
print("Got: ", result)
def add(x, y):
return x + y
apply_async(add, (2, 3), callback=print_result)
# Got: 5
apply_async(add, ('hello', 'world'), callback=print_result)
# Got: helloworld
现在我们希望回调函数print_reuslt()能够接受更多的参数,比如其他变量或者环境状态信息。比如我们想让print_result()函数每次的打印信息都包括一个序列号,以表示这是第几次被调用,如[1] ...、[2] ...这样。首先我们想到,可以用额外的参数在回调函数中携带状态,然后用partial()来处理参数个数问题:
class SequenceNo:
def __init__(self) -> None:
self.sequence = 0
def handler(result, seq):
seq.sequence += 1
print("[{}] Got: {}".format(seq.sequence, result))
seq = SequenceNo()
from functools import partial
apply_async(add, (2, 3), callback=partial(handler, seq=seq))
# [1] Got: 5
apply_async(add, ('hello', 'world'), callback=partial(handler, seq=seq))
# [2] Got: helloworld
看起来整个代码有点松散繁琐,我们有没有什么更简洁紧凑的方法能够处理这个问题呢?答案是直接使用和其他类绑定的方法(bound-method)。比如面这段代码就将print_result做为一个类的方法,这个类保存了计数用的ack序列号,每当调用print_reuslt()打印一个结果时就递增1:
class ResultHandler:
def __init__(self) -> None:
self.sequence = 0
def handler(self, result):
self.sequence += 1
print("[{}] Got: {}".format(self.sequence, result))
apply_async(add, (2, 3), callback=r.handler)
# [1] Got: 5
apply_async(add, ('hello', 'world'), callback=r.handler)
# [2] Got: helloworld
还有一种实现方法是使用闭包,这种方法和使用类绑定方法相似。但闭包更简洁优雅,运行速度也更快:
def make_handler():
sequence = 0
def handler(result):
nonlocal sequence # 在闭包中编写函数来修改内层变量,需要用nonlocal声明
sequence += 1
print("[{}] Got: {}".format(sequence, result))
return handler
handler = make_handler()
apply_async(add, (2, 3), callback=handler)
# [1] Got: 5
apply_async(add, ('hello', 'world'), callback=handler)
# [2] Got: helloworld
最后一种方法,则是利用协程(coroutine)来完成同样的任务:
def make_handler_cor():
sequence = 0
while True:
result = yield
sequence += 1
print("[{}] Got: {}".format(sequence, result))
handler = make_handler_cor()
next(handler) # 切记在yield之前一定要加这一句
apply_async(add, (2, 3), callback=handler.send) #对于协程来说,可以使用它的send()方法来做为回调函数
# [1] Got: 5
apply_async(add, ('hello', 'world'), callback=handler.send)
# [2] Got: helloworld
Python技法3:匿名函数、回调函数和高阶函数的更多相关文章
- Python中匿名函数与内置高阶函数详解
大家好,从今天起早起Python将持续更新由小甜同学从 初学者的角度 学习Python的笔记,其特点就是全文大多由 新手易理解 的 代码与注释及动态演示 .刚入门的读者千万不要错过! 很多人学习pyt ...
- Python 进程线程协程 GIL 闭包 与高阶函数(五)
Python 进程线程协程 GIL 闭包 与高阶函数(五) 1 GIL线程全局锁 线程全局锁(Global Interpreter Lock),即Python为了保证线程安全而采取的独立线程运行的 ...
- Python实用笔记 (12)函数式编程——高阶函数
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数! Python对函数式编程提供部分支持.由于Python允许使用变量,因此,Python不是纯函数式编程语言. 变量 ...
- 函数高阶(函数,改变函数this指向,高阶函数,闭包,递归)
一.函数的定义方式 1.函数声明方式 function 关键字(命名函数) 2.函数表达式(匿名函数) 3.new Function( ) var fn = new Function(‘参数1 ...
- Python中的高阶函数与匿名函数
Python中的高阶函数与匿名函数 高阶函数 高阶函数就是把函数当做参数传递的一种函数.其与C#中的委托有点相似,个人认为. def add(x,y,f): return f( x)+ f( y) p ...
- Python高阶函数和匿名函数
高阶函数:就是把函数当成参数传递的一种函数:例如 注解: 1.调用add函数,分别执行abs(-8)和abs(11),分别计算出他们的值 2.最后在做和运算 map()函数 python内置的一个高阶 ...
- Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)...啊啊啊
函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的程序设计 ...
- (转)Python进阶:函数式编程(高阶函数,map,reduce,filter,sorted,返回函数,匿名函数,偏函数)
原文:https://www.cnblogs.com/chenwolong/p/reduce.html 函数式编程 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数 ...
- Python之路-函数基础&局部变量与全局变量&匿名函数&递归函数&高阶函数
一.函数的定义与调用 函数:组织好的.可重复使用的.用户实现单一或者关联功能的代码段.函数能够提高应用的模块性和代码的重复利用率.Python提供了很多内置的函数,比如len等等,另外也可以根据自己的 ...
- python开发基础04-函数、递归、匿名函数、高阶函数、装饰器
匿名函数 lamba lambda x,y,z=1:x+y+z 匿名就是没有名字 def func(x,y,z=1): return x+y+z 匿名 lambda x,y,z=1:x+y+z #与函 ...
随机推荐
- 在CentOs7源码安装mysql-5.6.35单实例数据库
首先安装依赖包,避免在安装过程中出现问题 [root@bogon liuzhen]# yum -y install gcc gcc-c++[root@bogon liuzhen]# yum -y in ...
- 基于源码编译的lnmp架构实现论坛的搭建及memcache的应用
系统环境: RHEL6 x86-64 selinux and iptables disabled LNMP代表的就是:Linux系统下Nginx+MySQL+PHP这种网站服务器架构 Linux是一类 ...
- 性能测试工具JMeter 基础(八)—— 测试元件: 逻辑控制器之事物控制器
事物控制器是将控制器下的所有取样器作为一个事物统计.分析 事物控制器(Transaction Controller) 事务控制器一共有两个选项: Generate parent sample:默认不勾 ...
- Redis集群的搭建及与SpringBoot的整合
1.概述 之前聊了Redis的哨兵模式,哨兵模式解决了读的并发问题,也解决了Master节点单点的问题. 但随着系统越来越庞大,缓存的数据越来越多,服务器的内存容量又成了问题,需要水平扩容,此时哨兵模 ...
- Identity用户管理入门五(登录、注销)
一.建立LoginViewModel视图模型 using System.ComponentModel.DataAnnotations; namespace Shop.ViewModel { publi ...
- 排查dubbo接口重复注销问题,我发现了一个巧妙的设计
背景 我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈dubbo接口注销报错.经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会 ...
- (xxl_job | quartz):XXL_JOB 对比 Quartz 一决高下!
概述: XXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展. 现已开放源代码并接入多家公司线上产品线,开箱即用. 官方地址中文版:http://www.x ...
- 手把手教你如何玩转消息中间件(ActiveMQ)
手把手教你如何玩转消息中间件(ActiveMQ) 2018年07月15日 18:07:39 Cs_hnu_scw 阅读数 12270 标签: 中间件消息中间件ActiveMQ分布式集群 更多 个人分类 ...
- java版gRPC实战之七:基于eureka的注册发现
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- C++吃金币小游戏
上图: 游戏规则:按A,D键向左和向右移动小棍子,$表示金币,0表示炸弹,吃到金币+10分,吃到炸弹就GAME OVER. 大体思路和打字游戏相同,都是使用数组,refresh和run函数进行,做了一 ...