劳动节第二天,作为编程菜鸟继续来图书馆学习,今天来死磕第二部分——函数。
一、你希望函数的某些参数强制使用关键字参数传递
之前思路:直接用关键字传参。
遗漏点:以上是正确的,但是在传参的时候传入了一个位置参数可能就会报错。
def recv(maxsize, *, block): 'Receives a message' pass recv(1024, True) # TypeError 注意 * 之后的block参数只能接受关键字传参,所有位置参数都会被*接收 recv(1024, block=True) # Ok
注意:传递None和不传值是有差别的。
二、你写好了一个函数,然后想为这个函数的参数增加一些额外的信息,这样的话其他使用者就能清楚的知道这个函数应该怎么使用。
遗漏点:不知道大家在leecode上是否有注意到,他的一些算法题的格式在一般的函数定义中可能看不太懂。类似如下:
def add(x:int, y:int) -> int: # 表示x是int类型,y是int类型,返回值是int类型 return x + y
冒号后的注解和箭头后的注解都有助于我们理解,python解释器不会对这些注解添加任何的语义。它们不会被类型检查,运行时跟没有加注解之前的效果也没有任何差距。 然而,对于那些阅读源码的人来讲就很有帮助啦。
三、传参注意:
默认参数的值应该是不可变的对象,比如None、True、False、数字或字符串。 特别的,千万不要像下面这样写代码:
(我之前是真这么写过!)
def spam(a, b=[]): # NO! ...
为什么不能这样写呢:
因为函数里的参数值如果是可变类型的,每次修改都会变更,比如:
>>> def spam(a, b=[]): ... print(b) ... return b ... >>> x = spam(1) >>> x [] >>> x.append(99) >>> x.append('Yow!') >>> x [99, 'Yow!'] >>> spam(4) # Modified list gets returned! [99, 'Yow!']
如果你需要传个列表,定义时应该用一个默认参数None去接收。
四、你用lambda定义了一个匿名函数,并想在定义时捕获到某些变量的值。
先看如下代码:
>>> x = 10 >>> a = lambda y: x + y >>> x = 20 >>> b = lambda y: x + y >>>
a(10) , b(10) 输出的是什么? 正解:30,30
遗漏点:lambda表达式中的x是一个自由变量, 在运行时绑定值,而不是定义时就绑定,这跟函数的默认值参数定义是不同的。 因此,在调用这个lambda表达式的时候,x的值是执行时的值。
但是如果想让a(10)输出的是20怎么办呢?这里要把x当做一个默认参数来接受。
>>> x = 10 >>> a = lambda y, x=x: x + y >>> x = 20 >>> b = lambda y, x=x: x + y >>> a(10) 20 >>> b(10) 30
五、不恰当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表达式只有在运行的时候才会赋值,定义的时候不赋值,如何获取我们想要的结果呢:
>>> funcs = [lambda x, n=n: x+n for n in range(5)] >>> for f in funcs: ... print(f(0))
六、你有一个被其他python代码使用的callable对象,可能是一个回调函数或者是一个处理器, 但是它的参数太多了,导致调用时出错。
遗漏点:如果需要减少某个函数的参数个数,你可以使用 functools.partial()
。 partial()
函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。 为了演示清楚,假设你有下面这样的函数:
def spam(a, b, c, d): print(a, b, c, d)
现在我们使用 partial()
函数来固定某些参数值:
>>> from functools import partial >>> s1 = partial(spam, 1) # a = 1 >>> s1(2, 3, 4) 1 2 3 4 >>> s1(4, 5, 6) 1 4 5 6 >>> s2 = partial(spam, d=42) # d = 42 >>> s2(1, 2, 3) 1 2 3 42 >>> s2(4, 5, 5) 4 5 5 42 >>> s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42 >>> s3(3) 1 2 3 42 >>> s3(4) 1 2 4 42 >>> s3(5) 1 2 5 42
其实partial函数的作用相当于一个回调函数:
partial (spam,1) = spam(1) , 他的作用就是将第一个参数作为函数,第二个作为函数的参数。
partial函数的作用就是可以帮我们控制传递参数的数量。比如一个函数只接受两个变量,你可以传一个partial函数作为一个变量,里面可以处理多个变量。在回调函数中partial函数的作用还是非常重要的。
七、你有一个除 __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)) # Example use. Download stock data from yahoo yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}') for line in yahoo.open(names='IBM,AAPL,FB', fields='sl1c1v'): print(line.decode('utf-8'))
这里面的UrlTemplate只定义了一个方法,这个类的作用就是存储了template的名称,我们如何用一个函数来代替这个类呢?
def urltemplate(template): def opener(**kwargs): return urlopen(template.format_map(kwargs)) return opener # Example use yahoo = urltemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}') for line in yahoo(names='IBM,AAPL,FB', fields='sl1c1v'): print(line.decode('utf-8'))
简单来讲,一个闭包就是一个函数, 只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境。 因此,在我们的解决方案中,opener()
函数记住了 template
参数的值,并在接下来的调用中使用它。
任何时候只要你碰到需要给某个函数增加额外的状态信息的问题,都可以考虑使用闭包。 相比将你的函数转换成一个类而言,闭包通常是一种更加简洁和优雅的方案。
八、你的代码中需要依赖到回调函数的使用(比如事件处理器、等待后台任务完成后的回调等), 并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。
遗漏点:这一个问题主要讨论的是那些出现在很多函数库和框架中的回调函数的使用——特别是跟异步处理有关的。 为了演示与测试,我们先定义如下一个需要调用回调函数的函数:
def apply_async(func, args, *, callback): # Compute the result result = func(*args) # Invoke the callback with the result 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_result()
函数仅仅只接受一个参数 result
。不能再传入其他信息。 而当你想让回调函数访问其他变量或者特定环境的变量值的时候就会遇到麻烦。
为了让回调函数访问外部信息,一种方法是使用一个绑定方法来代替一个简单函数。 比如,下面这个类会保存一个内部序列号,每次接收到一个 result
的时候序列号加1:
class ResultHandler: def __init__(self): self.sequence = 0 def handler(self, result): self.sequence += 1 print('[{}] Got: {}'.format(self.sequence, result))
>>> r = ResultHandler() >>> 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 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
九、当你编写使用回调函数的代码的时候,担心很多小函数的扩张可能会弄乱程序控制流。 你希望找到某个方法来让代码看上去更像是一个普通的执行序列。
遗漏点:通过使用生成器和协程可以使得回调函数内联在某个函数中。
def apply_async(func, args, *, callback): # Compute the result result = func(*args) # Invoke the callback with the result callback(result)
from queue import Queue from functools import wraps class Async: def __init__(self, func, args): self.func = func self.args = args def inlined_async(func): @wraps(func) def wrapper(*args): f = func(*args) result_queue = Queue() result_queue.put(None) while True: result = result_queue.get() try: a = f.send(result) apply_async(a.func, a.args, callback=result_queue.put) except StopIteration: break return wrapper
def add(x, y): return x + y @inlined_async def test(): r = yield Async(add, (2, 3)) print(r) r = yield Async(add, ('hello', 'world')) print(r) for n in range(10): r = yield Async(add, (n, n)) print(r) print('Goodbye')
执行以下test(),会是什么结果呢?让我们来捋一下执行test的流程:
首先test被装饰器装饰了,相当于执行了wrapper这个函数,(test=inlined_async(test)=wrapper --> test()=wrapper())
f = test() --> 生成器对象 result_queue = [] while True: result = None try: a = f.send(None) 生成了 Async(add,(2,3)) apply_async(add,(2,3),callback=result_queue.put) --> result = 5, callback将其放入了result_queue队列[5,] 每次进入while True时,result都会取到最新的值,并且被send传递给生成器,产生一个新的Aysnc类,将send的值赋给r。
5 helloworld 0 2 4 6 8 10 12 14 16 18 Goodbye