Python代码结构:函数、生成器和装饰器

Published on 2017 - 01 - 22

函数

代码复用的第一步是使用函数,它是命名的用于区分的代码段。函数可以接受任何数字或者其他类型的输入作为参数,并且返回数字或者其他类型的结果。

你可以使用函数做以下两件事情:

  • 定义函数
  • 调用函数

为了定义 Python 函数,你可以依次输入 def、函数名、带有函数参数的圆括号,最后紧跟一个冒号(:)。函数命名规范和变量命名一样(必须使用字母或者下划线 _ 开头,仅能含有字母、数字和下划线)。

我们先定义和调用一个没有参数的函数。下面的例子是最简单的 Python 函数:

>>> def do_nothing():
...         pass

即使对于一个没有参数的函数,仍然需要在定义时加上圆括号和冒号。下面的一行需要像声明 if 语句一样缩进。Python 函数中的 pass 表明函数没有做任何事情。和这一页故意留白有同样的作用(即使它不再是)。

通过输入函数名和参数调用此函数,像前面说的一样,它没有做任何事情:

>>> do_nothing()
>>>

现在,定义一个无参数,但打印输出一个单词的函数:

>>> def make_a_sound():
...         print('quack')
...
>>> make_a_sound()
quack

当调用 make_a_sound() 函数时,Python 会执行函数内部的代码。在这个例子中,函数打印输出单个词,并且返回到主程序。

下面尝试一个没有参数但返回值的函数:

>>> def agree():
...         return True
...

或者,调用这个函数,使用 if 语句检查它的返回值:

>>> if agree():
...         print('Splendid!')
...     else:
...         print('That was unexpected.')
...
Splendid!

学到现在已经迈出了很大的一步。在函数中,使用 if 判断和 for/while 循环组合能实现之前无法实现的功能。

这个时候该在函数中引入参数。定义带有一个 anything 参数的函数 echo()。它使用 return 语句将 anything 返回给它的调用者两次,并在两次中间加入一个空格:

>>> def echo(anything):
...         return anything + ' ' + anything
...
>>>

然后用字符串 'Rumplestiltskin' 调用函数 echo():

>>> echo('Rumplestiltskin')
'Rumplestiltskin Rumplestiltskin'

传入到函数的值称为参数。当调用含参数的函数时,这些参数的值会被复制给函数中的对应参数。在之前的例子中,被调用的函数 echo() 的传入参数字符串是 'Rumplestiltskin',这个值被复制给参数 anything , 然后返回到调用方(在这个例子中,输出两次字符串,中间有一个空格)。

上面的这些函数例子都很基础。现在我们写一个含有输入参数的函数,它能真正处理一些事情。在这里依旧沿用评论颜色的代码段。调用 commentary 函数,把 color 作为输入的参数,使它返回对颜色的评论字符串:

>>> def commentary(color):
...         if color == 'red':
...             return "It's a tomato."
...         elif color == "green":
...             return "It's a green pepper."
...         elif color == 'bee purple':
...             return "I don't know what it is, but only bees can see it."
...         else:
...             return "I've never heard of the color "  + color +  "."
...
>>>

传入字符串参数 'blue',调用函数 commentary():

>>> comment = commentary('blue')

这个函数做了以下事情:

  • 把 'blue' 赋值给函数的内部参数 color
  • 运行 if-elif-else 的逻辑链
  • 返回一个字符串
  • 将该字符串赋值给变量 comment

我们如何得到返回值呢?

>>> print(comment)
I've never heard of the color blue.

一个函数可以接受任何数量(包括 0)的任何类型的值作为输入变量,并且返回任何数量(包括 0)的任何类型的结果。如果函数不显式调用 return 函数,那么会默认返回 None。

>>> print(do_nothing())
None

有用的 None

None 是 Python 中一个特殊的值,虽然它不表示任何数据,但仍然具有重要的作用。虽然 None 作为布尔值和 False 是一样的,但是它和 False 有很多差别。下面是一个例子:

>>> thing = None
>>> if thing:
...         print("It's some thing")
... else:
...         print("It's no thing")
...
It's no thing

