PythonCookbook第七章(函数)【透彻完结】
函数(前面已经看过了,这次把博客补上,书中讲的内容并不难)
用def语句定义的函数是所有程序的基石。书中介绍默认参数、可接受任意数量参数的函数、关键字参数、参数注解以及闭包。
7.1 编写可接收任意数量参数的函数
问题:
编写一个可接收任意数量参数的函数
解决方案:
*args,**kwargs
def avg(first, *args): # args 返回的是元祖 return (first + sum(args)) / (1 + len(args)) import html def make_element(name, value, **kwargs): # kwargs返回的是字典 keyvals = [' %s="%s"' % item for item in kwargs.items()] attr_str = ''.join(keyvals) element = '<{name} {attrs}>{value}</{name}>'.format( name=name, attrs = attr_str, # 替换value中的<>符号 value = html.escape(value) ) return element if __name__ == '__main__': print(avg(12, 3, 4, )) print(avg(12, 3, 2, 32, 34, 3)) print(make_element('item', 'Albatross', size='large', quantity=6)) print(make_element('p', '<spam>'))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_7/t1_2.py 6.333333333333333 14.333333333333334 <item size="large" quantity="6">Albatross</item> <p ><spam></p> Process finished with exit code 0
讨论:
在函数定义中,以*打头的参数只能作为最后一个位置参数出现,而以**打头的参数只能作为最后一个参数出现。
不要犯初级的错误了,默认参数的位置,一定要在非默认参数的后面,今天这个小问题尽然卡住我了,丢人丢到家了。
7.2 编写只接收关键字参数的函数
问题
只通过关键字形式接收特定的参数
解决方案
# 传入的参数中,无论多少,第一个maxsize接收第一个参数,后面所有的位置参数全部*接收 # 想给*后面的参数赋值,必须通过关键形式. def recv(maxsize, *, block): ... def minium(*values, clip=None): m = min(values) # 如果默认不是空 if clip is not None: m = clip if clip < m else m return m if __name__ == '__main__': print(minium(1, 2, 3, 4, clip=5)) print(recv(1024, block=True)) print(recv(1024, True))
讨论
这个叫kewword-only参数常常是一种提高代码可读性的好方法。
7.3 将元数据信息附加到函数上
讨论:
在函数上面添加一些元信息
解决方案:
def add(x:int, y:int) -> int: return x+y if __name__ == '__main__': print(help(add)) # 通过annotations属性输出信息 print(add.__annotations__)
讨论:
一般没什么用,我也基本不会用,可以在验证参数的时候用一下,但装饰器更好用。
7.4 从函数中返回多个值
问题:
我们想从函数中返回多个值
解决方案:
返回一个元祖
def myfun(x, y, z): # 元祖的定义靠逗号 return x, y, z if __name__ == '__main__': res = myfun(1, 2, 3) print(res) print(type(res))
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_7/t3_4.py (1, 2, 3) <class 'tuple'>
讨论:
没啥讨论的
7.5 定义带有默认参数的函数
问题
定义一个函数或者方法,其中由一个或者多个参数是可选的,并且带有默认值
解决方案:
# 普通默认参数 def spam1(a, b=42): print(a, b) # 定义参数为可变容器的话 def spam2(a, b=None): if b is None: b = [] _no_value = object() # 验证参数b有没有填写, 可以赋值一个object实例 def spam3(a, b=_no_value): if b is _no_value: print('No b value supplied') if __name__ == '__main__': spam3(1)
讨论:
定义带有默认参数的函数看似容易,但其实并不像看到的那么简单,很有道理。
流畅的Python书中说过,Python的函数传参其实使共享传参,在函数定义好以后,所有的函数参数就好比函数的属性,绑定在函数上面。
In [21]: x = 42 In [22]: def spam(a, b=x): ...: print(a,b) ...: In [23]: spam(1) 1 42 In [24]: x = 50 In [25]: spam(1) 1 42 In [26]: import inspect In [31]: inspect.getfullargspec(spam) Out[31]: FullArgSpec(args=['a', 'b'], varargs=None, varkw=None, defaults=(42,), kwonlyargs=[], kwonlydefaults=None, annotations={})
这里x属于不可变参数,定义函数的时候,b也指向了42值,当修改x的时候,并不影响42的值,所以函数的内部属性不会变。
In [32]: def spam(a, b=[]): ...: print(b) ...: return b ...: ...: In [33]: x = spam(1) [] In [34]: x Out[34]: [] In [35]: x.append(99) In [36]: x Out[36]: [99] In [37]: spam(1) [99] Out[37]: [99] In [38]:
上面一个定义了可变参数为函数属性的例子,这样使非常不可取的,只要脚本执行中,这个函数无论什么时候执行,这个可变参数都会根据函数的执行而发生变化,所有的函数执行都将共享这个可变参数,这是不对的。
def spam2(a, b=None):
if b not b:
b = []
在这个函数里这样定义b是否为None是不对的,因为'',[],空数据not以后都会变True
最后的那个ocject()默认值来测试是否提供了参数,还是比较有意思的,object()作为Python中所有对象的基类而存在,这玩意唯一的用处就是检测相等性。
7.6 定义匿名或内联函数
问题:
需要为高阶函数比如(sort)操作,但不想写def
解决方法:
lambda函数
In [44]: add = lambda x, y: x+y In [45]: add(2,3) Out[45]: 5 In [46]: add([1,2],[3,4]) Out[46]: [1, 2, 3, 4] In [47]: name = ['sidian','wudian','liusidian'] In [48]: sorted(name,key=lambda x: x[0]) Out[48]: ['liusidian', 'sidian', 'wudian'] In [49]:
讨论:
lambda不写复杂的逻辑
其实lambda在实际操作中,我经常会犯错误,可能还是我用的少,或者实例有限,在lambda冒号的左边的参数,就好比def(args)里面的参数,所有的在函数里面定义参数的规则,这里也一样。
冒号后面是返回的值,这样时候去理解7.7节的内容就更加好理解,在lambda y,x=x: x+y,中,就好比已经定义了内部参数x,这样冒号后面的函数体也就是返回值不会读取外部变量的x。
如果没有定义,则执行函数的时候就去读取最新的x的值。
7.7 在匿名函数中绑定变量的值
问题:
我们利用lambda表达式定义了一个匿名函数,但是希望可以在函数定义的时候完成对特定变量的绑定。
解决方案:
这个还是蛮有意思的,匿名函数在冒号前面的参数,可以直接读取外部的全局变量参数,当匿名函数执行的时候,全部变量是什么他就读取什么。
这个跟定义好的函数,函数内部读取外部参数一样的逻辑。
In [49]: x = 10 In [50]: a = lambda y: x+y In [51]: x= 20 In [52]: b = lambda y: x+y In [53]: a(30) Out[53]: 50 In [54]: b(30) Out[54]: 50 In [55]:
没有绑定的情况,如果想绑定,可以填写默认参数一样。
In [55]: x = 10 In [56]: b = lambda y,x=x: x+y In [57]: x= 20 In [58]: a = lambda y,x=x: x+y In [59]: a(20) Out[59]: 40 In [60]: b(20) Out[60]: 30 In [61]:
讨论:
这个还是蛮有意思的,lambda函数还是非常好用的,但流畅的Python里面好像不是很推荐,不知道为什么。
In [61]: func = [lambda x: x+n for n in range(5)] In [62]: for f in func: ...: print(f(0)) ...: 4 4 4 4 4
这个是在执行时,读取变量n,那时候n就是4
In [66]: func = [lambda x,n=n: x+n for n in range(5)] In [67]: for f in func: ...: print(f(0)) ...: 0 1 2 3 4
这个是在定义的匿名函数的时候设置参数
7.8 让带有N个参数的可调用对象以较少的参数形式调用
问题
我们有一个可调用的对象可能会以回调函数的形式同其他的Python代码交互。但是这个可调用对象的参数过多,如果直接调用的话会产生异常。
解决方案:
用funtools.partail(好像国内的教学叫做偏函数,当时想了半天,不懂),但我感觉其实lambda就够了,我的感觉,lambda更加好理解,两个返回的都是函数,但lambda学精了,感觉太强大了
In [14]: def spam(a,b,c,d): ...: print(a,b,c,d) ...: In [15]: s1 = partial(spam,1) In [16]: ss1 = lambda b,c,d:spam(1,b,c,d) In [17]: s1(2,3,4) 1 2 3 4 In [18]: ss1(2,3,4) 1 2 3 4 In [19]: s2 = partial(spam,d=42) In [20]: ss2 = lambda a,b,c,d=42:spam(a,b,c,d) In [21]: s2(1,2,3) 1 2 3 42 In [22]: ss2(1,2,3) 1 2 3 42 In [23]: s3 = partial(spam,1,2,d=42) In [25]: ss3 = lambda c, d=42:spam(1,2,c,d) In [26]: s3(1) 1 2 1 42 In [27]: ss3(1) 1 2 1 42
上面的列子中,我将所有的partial函数全部转换为了lambda函数,从操作可以看出,转换成lambda函数相对代码量更加大。
In [35]: s3 = partial(spam,1,2,d=42) In [36]: s3(3,4) --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-36-91a1173e6786> in <module> ----> 1 s3(3,4) TypeError: spam() got multiple values for argument 'd' In [37]: In [37]: s3(3,d=4) 1 2 3 4 In [38]: ss3 = lambda c,d=42:spam(1,2,c,d) In [39]: ss3(3,4) 1 2 3 4 In [40]: ss3(3,d=4) 1 2 3 4 In [41]:
刚刚测试了,在偏函数中定义了默认参数,后面执行该变函数,则一定要通过关键字传参,但lambda跟函数一样,定义了默认参数,仍然可以通过位置传参。
讨论:
书中用partial写了几个例子。
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() # 进程池很久没用了,第一个是函数,第二为参数,第三为回调函数,将函数返回的值传入回调函数。 # 本来output_result需要两个参数,但一个参数在partial定义了 p.apply_async(add, (3, 4), callback=partial(output_result, log=log)) p.close() p.join()
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() # 进程池很久没用了,第一个是函数,第二为参数,第三为回调函数,将函数返回的值传入回调函数。 # 本来output_result需要两个参数,但一个参数在partial定义了 p.apply_async(add, (3, 4), callback=partial(output_result, log=log)) p.close() p.join()
最后一个案例还是非常经典的,我觉的还是写一些,我主要被继承父类,如果添加一个自定义的初始化属性吸引,对于partial已经看腻了。
from functools import partial points = [ (1, 2), (3, 4), (5, 6), (7, 8) ] import math # 求两个坐标点的距离 def distance(p1, p2): x1, y1 = p1 x2, y2 = p2 return math.hypot(x2 - x1, y2 - y1) if __name__ == '__main__': # 求points内的点与p直线距离排序 p = (4, 3) # 默认将p填入,key函数只需要读取points里的内容,执行函数,按照大小进行排序 print(sorted(points, key=partial(distance,p)))
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() # 进程池很久没用了,第一个是函数,第二为参数,第三为回调函数,将函数返回的值传入回调函数。 # 本来output_result需要两个参数,但一个参数在partial定义了 p.apply_async(add, (3, 4), callback=partial(output_result, log=log)) p.close() p.join()
第三个案例比较有意思的是,如果继承父类的初始化函数,并通过关键字参数添加实例属性。
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() # 上面的写法需要该一些,需要添加一个自己的属性. class EchoHandler_O(StreamRequestHandler): # 我主要是被这个初始化写法吸引,他把函数的传参用到了实处 def __init__(self, *args, ack, **kwargs): self.ack = ack super(EchoHandler_O, self).__init__(*args, **kwargs) def handle(self): for line in self.rfile: self.wfile.write(self.ack + line) from functools import partial # 通过偏函数给实例定义属性,要不然会报错,少参数,但我执行的时候没报错。 serv1 = TCPServer(('',15000), partial(EchoHandler_O,ack=b'RECEIVED')) serv1.serve_forever()
7.9 用函数替代只有单个方法的类。
问题:
一个只定义了一个方法的类,(除了__init__方法外),为了简化代码,我们用一个函数代替。
解决方案:
简单的来说就是闭包实现。
上代码案例
from urllib.request import urlopen class UrlTemplate: def __init__(self, template): self.template = template def open(self, **kwargs): return urlopen(self.template.format_map(kwargs)) # 通过外部函数传入闭包参数template,然后返回内部函数 def urltemplate(template): def open(**kwargs): urlopen(template.format_map(kwargs)) return open
讨论
无论何时,当在编写代码中遇到需要附加额外的状态给函数,请考虑使用闭包。实例中,用闭包的元编程形式更加优雅。
7.10 在回调函数中携带额外的状态
问题:
正在编写使用回调函数的代码,但是希望回调函数可以携带额外的状态以便在回调函数内部使用。
解决方案:
通过传入实例,闭包函数,或者生成器,或者变函数内部携带一个实例。
def apply_async(func, args, *, callback): # 运行func函数 result = func(*args) # 结果执行回调函数 callback(result) def print_result(result): print('Got:', result) def add(x, y): return x + y if __name__ == '__main__': apply_async(add, (2,3), callback=print_result) apply_async(add, ('hello', 'world'), callback=print_result)
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_7/t10_2.py Got: 5 Got: helloworld Process finished with exit code 0
上面是一个普通的回调函数,但现在希望回调函数可以同其他变量或者部分函数进行交互时,缺乏这类信息就会带来麻烦。
第一种进化方式,回调函数时一个实例方法
def apply_async(func, args, *, callback): # 运行func函数 result = func(*args) # 结果执行回调函数 callback(result) def print_result(result): print('Got:', result) def add(x, y): return x + y # 定义一个类 class ResultHandle: def __init__(self): self.sequence = 0 def handle(self, reslut): self.sequence += 1 print('[{}] Got: {}'.format(self.sequence, reslut)) # 创建一个闭包函数 def make_handle(): sequence = 0 def handle(result): nonlocal sequence sequence += 1 print('[{}] Got: {}'.format(sequence, result)) return handle # 创建一个生成器 def make_handle_2(): sequence = 0 while True: result = yield sequence += 1 print('[{}] Got: {}'.format(sequence, result)) # 通过变函数,在函数体内部先内置一个实例。 class Sequence: def __init__(self): self.sequence = 0 sequence = Sequence() def handle(res, seq): seq.sequence += 1 print('[{}] Got: {}'.format(seq.sequence, res)) if __name__ == '__main__': apply_async(add, (2, 3), callback=print_result) apply_async(add, ('hello', 'world'), callback=print_result) print('=.' * 15) # 传入实例 r = ResultHandle() apply_async(add, (2, 3), callback=r.handle) apply_async(add, ('hello', 'world'), callback=r.handle) print('=.' * 15) # 传入闭包函数 f = make_handle() apply_async(add, (2, 3), callback=f) apply_async(add, ('hello', 'world'), callback=f) print('=.' * 15) # 传入一个生成器 generator = make_handle_2() # 预激活 generator.send(None) apply_async(add, (2, 3), callback=generator.send) apply_async(add, ('hello', 'world'), callback=generator.send) print('=.' * 15) from functools import partial # 通过偏函数传入实例 apply_async(add, (2, 3), callback=partial(handle, seq=sequence)) apply_async(add, ('hello', 'world'), callback=partial(handle, seq=sequence)) # 通过lambda函数传入实例 apply_async(add, (2, 3), callback=lambda r: handle(r, sequence)) apply_async(add, ('hello', 'world'), callback=lambda r: handle(r, sequence))
写的时候,一把全上了。
讨论:
重点全部写在代码注释里面了
7.11 内联回调函数
问题:
我正在编写使用回调函数的代码,但是担心小型函数在代码中泛滥,程序的控制流会失控,我们希望有某种方法使代码看来更像一般的过程式步骤
解决方案
书中用了装饰器,队列,生成器,回调函数,整个思路流畅,佩服,羡慕,嫉妒,恨。
国内喜欢叫生产者与消费者的关系,但又涉及了回调函数,确实复杂度一下子上去了。
from queue import Queue from functools import wraps def apply_async(func, args, *, callback): # 运行func函数 result = func(*args) # 结果执行回调函数 callback(result) class Asyns: def __init__(self, func, args): self.func = func self.args = args def add(x, y): return x + y def inlined_async(func): @wraps(func) def wrapper(*args): # 运行生成器函数,f为生成器 f = func(*args) result_queue = Queue() # 首相放入None,用与预激活 result_queue.put(None) while True: try: # 取回第一个返回的对象 a = f.send(result_queue.get()) # 执行回调函数,将执行结果放入队列,用于循环中读取队列 apply_async(a.func, a.args, callback=result_queue.put) # 当生成器执行send,后面没有yiled,碰到return了,产生StopIteration信号,停止循环 except StopIteration: break return wrapper @inlined_async def test(): # 包装成对象,到时候供func提取属性使用 r = yield Asyns(add, (2, 3)) print(r) r = yield Asyns(add, ('hello', 'world')) print(r) for n in range(10): r = yield Asyns(add, (n, n)) print(r) print('Goodbye') if __name__ == '__main__': test()
/usr/local/bin/python3.7 /Users/shijianzhong/study/PythonCookbook/chapter_7/t11_2.py 5 helloworld 0 2 4 6 8 10 12 14 16 18 Goodbye Process finished with exit code 0
讨论:
这个代码写的很骚,很好,我喜欢。
7.12访问定义在闭包内的变量
我们希望通过函数来扩展闭包,使得闭包在内层定义的变量可以被访问和修改
解决方案
In [49]: def sample(): ...: n = 0 ...: def func(): ...: print('n=',n) ...: ...: def get_n(): ...: return n ...: ...: def set_n(value): ...: nonlocal n ...: n = value ...: ...: setattr(func, get_n.__name__, get_n) ...: setattr(func, set_n.__name__, set_n) ...: return func ...: ...: ...: ...: In [50]: f = sample() In [51]: f.set_n(5) In [52]: f.get_n Out[52]: <function __main__.sample.<locals>.get_n()> In [53]: f.get_n() Out[53]: 5
给闭包函数的返回函数的属性添加一些绑定的函数,读取或者修改闭包数据。
讨论:
书中通过测试,对于读取修改数据,闭包的数据修改,比定义类,修改实例属性要快,因为闭包函数不设计额外的self变量。
代码不上了,书中一种是通过属性赋值,给对象附带函数
还有一种是通过实例化方式给对象绑定方法,运行下来使一种快。
函数结束,干货满满。