AI智能
改变未来

Python技法3:匿名函数、回调函数和高阶函数


1、定义匿名或内联函数

如果我们想提供一个短小的回调函数供

sort()

这样的函数用,但不想用

def

这样的语句编写一个单行的函数,我们可以借助

lambda

表达式来编写“内联”式的函数。如下图所示:

add = lambda x, y: x + yprint(add(2, 3)) # 5print(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 = 10a = lambda y: x + yx = 20b = lambda y: x + y

然后计算

a(10)

b(10)

。你可能希望结果是

20

30

,然而实际程序的运行结果会出人意料:结果是

30

30


这个问题的关键在于

lambda

表达式中的

x

是个自由变量(未绑定到本地作用域的变量),在运行时绑定而不是定义的时候绑定(其实普通函数中使用自由变量同理),而这里执行

a(10)

的时候

x

已经变成了

20

,故最终

a(10)

的值为30。如果希望匿名函数在定义的时候绑定变量,而之后绑定值不再变化,那我们可以将想要绑定的变量做为默认参数,如下所示:

x = 10a = lambda y, x=x: x + yx = 20b = lambda y, x=x: x + yprint(a(10)) # 20print(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 partials1 = partial(spam, 1) # 设定好a = 1(如果没指定参数名,默认按顺序设定)s1(2, 3, 4) # 1 2 3 4s2 = partial(spam, d=42) # 设定好d为42s2(1, 2, 3) # 1 2 3 42s3 = partial(spam, 1, 2, d=42) #设定好a = 1, b = 2, d = 42s3(3) # 1 2 3 42

上面提到的技术常常用于将不兼容的代码“粘”起来,尤其是在你调用别人的轮子,而别人写好的函数不能修改的时候。比如我们有以下一组元组表示的点的坐标:

points = [(1, 2), (3, 4), (5, 6), (7, 8)]

有已知的一个

distance()

函数可供使用,假设这是别人造的轮子不能修改。

import mathdef distance(p1, p2):x1, y1 = p1x2, y2 = p2return 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变了就GGpoints.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 + yif __name__ == \'__main__\':import loggingfrom multiprocessing import Poolfrom functools import partiallogging.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, TCPServerclass 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 = acksuper().__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 partialserv = 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 + yapply_async(add, (2, 3), callback=print_result)# Got: 5apply_async(add, (\'hello\', \'world\'), callback=print_result)# Got: helloworld

现在我们希望回调函数

print_reuslt()

能够接受更多的参数,比如其他变量或者环境状态信息。比如我们想让

print_result()

函数每次的打印信息都包括一个序列号,以表示这是第几次被调用,如

[1] ...

[2] ...

这样。首先我们想到,可以用额外的参数在回调函数中携带状态,然后用

partial()

来处理参数个数问题:

class SequenceNo:def __init__(self) -> None:self.sequence = 0def handler(result, seq):seq.sequence += 1print("[{}] Got: {}".format(seq.sequence, result))seq = SequenceNo()from functools import partialapply_async(add, (2, 3), callback=partial(handler, seq=seq))# [1] Got: 5apply_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 = 0def handler(self, result):self.sequence += 1print("[{}] Got: {}".format(self.sequence, result))apply_async(add, (2, 3), callback=r.handler)# [1] Got: 5apply_async(add, (\'hello\', \'world\'), callback=r.handler)# [2] Got: helloworld

还有一种实现方法是使用闭包,这种方法和使用类绑定方法相似。但闭包更简洁优雅,运行速度也更快:

def make_handler():sequence = 0def handler(result):nonlocal sequence # 在闭包中编写函数来修改内层变量,需要用nonlocal声明sequence += 1print("[{}] Got: {}".format(sequence, result))return handlerhandler = make_handler()apply_async(add, (2, 3), callback=handler)# [1] Got: 5apply_async(add, (\'hello\', \'world\'), callback=handler)# [2] Got: helloworld

最后一种方法,则是利用协程(coroutine)来完成同样的任务:

def make_handler_cor():sequence = 0while True:result = yieldsequence += 1print("[{}] Got: {}".format(sequence, result))handler = make_handler_cor()next(handler) # 切记在yield之前一定要加这一句apply_async(add, (2, 3), callback=handler.send) #对于协程来说,可以使用它的send()方法来做为回调函数# [1] Got: 5apply_async(add, (\'hello\', \'world\'), callback=handler.send)# [2] Got: helloworld

参考文献

  • [1] https://www.python.org/
  • [2] Martelli A, Ravenscroft A, Ascher D. Python cookbook[M]. " O\’Reilly Media, Inc.", 2005.
赞(0) 打赏
未经允许不得转载:爱站程序员基地 » Python技法3:匿名函数、回调函数和高阶函数