为了区分 None 和布尔值 False , 使用 Python 的 is 操作符:

>>> if thing is None:
...         print("It's nothing")
... else:
...         print("It's something")
...
It's nothing

这虽然是一个微妙的区别,但是对于 Python 来说是很重要的。你需要把 None 和不含任何值的空数据结构区分开来。0 值的整型 / 浮点型、空字符串('')、空列表([])、空元组((,))、空字典({})、空集合(set())都等价于 False,但是不等于 None。

位置参数

Python 处理参数的方式要比其他语言更加灵活。其中,最熟悉的参数类型是位置参数,传入参数的值是按照顺序依次复制过去的。

下面创建一个带有位置参数的函数,并且返回一个字典:

>>> def menu(wine, entree, dessert):
...         return {'wine': wine, 'entree': entree, 'dessert': dessert}
...
>>> menu('chardonnay', 'chicken', 'cake')
{'dessert': 'cake', 'wine': 'chardonnay', 'entree': 'chicken'}

尽管这种方式很常见,但是位置参数的一个弊端是必须熟记每个位置的参数的含义。在调用函数 menu() 时误把最后一个参数当作第一个参数,会得到完全不同的结果:

>>> menu('beef', 'bagel', 'bordeaux')
{'dessert': 'bordeaux', 'wine': 'beef', 'entree': 'bagel'}

关键字参数

为了避免位置参数带来的混乱,调用参数时可以指定对应参数的名字,甚至可以采用与函数定义不同的顺序调用:

>>> menu(entree='beef', dessert='bagel', wine='bordeaux')
{'dessert': 'bagel', 'wine': 'bordeaux', 'entree': 'beef'}

你也可以把位置参数和关键字参数混合起来。首先,实例化参数 wine,然后对参数 entree 和 dessert 使用关键字参数的方式:

>>> menu('frontenac', dessert='flan', entree='fish')
{'entree': 'fish', 'dessert': 'flan', 'wine': 'frontenac'}

如果同时出现两种参数形式,首先应该考虑的是位置参数。

指定默认参数值

当调用方没有提供对应的参数值时,你可以指定默认参数值。这个听起来很普通的特性实际上特别有用,以之前的例子为例:

>>> def menu(wine, entree, dessert='pudding'):
...         return {'wine': wine, 'entree': entree, 'dessert': dessert}

这一次调用不带 dessert 参数的函数 menu():

>>> menu('chardonnay', 'chicken')
{'dessert': 'pudding', 'wine': 'chardonnay', 'entree': 'chicken'}

如果你提供参数值,在调用时会代替默认值:

>>> menu('dunkelfelder', 'duck', 'doughnut')
{'dessert': 'doughnut', 'wine': 'dunkelfelder', 'entree': 'duck'}

使用*收集位置参数

如果你之前使用 C/C++ 编程,可能会认为 Python 中的星号(*)和指针相关。然而,Python 是没有指针概念的。

当参数被用在函数内部时,星号将一组可变数量的位置参数集合成参数值的元组。在下面的例子中 args 是传入到函数 print_args() 的参数值的元组:

>>> def print_args(*args):
...         print('Positional argument tuple:', args)
...

无参数调用函数,则什么也不会返回:

>>> print_args()
Positional argument tuple: ()

给函数传入的所有参数都会以元组的形式返回输出:

>>> print_args(3, 2, 1, 'wait!', 'uh...')
Positional argument tuple: (3, 2, 1, 'wait!', 'uh...')

这样的技巧对于编写像 print() 一样接受可变数量的参数的函数是非常有用的。如果你的函数同时有限定的位置参数,那么 *args 会收集剩下的参数:

>>> def print_more(required1, required2, *args):
...         print('Need this one:', required1)
...         print('Need this one too:', required2)
...         print('All the rest:', args)
...
>>> print_more('cap', 'gloves', 'scarf', 'monocle', 'mustache wax')
Need this one: cap
Need this one too: gloves
All the rest: ('scarf', 'monocle', 'mustache wax')

当使用 * 时不需要调用元组参数 args,不过这也是 Python 的一个常见做法。

