第七章 函数

7.1 可接受任意数量参数的函数

问题

需要构造一个可接受任意数量参数的函数。

解决方案

使用*参数。

要接受任意数量的关键字参数,使用以**开头的参数。

如果希望函数能同时接受任意数量的位置参数和关键字参数,可以同时使用***

讨论

一个*参数只能出现在函数定义中最后一个位置参数后面,而 **参数只能是最后一个参数。 要注意的是,在*参数后面仍可以定义其他参数。

def a(x, *args, y):
    pass

def b(x, *args, y, **kwargs):
    pass

这种参数就是强制关键字参数

7.2 只接受关键字参数的函数

问题

你希望函数的某些参数强制使用关键字参数传递

解决方案

将强制关键字参数放到某个*参数或者单个*后面就能达到这种效果。

def recv(maxsize, *, block):
    'Receives a message'
    pass

recv(1024, True) # TypeError
recv(1024, block=True) # Ok

7.3 给函数参数增加元信息

问题

想为这个函数的参数增加一些额外的信息,使得其他使用者知道这个函数应该怎么使用。

解决方案

使用函数参数注解

def add(x:int, y:int) -> int:
    return x + y

python解释器不会对这些注解添加任何的语义,但第三方工具和框架可能会对这些注解添加语义。

讨论

函数注解只存储在函数的annotations 属性中

7.4 返回多个值的函数

问题

你希望构造一个可以返回多个值的函数

解决方案 函数直接返回一个元组就行了

>>> def myfun():
... return 1, 2, 3
...
>>> a, b, c = myfun()
>>> a
1
>>> b
2
>>> c
3

讨论

很多时候序列的构造和解包都是自动完成的

比如可以这样

return a,

来返回一个只有一个元素的元组

7.5 定义有默认参数的函数

问题

你想定义一个函数或者方法,它的一个或多个参数是可选的并且有一个默认值。

解决方案

定义一个有可选参数的函数是非常简单的,直接在函数定义中给参数指定一个默认值,并放到参数列表最后就行了。

我的理解

此处所说的参数列表最后有些不够清楚,我想这样的情况可以分为两种,第一是带默认值的位置参数,第二是带默认值的强制关键字参数。比如

def func1(a, b=1, *args, **kw):
    pass

def func2(a, *args, b=1, **kw):
    pass

总之无论如何 **kw 都必须在所有参数的最后

讨论

默认参数的值仅仅在函数定义的时候赋值一次其后对默认参数的修改是没有作用的

>>> x = 42
>>> def spam(a, b=x):
... print(a, b)
...
>>> spam(1)
1 42
>>> x = 23 # Has no effect
>>> spam(1)
1 42

其次默认参数的值应该是不可变的对象,比如None、True、False、数字或字符串。不要把参数默认值设为 [].

在测试 None 值时使用 is 操作符是很重要的,也是这种方案的关键点。 有时候大家会犯下下面这样的错误

def spam(a, b=None):
    if not b: # NO! Use 'b is None' instead
        b = []

另外还有一个比较微妙的问题,就是测试某个可选参数是否被用户传递进来的时候,仅仅通过 None 0 或者 False 来测试是远远不够的。 一般来说可以创造一个特有的值来进行测试,比如这样

_no_value = object()
def spam(a, b=_no_value):
    if b is _no_value:
        print('No b value supplied')

7.6 定义匿名或内联函数

问题

你想为 sort() 操作创建一个很短的回调函数,但又不想用 def 去写一个单行函数, 而是希望通过某个快捷方式以内联方式来创建这个函数。

解决方案

可以使用lambda表达式来代替。lambda表达式典型的使用场景是排序或数据reduce等:

>>> names = ['David Beazley', 'Brian Jones',
...         'Raymond Hettinger', 'Ned Batchelder']
>>> sorted(names, key=lambda name: name.split()[-1].lower())

讨论

尽管lambda表达式允许你定义简单函数,但是它的使用是有限制的。 你只能指定单个表达式,它的值就是最后的返回值。你不能使用复杂的条件语句,迭代以及异常等