使用**收集关键字参数

使用两个星号可以将参数收集到一个字典中,参数的名字是字典的键,对应参数的值是字典的值。下面的例子定义了函数 print_kwargs(),然后打印输出它的关键字参数:

>>> def print_kwargs(**kwargs):
...         print('Keyword arguments:', kwargs)
...

现在,使用一些关键字参数调用函数:

>>> print_kwargs(wine='merlot', entree='mutton', dessert='macaroon')
Keyword arguments: {'dessert': 'macaroon', 'wine': 'merlot', 'entree': 'mutton'}

在函数内部,kwargs 是一个字典。

如果你把带有 *args 和 **kwargs 的位置参数混合起来,它们会按照顺序解析。和 args 一样,调用时不需要参数 kwargs,这也是常见用法。

文档字符串

正如《Python 之禅》(the Zen of Python)中提到的,程序的可读性很重要。建议在函数体开始的部分附上函数定义说明的文档,这就是函数的文档字符串:

>>> def echo(anything):
...         'echo returns its input argument'
...         return anything

可以定义非常长的文档字符串,加上详细的规范说明,如下所示:

def print_if_true(thing, check):
    '''
    Prints the first argument if a second argument is true.
    The operation is:
        1. Check whether the *second* argument is true.
        2. If it is, print the *first* argument.
    '''
    if check:
        print(thing)

调用 Python 函数 help() 可以打印输出一个函数的文档字符串。把函数名传入函数 help() 就会得到参数列表和规范的文档:

>>> help(echo)
Help on function echo in module __main__:

echo(anything)
    echo returns its input argument

如果仅仅想得到文档字符串:

>>> print(echo.__doc__)
echo returns its input argument

看上去很奇怪的 __doc__ 是作为函数中变量的文档字符串的名字。

一等公民:函数

之前提过 Python 中一切都是对象,包括数字、字符串、元组、列表、字典和函数。函数是 Python 中的一等公民,可以把它们(返回值)赋给变量,可以作为参数被其他函数调用,也可以从其他函数中返回值。它可以帮助你在 Python 中实现其他语言难以实现的功能。

为了测试,现在定义一个简单的函数 answer(),它没有任何参数,仅仅打印输出数字 42:

>>> def answer():
...         print(42)

运行该函数,会得到下面的结果:

>>> answer()
42

再定义一个函数 run_something。它有一个参数 func,这个参数是一个可以运行的函数的名字:

>>> def run_something(func):
...         func()

将参数 answer 传到该函数,在这里像之前碰到的一样,把函数名当作数据使用:

>>> run_something(answer)
42

我们来运行一个带参数的例子。定义函数 add_args(),它会打印输出两个数值参数(arg1 和 arg2)的和:

>>> def add_args(arg1, arg2):
...         print(arg1 + arg2)

那么,add_args() 的类型是什么?

>>> type(add_args)
<class 'function'>

此刻定义一个函数 run_something_with_args(),它带有三个参数:

  • func——可以运行的函数
  • arg1——func 函数的第一个参数
  • arg2——func 函数的第二个参数
>>> def run_something_with_args(func, arg1, arg2):
...         func(arg1, arg2)

当调用 run_something_with_args() 时,调用方传来的函数赋值给 func 参数,而 arg1 和 arg2 从参数列表中获得值。然后运行带参数的 func(arg1, arg2)。

将函数名 add_args 和参数 5、9 传给函数 run_something_with_args():

>>> run_something_with_args(add_args, 5, 9)
14

在函数 run_something_with_args() 内部,函数名 add_args 被赋值给参数 func,5 和 9 分别赋值给 arg1 和 arg2。程序最后执行:

add_args(5, 9)

同样可以在此用上 *args(位置参数收集)和 **kwargs(关键字参数收集)的技巧。

我们定义一个测试函数,它可以接受任意数量的位置参数,使用 sum() 函数计算它们的和,并返回这个和:

>>> def sum_args(*args):
...     return sum(args)

之前没有提到 sum() 函数,它是 Python 的一个内建函数,用来计算可迭代的数值(整型或者浮点型)参数的和。

下面再定义一个新的函数 run_with_positional_args(),接收一个函数名以及任意数量的位置参数:

>>> def run_with_positional_args(func, *args):
...    return func(*args)

现在直接调用它:

>>> run_with_positional_args(sum_args, 1, 2, 3, 4)
10

同样可以把函数作为列表、元组、集合和字典的元素。函数名是不可变的,因此可以把函数用作字典的键。

内部函数

在 Python 中,可以在函数中定义另外一个函数:

>>> def outer(a, b):
...         def inner(c, d):
...             return c + d
...         return inner(a, b)
...
>>>
>>> outer(4, 7)
11

当需要在函数内部多次执行复杂的任务时,内部函数是非常有用的,从而避免了循环和代码的堆叠重复。对于这样一个字符串的例子,内部函数的作用是给外部的函数增加字符串参数:

>>> def knights(saying):
...         def inner(quote):
...             return "We are the knights who say: '%s'" % quote
...         return inner(saying)
...
>>> knights('Ni!')
"We are the knights who say: 'Ni!'"

闭包

内部函数可以看作一个闭包。闭包是一个可以由另一个函数动态生成的函数,并且可以改变和存储函数外创建的变量的值。

下面的例子以之前的 knights() 为基础。现在,调用新的函数 knight2(),把 inner() 函数变成一个叫 inner2() 的闭包。可以看出有以下不同点。

  • inner2() 直接使用外部的 saying 参数,而不是通过另外一个参数获取。
  • knights2() 返回值为 inner2 函数,而不是调用它。
>>> def knights2(saying):
...         def inner2():
...                 return "We are the knights who say: '%s'" % saying
...         return inner2
...

inner2() 函数可以得到 saying 参数的值并且记录下来。return inner2 这一行返回的是 inner2 函数的复制(没有直接调用)。所以它就是一个闭包:一个被动态创建的可以记录外部变量的函数。

用不同的参数调用 knights2() 两次:

>>> a = knights2('Duck')
>>> b = knights2('Hasenpfeffer')

那么 a 和 b 会是什么类型?

>>> type(a)
<class 'function'>
>>> type(b)
<class 'function'>

它们是函数,同时也是闭包:

>>> a
<function knights2.<locals>.inner2 at 0x10193e158>
>>> b
<function knights2.<locals>.inner2 at 0x10193e1e0>

如果调用它们,它们会记录被 knights2 函数创建时的外部变量 saying:

>>> a()
"We are the knights who say: 'Duck'"
>>> b()
"We are the knights who say: 'Hasenpfeffer'"

匿名函数:lambda()函数

Python 中,lambda 函数是用一个语句表达的匿名函数。可以用它来代替小的函数。

首先,举一个使用普通函数的例子。定义函数 edit_story(),参数列表如下所示:

  • words——单词列表
  • func——遍历列表中单词的函数
>>> def edit_story(words, func):
...         for word in words:
...             print(func(word))

现在,需要一个单词列表和一个遍历单词的函数。对于单词,可以选择我的猫从某一台阶上掉下时发出的声音:

>>> stairs = ['thud', 'meow', 'thud', 'hiss']

对于函数,它要将每个单词的首字母变为大写,然后在末尾加上感叹号,用作猫画报的标题非常完美:

>>> def enliven(word):   # 让这些单词更有情感
...         return word.capitalize() + '!'

混合这些“配料”:

>>> edit_story(stairs, enliven)
Thud!
Meow!
Thud!
Hiss!

最后,到了 lambda。enliven() 函数可以简洁地用下面的一个 lambda 代替:

>>>
>>> edit_story(stairs, lambda word: word.capitalize() + '!')
Thud!
Meow!
Thud!
Hiss!
>>>

lambda 函数接收一个参数 word。在冒号和末尾圆括号之间的部分为函数的定义。

通常,使用实际的函数(例如 enliven())会比使用 lambda 更清晰明了。但是,当需要定义很多小的函数以及记住它们的名字时,lambda 会非常有用。尤其是在图形用户界面中,可以使用 lambda 来定义回调函数。

生成器