7.7 匿名函数捕获变量值

问题

你用lambda定义了一个匿名函数,并想在定义时捕获到某些变量的值。

解决方案

先看下下面代码的效果:

>>> x = 10
>>> a = lambda y: x + y
>>> x = 20
>>> b = lambda y: x + y
>>> a(10)
30  # 不是 20
>>> b(10)
30

这其中的奥妙在于lambda表达式中的 x 是一个自由变量, 在运行时绑定值,而不是定义时就绑定,这跟函数的默认值参数定义是不同的。 因此,在调用这个lambda表达式的时候,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

但是实际效果是运行是n的值为迭代的最后一个值。 通过使用函数默认值参数形式,lambda函数在定义时就能绑定到值。

>>> funcs = [lambda x, n=n: x+n for n in range(5)]
>>> for f in funcs:
... print(f(0))
...
0
1
2
3
4

7.8 减少可调用对象的参数个数

问题

你有一个被其他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

可以看出 partial() 固定某些参数并返回一个新的callable对象。这个新的callable接受未赋值的参数, 然后跟之前已经赋值过的参数合并起来,最后将所有参数传递给原始函数。

讨论

本节要解决的问题是让原本不兼容的代码可以一起工作。

partial() 通常被用来微调其他库函数所使用的回调函数的参数。

例如,下面是一段代码,使用 multiprocessing 来异步计算一个结果值, 然后这个值被传递给一个接受一个result值和一个可选logging参数的回调函数:

def output_result(result, log=None):
    if log is not None:
        log.debug('Got: %r', result)

# A sample function
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()

当给 apply_async() 提供回调函数时,通过使用 partial() 传递额外的 logging 参数。 而 multiprocessing 对这些一无所知——它仅仅只是使用单个值来调用回调函数。

很多时候 partial() 能实现的效果,lambda表达式也能实现。只是稍显臃肿

points.sort(key=lambda p: distance(pt, p))
p.apply_async(add, (3, 4), callback=lambda result: output_result(result,log))
serv = TCPServer(('', 15000),
lambda *args, **kwargs: EchoHandler(*args, ack=b'RECEIVED:',␣
,!**kwargs))

7.9 将单方法的类转换为函数

问题

你有一个除 init() 方法外只定义了一个方法的类。为了简化代码,你想将它转换成一个函数。

解决方案

大多数情况下,可以使用闭包来将单个方法的类转换成函数。 举个例子,下面示例中的类允许使用者根据某个模板方案来获取到URL链接地址。

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'))

这个类可以被一个更简单的函数来代替:

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'))

讨论

大部分情况下,你拥有一个单方法类的原因是需要存储某些额外的状态来给方法使用。

使用一个内部函数或者闭包的方案通常会更优雅一些。简单来讲,一个闭包就是一个函数, 只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境

7.10 带额外状态信息的回调函数

问题

你的代码中需要依赖到回调函数的使用(比如事件处理器、等待后台任务完成后的回调等), 并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。

解决方案

这一小节主要讨论的是那些出现在很多函数库和框架中的回调函数的使用——特别是跟异步处理有关的。

为了让回调函数访问外部信息

第一种方法是使用一个绑定方法来代替一个简单函数。 比如,下面这个类会保存一个内部序列号,每次接收到一个 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 make_handler():
    sequence = 0
    while True:
        result = yield
        sequence += 1
        print('[{}] Got: {}'.format(sequence, result))

记住要使用协程的 send() 方法作为回调函数

>>> handler = make_handler()
>>> next(handler) # Advance to the yield
>>> apply_async(add, (2, 3), callback=handler.send)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=handler.send)
[2] Got: helloworld

7.11 内联回调函数

本节需要认真理解

问题

当你编写使用回调函数的代码的时候,担心很多小函数的扩张可能会弄乱程序控制流。你希望找到某个方法来让代码看上去更像是一个普通的执行序列。

解决方案