生成器是用来创建 Python 序列的一个对象。使用它可以迭代庞大的序列,且不需要在内存中创建和存储整个序列。通常,生成器是为迭代器产生数据的。回想起来,我们已经在之前的例子中使用过其中一个,即 range(),来产生一系列整数。range() 在 Python 2 中返回一个列表,这也限制了它要进入内存空间。Python 2 中同样存在的生成器 xrange() 在 Python 3 中成为标准的 range() 生成器。这个例子累加从 1 到 100 的整数:

>>> sum(range(1, 101))
5050

每次迭代生成器时,它会记录上一次调用的位置,并且返回下一个值。这一点和普通的函数是不一样的,一般函数都不记录前一次调用,而且都会在函数的第一行开始执行。

如果你想创建一个比较大的序列,使用生成器推导的代码会很长,这时可以尝试写一个生成器函数。生成器函数和普通函数类似,但是它的返回值使用 yield 语句声明而不是 return。下面编写我们自己的 range() 函数版本:

>>> def my_range(first=0, last=10, step=1):
...     number = first
...     while number < last:
...         yield number
...         number += step
...

这是一个普通的函数:

>>> my_range
<function my_range at 0x10193e268>

并且它返回的是一个生成器对象:

>>> ranger = my_range(1, 5)
>>> ranger
<generator object my_range at 0x101a0a168>

可以对这个生成器对象进行迭代:

>>> for x in ranger:
...        print(x)
...
1
2
2
4

装饰器

有时你需要在不改变源代码的情况下修改已经存在的函数。常见的例子是增加一句调试声明,以查看传入的参数。

装饰器实质上是一个函数。它把一个函数作为输入并且返回另外一个函数。在装饰器中,通常使用下面这些 Python 技巧:

  • *args 和 **kwargs
  • 闭包
  • 作为参数的函数

函数 document_it() 定义了一个装饰器,会实现如下功能:

  • 打印输出函数的名字和参数的值
  • 执行含有参数的函数
  • 打印输出结果
  • 返回修改后的函数

看下面的代码:

>>> def document_it(func):
...         def new_function(*args, **kwargs):
...             print('Running function:', func.__name__)
...             print('Positional arguments:', args)
...             print('Keyword arguments:', kwargs)
...             result = func(*args, **kwargs)
...             print('Result:', result)
...             return result
...         return new_function

无论传入 document_it() 的函数 func 是什么,装饰器都会返回一个新的函数,其中包含函数 document_it() 增加的额外语句。实际上,装饰器并不需要执行函数 func 中的代码,只是在结束前函数 document_it() 调用函数 func 以便得到 func 的返回结果和附加代码的结果。

那么,如何使用装饰器?当然,可以通过人工赋值:

>>> def add_ints(a, b):
...         return a + b
...
>>> add_ints(3, 5)
8
>>> cooler_add_ints = document_it(add_ints)  # 人工对装饰器赋值
>>> cooler_add_ints(3, 5)
Running function: add_ints
Postitional arguments: (3, 5)
Keyword arguments: {}
Result: 8
8

作为对前面人工装饰器赋值的替代,可以直接在要装饰的函数前添加装饰器名字 @decorator_name:

>>> @document_it
... def add_ints(a, b):
...     return a + b
...
>>> add_ints(3, 5)
Start function add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
8

同样一个函数可以有多个装饰器。下面,我们写一个对结果求平方的装饰器 square_it():

>>> def square_it(func):
...         def new_function(*args, **kwargs):
...             result = func(*args, **kwargs)
...             return result * result
...         return new_function
...

靠近函数定义(def 上面)的装饰器最先执行,然后依次执行上面的。任何顺序都会得到相同的最终结果。下面的例子中会看到中间步骤的变化:

>>> @document_it
... @square_it
... def add_ints(a, b):
...     return a + b
...
>>> add_ints(3, 5)
Running function: new_function
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 64
64

交换两个装饰器的顺序:

>>> @square_it
... @document_it
... def add_ints(a, b):
...     return a + b
...
>>> add_ints(3, 5)
Running function: add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
64

参考文档