通过使用生成器和协程可以使得回调函数内联在某个函数中。

假设有需求

def apply_async(func, args, *, callback):
    # Compute the result
    result = func(*args)

    # Invoke the callback with the result
    callback(result)

下面的代码,包含了一个 Async 类和一个 inlined_async 装饰器:

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

这两个代码片段允许你使用 yield 语句内联回调步骤。比如:

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()
5
helloworld
0
2468
10
12
14
16
18
Goodbye

首先,在需要使用到回调的代码中,关键点在于当前计算工作会挂起并在将来的某个时候重启(比如异步执行)。当计算重启时,回调函数被调用来继续处理结果。 apply_async() 函数演示了执行回调的实际逻辑,尽管实际情况中它可能会更加复杂(包括线程、进程、事件处理器等等)。计算的暂停与重启思路跟生成器函数的执行模型不谋而合。

具体来讲,yield 操作会使一个生成器函数产生一个值并暂停。接下来调用生成器的 next() 或 send() 方法又会让它从暂停处继续执行。根据这个思路,这一小节的核心就在inline_async() 装饰器函数中了。

关键点就是,装饰器会逐步遍历生成器函数的所有yield 语句,每一次一个。为了这样做,刚开始的时候创建了一个result 队列并向里面放入一个None 值。然后开始一个循环操作,从队列中取出结果值并发送给生成器,它会持续到下一个yield 语句,在这里一个Async 的实例被接受到。然后循环开始检查函数和参数,并开始进行异步计算 apply_async() 。然而,这个计算有个最诡异部分是它并没有使用一个普通的回调函数,而是用队列的put() 方法来回调。这时候,是时候详细解释下到底发生了什么了。主循环立即返回顶部并在队列上执行get() 操作。如果数据存在,它一定是put() 回调存放的结果。如果没有数据,那么先暂停操作并等待结果的到来。这个具体怎样实现是由apply_async() 函数来决定的

7.12 访问闭包中定义的变量

问题

你想要扩展函数中的某个闭包,允许它能访问和修改函数的内部变量。

解决方案

通常来讲,闭包的内部变量对于外界来讲是完全隐藏的。但是,你可以通过编写访问函数并将其作为函数属性绑定到闭包上来实现这个目的

def sample():
    n = 0
    # Closure function
    def func():
        print('n=', n)

    # Accessor methods for n
    def get_n():
        return n

    def set_n(value):
        nonlocal n
        n = value

    # Attach as function attributes
    func.get_n = get_n
    func.set_n = set_n
    return func

使用

>>> f = sample()
>>> f()
n= 0
>>> f.set_n(10)
>>> f()
n= 10
>>> f.get_n()
10

讨论

首先,nonlocal 声明可以让我们编写函数来修改内部变量的值。 其次,函数属性允许我们用一种很简单的方式将访问方法绑定到闭包函数上,这个跟实例方法很像(尽管并没有定义任何类)。

还可以进一步的扩展,让闭包模拟类的实例。

import sys
class ClosureInstance:
    def __init__(self, locals=None):
        if locals is None:
            locals = sys._getframe(1).f_locals

        # Update instance dictionary with callables
        self.__dict__.update((key,value) for key, value in locals.items()
                            if callable(value) )
    # Redirect special methods
    def __len__(self):
        return self.__dict__['__len__']()

# Example use
def Stack():
    items = []
    def push(item):
        items.append(item)

    def pop():
        return items.pop()

    def __len__():
        return len(items)

    return ClosureInstance()

代码中的 sys._getframe(1).f_locals 是获得当前栈中上一层函数的方法。也可以指定 flocals 关键字参数。 工作

>>> s = Stack()
>>> s
<__main__.ClosureInstance object at 0x10069ed10>
>>> s.push(10)
>>> s.push(20)
>>> s.push('Hello')
>>> len(s)
3
>>> s.pop()
'Hello'

上面代码的作用和类其实已经很像了,但是经过测试这样的代码会比类要快一些,大概是因为不会涉及到额外的 self 变量