第 4 章 Python 外壳:代码结构

在前三章中,我们用到很多关于数据的例子,但是没有对它们进行复杂的操作。大多数的示例代码都很短,并且使用交互式解释器进行解释执行。本章将介绍如何组织代码和数据。

许多计算机编程语言使用字符(例如花括号 {})或者关键字(例如 beginend)来划分代码段。在这些语言中,使用一致的代码缩进可以增加代码的可读性,并且有很多便利的工具整理缩进代码。

在吉多 · 范 · 罗苏姆开始考虑设计 Python 语言时,他决定通过代码缩进来区分代码块结构,避免输入太多的花括号和关键字。Python 使用空白来区分代码结构,这是初学者需要注意的不同寻常的第一点,而且有其他语言开发经验的人会觉得奇怪。但使用 Python 一段时间后会觉得很自然,而且会习惯于编写简洁的代码来进行大量的编程工作。

4.1 使用#注释

注释是程序中会被 Python 解释器忽略的一段文本。通过使用注释,可以解释和明确 Python 代码的功能,记录将来要修改的地方,甚至写下你想写的任何东西。在 Python 中使用 # 字符标记注释,从 # 开始到当前行结束的部分都是注释。你可以把注释作为单独的一行,如下所示:

  1. >>> # 60 sec/min 60 min/hr 24 hr/day
  2. >>> seconds_per_day = 86400

也可以把注释和代码放在同一行:

  1. >>> seconds_per_day = 86400 # 60 sec/min 60 min/hr 24 hr/day

# 有很多含义和叫法:hash、sharp、pound 或者 octothorpe1。无论你如何称呼它 2,在 Python 代码中,注释的作用只对它出现的当前行有效。

1样子像在你身边的凶恶的八脚怪物。

2嘘,别叫它,小心它回来找你。

Python 没有多行注释的符号。你需要明确地在注释部分的每一行开始处加上一个 #

  1. >>> # 尽管Python不会喜欢,但是我可以在这里讲任何东西
  2. ... # 因为我被“保护”
  3. ... # 令人敬畏的#号
  4. ...
  5. >>>

然而,如果它出现在文本串中,将回归普通字符 # 的角色:

  1. >>> print("No comment: quotes make the # harmless.")
  2. No comment: quotes make the # harmless.

4.2 使用\连接

程序在合理的长度下是易读的。一行程序的(非强制性)最大长度建议为 80 个字符。如果你在该长度下写不完你的代码,可以使用连接符 \(反斜线)。把它放在一行的结束位置,Python 仍然将其解释为同一行。

例如,假设想把一些短字符串拼接为一个长字符串,可以按照下面的步骤:

  1. >>> alphabet = ''
  2. >>> alphabet += 'abcdefg'
  3. >>> alphabet += 'hijklmnop'
  4. >>> alphabet += 'qrstuv'
  5. >>> alphabet += 'wxyz'

或者,使用连接符一步就可以完成:

  1. >>> alphabet = 'abcdefg' + \
  2. ... 'hijklmnop' + \
  3. ... 'qrstuv' + \
  4. ... 'wxyz'

在 Python 表达式占很多行的情况下,行连接符也是必需的:

  1. >>> 1 + 2 +
  2. File "<stdin>", line 1
  3. 1 + 2 +
  4. ^
  5. SyntaxError: invalid syntax
  6. >>> 1 + 2 + \
  7. ... 3
  8. 6
  9. >>>

4.3 使用ifelifelse进行比较

到目前为止,我们几乎一直在讨论数据结构。现在,我们将迈出探讨代码结构的第一步,将数据编排成代码(上一章讲到集合时已经见到一些例子,希望那些不会造成理解困难)。下面第一个例子是一个 Python 小程序,判断一个布尔变量 disaster 的值,然后打印输出合适的取值:

  1. >>> disaster = True
  2. >>> if disaster:
  3. ... print("Woe!")
  4. ... else:
  5. ... print("Whee!")
  6. ...
  7. Woe!
  8. >>>

程序中,ifelse 两行是 Python 用来声明判断条件(本例中是 disaster 的值)是否满足的语句print() 是将字符打印到屏幕的 Python 内建函数

第 4 章 Python 外壳:代码结构 - 图1 如果你之前使用过其他编程语言,不需要在 if 判断语句中加上圆括号,例如 if (disaster == True) 是没必要的,但在判断的末尾要加上冒号(:)。如果你像我一样有时会忘记输入冒号,这会导致 Python 解释器报错。

每一个 print() 在判断语句之后要缩进。我一般缩进四个空格。尽管你可以使用你喜欢的任何方式缩进,但是在同一代码段内最好使用一致的缩进——从每一行的左边开始使用相同数量的缩进字符。推荐的代码缩进风格 PEP-8(http://legacy.python.org/dev/peps/pep-0008/)同样使用四个空格。避免使用 Tab 字符或者 Tab 与 Space 混合的缩进风格,这样会使缩进字符的数量变得混乱。

上面的示例代码做了以下事情,在随后的内容中我还会更详细地介绍。

  • 将布尔值 True 赋值给变量 disaster

  • 使用 ifelse 进行条件比较判断,根据 disaster 的值执行不同的代码。

  • 调用 print() 函数,在屏幕打印文本。

同时你可以根据需要进行多层判断语句的嵌套:

  1. >>> furry = True
  2. >>> small = True
  3. >>> if furry:
  4. ... if small:
  5. ... print("It's a cat.")
  6. ... else:
  7. ... print("It's a bear!")
  8. ... else:
  9. ... if small:
  10. ... print("It's a skink!")
  11. ... else:
  12. ... print("It's a human. Or a hairless bear.")
  13. ...
  14. It's a cat.

在 Python 中,代码缩进决定了 ifelse 是如何配对的。在第一个判断 furry 中,因为 furry 的值是 True,所以程序跳转到执行判断 if small。我们之前将 small 赋值为 True,所以 if small 的值被估计为 True。因此程序会执行它的下一行,输出 It's a cat

如果要检查超过两个条件,可以使用 ifelif(即 else if)和 else

  1. >>> color = "puce"
  2. >>> if color == "red":
  3. ... print("It's a tomato")
  4. ... elif color == "green":
  5. ... print("It's a green pepper")
  6. ... elif color == "bee purple":
  7. ... print("I don't know what it is, but only bees can see it")
  8. ... else:
  9. ... print("I've never heard of the color", color)
  10. ...
  11. I've never heard of the color puce

上面的例子中,我们使用了 == 作为判断相等的操作符,Python 中的比较操作符见下表。

相等==
不等于!=
小于<
不大于<=
大于>
不小于>=
属于in…

这些操作符都返回布尔值 True 或者 False。让我们看看这些是如何执行的,首先对变量 x

赋值:

  1. >>> x = 7

现在,测试一些例子:

  1. >>> x == 5
  2. False
  3. >>> x == 7
  4. True
  5. >>> 5 < x
  6. True
  7. >>> x < 10
  8. True

注意,两个等号(==)是用来判断相等的,而一个等号(=)是把某个值赋给一个变量。

如果你想同时进行多重比较判断,可以使用布尔操作符 andor 或者 not 连接来决定最终表达式的布尔取值。

布尔操作符的优先级没有比较表达式的代码段高,也就是说,表达式要先计算然后再比较。在这个例子中,x 赋值为 75 < x 返回 Truex < 10 也同样返回 True, 最终的结果就是 True and True

  1. >>> 5 < x and x < 10
  2. True

2.2.2 节中提到,避免混淆的最简单的办法是加圆括号:

  1. >>> ( 5 < x ) and ( x < 10)
  2. True

下面是其他的一些例子:

  1. >>> 5 < x or x < 10
  2. True
  3. >>> 5 < x and x > 10
  4. False
  5. >>> 5 < x and not x > 10
  6. True

如果对同一个变量做多个 and 比较操作,Python 允许下面的用法:

  1. >>> 5 < x < 10
  2. True

这个表达式和 5 < x and x < 10 是一样的,你也可以使用更长的比较:

  1. >>> 5 < x < 10 < 999
  2. True

什么是真值(True

如果表达式的返回类型不是布尔会发生什么?什么情况下 Python 会认为是 TrueFalse?

一个成假赋值不一定明确表示为 False,下面的情况也会被认为是 False

布尔False
null 类型None
整型0
浮点型0.0
空字符串''
空列表[]
空元组()
空字典{}
空集合set()

剩下的都会被认为是 True。Python 程序中使用定义“真值”(在本例中是“假值”)的方式来判断数据结构是否为空以及成假条件:

  1. >>> some_list = []
  2. >>> if some_list:
  3. ... print("There's something in here")
  4. ... else:
  5. ... print("Hey, it's empty!")
  6. ...
  7. Hey, it's empty!

如果你在判断一个表达式而不是一个简单的变量,Python 会先计算表达式的值,然后返回布尔型结果。所以,如果你输入以下的式子:

  1. if color == "red":

Python 会判断 color == "red"。示例中,已把字符串 "puce" 赋值给 color,所以 color == "red"False,Python 转而执行下面的判断:

  1. elif color == "green":

4.4 使用while进行循环

使用 ifelifelse 条件判断的例子是自顶向下执行的,但是有时候我们需要重复一些操作——循环。Python 中最简单的循环机制是 while。打开交互式解释器,执行下面的从 1 打印到 5 的简单循环:

  1. >>> count = 1
  2. >>> while count <= 5:
  3. ... print(count)
  4. ... count += 1
  5. ...
  6. 1
  7. 2
  8. 3
  9. 4
  10. 5
  11. >>>

首先将变量 count 的值赋为 1while 循环比较 count 的值和 5 的大小关系,如果 count 小于等于 5 的话继续执行。在循环内部,打印 count 变量的值,然后使用语句 count += 1count 进行自增操作,返回到循环的开始位置,继续比较 count5 的大小关系。现在,count 变量的值为 2,因此 while 循环内部的代码会被再次执行,count 值变为 3

count5 自增到 6 之前循环一直进行。然后下次判断时,count <= 5 的条件不满足,while 循环结束。Python 跳到循环下面的代码。

4.4.1 使用break跳出循环

如果你想让循环在某一条件下停止,但是不确定在哪次循环跳出,可以在无限循环中声明 break 语句。这次,我们通过 Python 的 input() 函数从键盘输入一行字符串,然后将字符串首字母转化成大写输出。当输入的一行仅含有字符 q 时,跳出循环 :

  1. >>> while True:
  2. ... stuff = input("String to capitalize [type q to quit]: ")
  3. ... if stuff == "q":
  4. ... break
  5. ... print(stuff.capitalize())
  6. ...
  7. String to capitalize [type q to quit]: test
  8. Test
  9. String to capitalize [type q to quit]: hey, it works
  10. Hey, it works
  11. String to capitalize [type q to quit]: q
  12. >>>

4.4.2 使用continue跳到循环开始

有时我们并不想结束整个循环,仅仅想跳到下一轮循环的开始。下面是一个编造的例子:读入一个整数,如果它是奇数则输出它的平方数;如果是偶数则跳过。同样使用 q 来结束循环,代码中加上了适当的注释:

  1. >>> while True:
  2. ... value = input("Integer, please [q to quit]: ")
  3. ... if value == 'q': # 停止循环
  4. ... break
  5. ... number = int(value)
  6. ... if number % 2 == 0: # 判断偶数
  7. ... continue
  8. ... print(number, "squared is", number*number)
  9. ...
  10. Integer, please [q to quit]: 1
  11. 1 squared is 1
  12. Integer, please [q to quit]: 2
  13. Integer, please [q to quit]: 3
  14. 3 squared is 9
  15. Integer, please [q to quit]: 4
  16. Integer, please [q to quit]: 5
  17. 5 squared is 25
  18. Integer, please [q to quit]: q
  19. >>>

4.4.3 循环外使用else

如果 while 循环正常结束(没有使用 break 跳出),程序将进入到可选的 else 段。当你使用循环来遍历检查某一数据结构时,找到满足条件的解使用 break 跳出;循环结束,即没有找到可行解时,将执行 else 部分代码段:

  1. >>> numbers = [1, 3, 5]
  2. >>> position = 0
  3. >>> while position < len(numbers):
  4. ... number = numbers[position]
  5. ... if number % 2 == 0:
  6. ... print('Found even number', number)
  7. ... break
  8. ... position += 1
  9. ... else: #没有执行break
  10. ... print('No even number found')
  11. ...
  12. No even number found

第 4 章 Python 外壳:代码结构 - 图2 else 在此处的用法不是很直观,可以认为是循环中没有调用 break 后执行的检查。

4.5 使用for迭代

Python 频繁地使用迭代器。它允许在数据结构长度未知和具体实现未知的情况下遍历整个数据结构,并且支持迭代快速读写中的数据,以及允许不能一次读入计算机内存的数据流的处理。

下面这一遍历序列的方法是可行的:

  1. >>> rabbits = ['Flopsy', 'Mopsy', 'Cottontail', 'Peter']
  2. >>> current = 0
  3. >>> while current < len(rabbits):
  4. ... print(rabbits[current])
  5. ... current += 1
  6. ...
  7. Flopsy
  8. Mopsy
  9. Cottontail
  10. Peter

但是,有一种更优雅的、Python 风格的遍历方式:

  1. >>> for rabbit in rabbits:
  2. ... print(rabbit)
  3. ...
  4. Flopsy
  5. Mopsy
  6. Cottontail
  7. Peter

列表(例如 rabbits)、字符串、元组、字典、集合等都是 Python 中可迭代的对象。元组或者列表在一次迭代过程产生一项,而字符串迭代会产生一个字符,如下所示:

  1. >>> word = 'cat'
  2. >>> for letter in word:
  3. ... print(letter)
  4. ...
  5. c
  6. a
  7. t

对一个字典(或者字典的 keys() 函数)进行迭代将返回字典中的键。在下面的例子中,字典的键为图板游戏 Clue(《妙探寻凶》)中牌的类型:

  1. >>> accusation = {'room': 'ballroom', 'weapon': 'lead pipe',
  2. 'person': 'Col. Mustard'}
  3. >>> for card in accusation: # 或者是for card in accusation.keys():
  4. ... print(card)
  5. ...
  6. room
  7. weapon
  8. person

如果想对字典的值进行迭代,可以使用字典的 values() 函数:

  1. >>> for value in accusation.values():
  2. ... print(value)
  3. ...
  4. ballroom
  5. lead pipe
  6. Col. Mustard

为了以元组的形式返回键值对,可以使用字典的 items() 函数:

  1. >>> for item in accusation.items():
  2. ... print(item)
  3. ...
  4. ('room', 'ballroom')
  5. ('weapon', 'lead pipe')
  6. ('person', 'Col. Mustard')

记住,元组只能被初始化一次,它的值是不能改变的。对于调用函数 items() 返回的每一个元组,将第一个返回值(键)赋给 card,第二个返回值(值)赋给 contents

  1. >>> for card, contents in accusation.items():
  2. ... print('Card', card, 'has the contents', contents)
  3. ...
  4. Card weapon has the contents lead pipe
  5. Card person has the contents Col. Mustard
  6. Card room has the contents ballroom

4.5.1 使用break跳出循环

for 循环中跳出的用法和在 while 循环中是一样的。

4.5.2 使用continue跳到循环开始

在一个循环中使用 continue 会跳到下一次的迭代开始,这一点和 while 循环也是类似的。

4.5.3 循环外使用else

类似于 whilefor 循环也可以使用可选的 else 代码段,用来判断 for 循环是否正常结束(没有调用 break 跳出)。若正常结束,则会执行 else 段。

当你想确认之前的 for 循环是否正常跑完,增加 else 判断是有用的。下面的例子中,for 循环打印输出奶酪的名称,并且如果任一奶酪在商店中找到则跳出循环:

  1. >>> cheeses = []
  2. >>> for cheese in cheeses:
  3. ... print('This shop has some lovely', cheese)
  4. ... break
  5. ... else: # 没有break表示没有找到奶酪
  6. ... print('This is not much of a cheese shop, is it?')
  7. ...
  8. This is not much of a cheese shop, is it?

第 4 章 Python 外壳:代码结构 - 图3 在 for 循环外使用 else 可能和 while 循环一样,不是很直观和容易理解。下面的理解方式会更清楚:for 循环用来遍历查找,如果没有找到则调用执行 else。同样在没有 else 的情况下,为了达到相同的作用,可以声明某个变量指出在 for 循环中是否找到,看下面的例子:

  1. >>> cheeses = []
  2. >>> found_one = False
  3. >>> for cheese in cheeses:
  4. found_one = True
  5. print('This shop has some lovely', cheese)
  6. break
  7. >>> if not found_one:
  8. print('This is not much of a cheese shop, is it?')
  9. This is not much of a cheese shop, is it?

4.5.4 使用zip()并行迭代

在使用迭代时,有一个非常方便的技巧:通过 zip() 函数对多个序列进行并行迭代:

  1. >>> days = ['Monday', 'Tuesday', 'Wednesday']
  2. >>> fruits = ['banana', 'orange', 'peach']
  3. >>> drinks = ['coffee', 'tea', 'beer']
  4. >>> desserts = ['tiramisu', 'ice cream', 'pie', 'pudding']
  5. >>> for day, fruit, drink, dessert in zip(days, fruits, drinks, desserts):
  6. ... print(day, ": drink", drink, "- eat", fruit, "- enjoy", dessert)
  7. ...
  8. Monday : drink coffee - eat banana - enjoy tiramisu
  9. Tuesday : drink tea - eat orange - enjoy ice cream
  10. Wednesday : drink beer - eat peach - enjoy pie

zip() 函数在最短序列“用完”时就会停止。上面例子中的列表(desserts)是最长的,所以我们无法填充列表,除非人工扩展其他列表。

3.4 节中的 dict() 函数会将两项序列,比如元组、列表、字符串,创建成一个字典,同时使用 zip() 函数可以遍历多个序列,在具有相同位移的项之间创建元组。下面创建英语单词和法语单词之间的对应关系的两个元组:

  1. >>> english = 'Monday', 'Tuesday', 'Wednesday'
  2. >>> french = 'Lundi', 'Mardi', 'Mercredi'

现在使用 zip() 函数配对两个元组。函数的返回值既不是元组也不是列表,而是一个整合在一起的可迭代变量:

  1. >>> list( zip(english, french) )
  2. [('Monday', 'Lundi'), ('Tuesday', 'Mardi'), ('Wednesday', 'Mercredi')]

配合 dict() 函数和 zip() 函数的返回值就可以得到一本微型的英法词典:

  1. >>> dict( zip(english, french) )
  2. {'Monday': 'Lundi', 'Tuesday': 'Mardi', 'Wednesday': 'Mercredi'}

4.5.5 使用range()生成自然数序列

range() 函数返回在特定区间的自然数序列,不需要创建和存储复杂的数据结构,例如列表或者元组。这允许在不使用计算机全部内存的情况下创建较大的区间,也不会使你的程序崩溃。

range() 函数的用法类似于使用切片:range(start,stop,step)。而 start 的默认值为 0。唯一要求的参数值是 stop,产生的最后一个数的值是 stop 的前一个,并且 step 的默认值是 1。当然,也可以反向创建自然数序列,这时 step 的值为 -1

zip()range() 这些函数返回的是一个可迭代的对象,所以可以使用 for … in 的结构遍历,或者把这个对象转化为一个序列(例如列表)。我们来产生序列 0, 1, 2

  1. >>> for x in range(0,3):
  2. ... print(x)
  3. ...
  4. 0
  5. 1
  6. 2
  7. >>> list( range(0, 3) )
  8. [0, 1, 2]

下面是如何从 20 反向创建序列:

  1. >>> for x in range(2, -1, -1):
  2. ... print(x)
  3. ...
  4. 2
  5. 1
  6. 0
  7. >>> list( range(2, -1, -1) )
  8. [2, 1, 0]

下面的代码片段将 step 赋值为 2,得到从 010 的偶数:

  1. >>> list( range(0, 11, 2) )
  2. [0, 2, 4, 6, 8, 10]

4.5.6 其他迭代方式

第 8 章将介绍文件之间的迭代。在第 6 章中,你会看到如何在自己创建的对象之间迭代。

4.6 推导式

推导式是从一个或者多个迭代器快速简洁地创建数据结构的一种方法。它可以将循环和条件判断结合,从而避免语法冗长的代码。会使用推导式有时可以说明你已经超过 Python 初学者的水平。也就是说,使用推导式更像 Python 风格。

4.6.1 列表推导式

你可以从 15 创建一个整数列表,每次增加一项,如下所示:

  1. >>> number_list = []
  2. >>> number_list.append(1)
  3. >>> number_list.append(2)
  4. >>> number_list.append(3)
  5. >>> number_list.append(4)
  6. >>> number_list.append(5)
  7. >>> number_list
  8. [1, 2, 3, 4, 5]

或者,可以结合 range() 函数使用一个迭代器:

  1. >>> number_list = []
  2. >>> for number in range(1, 6):
  3. ... number_list.append(number)
  4. ...
  5. >>> number_list
  6. [1, 2, 3, 4, 5]

或者,直接把 range() 的返回结果放到一个列表中:

  1. >>> number_list = list(range(1, 6))
  2. >>> number_list
  3. [1, 2, 3, 4, 5]

上面这些方法都是可行的 Python 代码,会得到相同的结果。然而,更像 Python 风格的创建列表方式是使用列表推导。最简单的形式如下所示:

  1. [ expression for item in iterable ]

下面的例子将通过列表推导创建一个整数列表:

  1. >>> number_list = [number for number in range(1,6)]
  2. >>> number_list
  3. [1, 2, 3, 4, 5]

在第一行中,第一个 number 变量为列表生成值,也就是说,把循环的结果放在列表 number_list 中。第二个 number 为循环变量。其中第一个 number 可以为表达式,试试下面改编的例子:

  1. >>> number_list = [number-1 for number in range(1,6)]
  2. >>> number_list
  3. [0, 1, 2, 3, 4]

列表推导把循环放在方括号内部。这种例子和之前碰到的不大一样,但却是更为常见的方式。同样,列表推导也可以像下面的例子加上条件表达式:

  1. [expression for item in iterable if condition]

现在,通过推导创建一个在 15 之间的奇数列表(当 number % 2 为真时,代表奇数;为假时,代表偶数):

  1. >>> a_list = [number for number in range(1,6) if number % 2 == 1]
  2. >>> a_list
  3. [1, 3, 5]

于是,上面的推导要比之前传统的方法更简洁:

  1. >>> a_list = []
  2. >>> for number in range(1,6):
  3. ... if number % 2 == 1:
  4. ... a_list.append(number)
  5. ...
  6. >>> a_list
  7. [1, 3, 5]

最后,正如存在很多嵌套循环一样,在对应的推导中会有多个 for … 语句。我们先来看一个简单的嵌套循环的例子,并在屏幕上打印出结果:

  1. >>> rows = range(1,4)
  2. >>> cols = range(1,3)
  3. >>> for row in rows:
  4. ... for col in cols:
  5. ... print(row, col)
  6. ...
  7. 1 1
  8. 1 2
  9. 2 1
  10. 2 2
  11. 3 1
  12. 3 2

下面使用一次推导,将结果赋值给变量 cells,使它成为元组 (row,col):

  1. >>> rows = range(1,4)
  2. >>> cols = range(1,3)
  3. >>> cells = [(row, col) for row in rows for col in cols]
  4. >>> for cell in cells:
  5. ... print(cell)
  6. ...
  7. (1, 1)
  8. (1, 2)
  9. (2, 1)
  10. (2, 2)
  11. (3, 1)
  12. (3, 2)

另外,在对 cells 列表进行迭代时可以通过元组拆封将变量 rowcol 的值分别取出:

  1. >>> for row, col in cells:
  2. ... print(row, col)
  3. ...
  4. 1 1
  5. 1 2
  6. 2 1
  7. 2 2
  8. 3 1
  9. 3 2

其中,在列表推导中 for row …for col … 都可以有自己单独的 if 条件判断。

4.6.2 字典推导式

除了列表,字典也有自己的推导式。最简单的例子就像:

  1. { key_expression : value_expression for expression in iterable }

类似于列表推导,字典推导也有 if 条件判断以及多个 for 循环迭代语句:

  1. >>> word = 'letters'
  2. >>> letter_counts = {letter: word.count(letter) for letter in word}
  3. >>> letter_counts
  4. {'l': 1, 'e': 2, 't': 2, 'r': 1, 's': 1}

程序中,对字符串 'letters' 中出现的字母进行循环,计算出每个字母出现的次数。对于程序执行来说,两次调用 word.count(letter) 浪费时间,因为字符串中 te 都出现了两次,第一次调用 word.count() 时已经计算得到相应的值。下面的例子会解决这个小问题,更符合 Python 风格:

  1. >>> word = 'letters'
  2. >>> letter_counts = {letter: word.count(letter) for letter in set(word)}
  3. >>> letter_counts
  4. {'t': 2, 'l': 1, 'e': 2, 'r': 1, 's': 1}

字典键的顺序和之前的例子是不同的,因为是对 set(word) 集合进行迭代的,而前面的例子是对 word 字符串迭代。

4.6.3 集合推导式

集合也不例外,同样有推导式。最简单的版本和之前的列表、字典推导类似:

  1. {expression for expression in iterable }

最长的版本(if tests, multiple for clauses)对于集合而言也是可行的:

  1. >>> a_set = {number for number in range(1,6) if number % 3 == 1}
  2. >>> a_set
  3. {1, 4}

4.6.4 生成器推导式

元组是没有推导式的。你可能认为将列表推导式中的方括号变成圆括号就可以定义元组推导式,就像下面的表达式一样:

  1. >>> number_thing = (number for number in range(1, 6))

其实,圆括号之间的是生成器推导式,它返回的是一个生成器对象

  1. >>> type(number_thing)
  2. <class 'generotor'>

4.8 节会详细介绍。它是将数据传给迭代器的一种方式。

你可以直接对生成器对象进行迭代,如下所示:

  1. >>> for number in number_thing:
  2. ... print(number)
  3. ...
  4. 1
  5. 2
  6. 3
  7. 4
  8. 5

或者,通过对一个生成器的推导式调用 list() 函数,使它类似于列表推导式:

  1. >>> number_list = list(number_thing)
  2. >>> number_list
  3. [1, 2, 3, 4, 5]

第 4 章 Python 外壳:代码结构 - 图4 一个生成器只能运行一次。列表、集合、字符串和字典都存储在内存中,但是生成器仅在运行中产生值,不会被存下来,所以不能重新使用或者备份一个生成器。

如果想再一次迭代此生成器,会发现它被擦除了:

  1. >>> try_again = list(number_thing)
  2. >>> try_again
  3. []

你既可以通过生成器推导式创建生成器,也可以使用生成器的函数。后面会介绍这些函数,并且探讨这些生成器函数的特殊用法。

4.7 函数

到目前为止,我们的 Python 代码已经实现了小的分块。它们都适合处理微小任务,但是我们想复用这些代码,所以需要把大型代码组织成可管理的代码段。

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

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

  • 定义函数

  • 调用函数

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

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

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

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

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

  1. >>> do_nothing()
  2. >>>

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

  1. >>> def make_a_sound():
  2. ... print('quack')
  3. ...
  4. >>> make_a_sound()
  5. quack

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

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

  1. >>> def agree():
  2. ... return True
  3. ...

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

  1. >>> if agree():
  2. ... print('Splendid!')
  3. ... else:
  4. ... print('That was unexpected.')
  5. ...
  6. Splendid!

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

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

  1. >>> def echo(anything):
  2. ... return anything + ' ' + anything
  3. ...
  4. >>>

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

  1. >>> echo('Rumplestiltskin')
  2. 'Rumplestiltskin Rumplestiltskin'

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

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

  1. >>> def commentary(color):
  2. ... if color == 'red':
  3. ... return "It's a tomato."
  4. ... elif color == "green":
  5. ... return "It's a green pepper."
  6. ... elif color == 'bee purple':
  7. ... return "I don't know what it is, but only bees can see it."
  8. ... else:
  9. ... return "I've never heard of the color " + color + "."
  10. ...
  11. >>>

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

  1. >>> comment = commentary('blue')

这个函数做了以下事情:

  • 'blue' 赋值给函数的内部参数 color

  • 运行 if-elif-else 的逻辑链

  • 返回一个字符串

  • 将该字符串赋值给变量 comment

我们如何得到返回值呢?

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

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

  1. >>> print(do_nothing())
  2. None

有用的 None

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

  1. >>> thing = None
  2. >>> if thing:
  3. print("It's some thing")
  4. else:
  5. print("It's no thing")
  6. It's no thing

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

  1. >>> if thing is None:
  2. print("It's nothing")
  3. else:
  4. print("It's something")
  5. It's nothing

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

现在,快速写一个函数,输出它的参数是否是 None

  1. >>> def is_none(thing):
  2. if thing is None:
  3. print("It's None")
  4. elif thing:
  5. print("It's True")
  6. else:
  7. print("It's False")

现在,运行一些测试函数:

  1. >>> is_none(None)
  2. It's None
  3. >>> is_none(True)
  4. It's True
  5. >>> is_none(False)
  6. It's False
  7. >>> is_none(0)
  8. It's False
  9. >>> is_none(0.0)
  10. It's False
  11. >>> is_none(())
  12. It's False
  13. >>> is_none([])
  14. It's False
  15. >>> is_none({})
  16. It's False
  17. >>> is_none(set())
  18. It's False

4.7.1 位置参数

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

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

  1. >>> def menu(wine, entree, dessert):
  2. ... return {'wine': wine, 'entree': entree, 'dessert': dessert}
  3. ...
  4. >>> menu('chardonnay', 'chicken', 'cake')
  5. {'dessert': 'cake', 'wine': 'chardonnay', 'entree': 'chicken'}

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

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

4.7.2 关键字参数

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

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

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

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

如果同时使用位置参数和关键字参数两种方式调用函数,位置参数必须放置于关键字参数之前。

4.7.3 指定默认参数值

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

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

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

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

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

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

第 4 章 Python 外壳:代码结构 - 图5 默认参数值在函数被定义时已经计算出来,而不是在程序运行时。Python 程序员经常犯的一个错误是把可变的数据类型(例如列表或者字典)当作默认参数值。

在下面的例子中,函数 buggy() 在每次调用时,添加参数 arg 到一个空的列表 result,然后打印输出一个单值列表。但是存在一个问题:只有在第一次调用时列表是空的,第二次调用时就会存在之前调用的返回值:

  1. >>> def buggy(arg, result=[]):
  2. ... result.append(arg)
  3. ... print(result)
  4. ...
  5. >>> buggy('a')
  6. ['a']
  7. >>> buggy('b') # expect ['b']
  8. ['a', 'b']

如果写成下面的样子就会解决刚才的问题:

  1. >>> def works(arg):
  2. ... result = []
  3. ... result.append(arg)
  4. ... return result
  5. ...
  6. >>> works('a')
  7. ['a']
  8. >>> works('b')
  9. ['b']

这样的修改也为了表明是第一次调用跳过一些操作:

  1. >>> def nonbuggy(arg, result=None):
  2. ... if result is None:
  3. ... result = []
  4. ... result.append(arg)
  5. ... print(result)
  6. ...
  7. >>> nonbuggy('a')
  8. ['a']
  9. >>> nonbuggy('b')
  10. ['b']

4.7.4 使用*收集位置参数

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

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

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

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

  1. >>> print_args()
  2. Positional argument tuple: ()

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

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

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

  1. >>> def print_more(required1, required2, *args):
  2. ... print('Need this one:', required1)
  3. ... print('Need this one too:', required2)
  4. ... print('All the rest:', args)
  5. ...
  6. >>> print_more('cap', 'gloves', 'scarf', 'monocle', 'mustache wax')
  7. Need this one: cap
  8. Need this one too: gloves
  9. All the rest: ('scarf', 'monocle', 'mustache wax')

当使用 * 时,不一定要把元组参数命名为 args,但这是 Python 中的一个常见做法。

4.7.5 使用**收集关键字参数

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

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

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

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

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

如果你把带有 *args**kwargs 的位置参数混合起来,它们必须按照顺序解析。和 args 一样,不一定要把这个关键字参数命名为 kwargs,但这是通常做法。

4.7.6 文档字符串

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

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

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

  1. def print_if_true(thing, check):
  2. '''
  3. Prints the first argument if a second argument is true.
  4. The operation is:
  5. 1. Check whether the second argument is true.
  6. 2. If it is, print the first argument.
  7. '''
  8. if check:
  9. print(thing)

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

  1. >>> help(echo)
  2. Help on function echo in module __main__:
  3. echo(anything)
  4. echo returns its input argument

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

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

看上去很奇怪的 __doc__ 是作为函数中变量的文档字符串的名字。4.10 节的“名称中 ___ 的用法”会解释所有下划线背后的原理。

4.7.7 一等公民:函数

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

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

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

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

  1. >>> answer()
  2. 42

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

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

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

  1. >>> run_something(answer)
  2. 42

注意,你传给函数的是 answer , 而不是 answer()。在 Python 中圆括号意味着调用函数。在没有圆括号的情况下,Python 会把函数当作普通对象。这是因为在其他情况下,它也仅仅代表一个对象:

  1. >>> type(run_something)
  2. <class 'function'>

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

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

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

  1. >>> type(add_args)
  2. <class 'function'>

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

  • func——可以运行的函数

  • arg1——func 函数的第一个参数

  • arg2——func 函数的第二个参数

  1. >>> def run_something_with_args(func, arg1, arg2):
  2. ... func(arg1, arg2)

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

将函数名 add_args 和参数 59 传给函数 run_something_with_args()

  1. >>> run_something_with_args(add_args, 5, 9)
  2. 14

在函数 run_something_with_args() 内部,函数名 add_args 被赋值给参数 func59 分别赋值给 arg1arg2。程序最后执行:

  1. add_args(5, 9)

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

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

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

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

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

  1. >>> def run_with_positional_args(func, args):
  2. ... return func(args)

现在直接调用它:

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

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

4.7.8 内部函数

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

  1. >>> def outer(a, b):
  2. ... def inner(c, d):
  3. ... return c + d
  4. ... return inner(a, b)
  5. ...
  6. >>>
  7. >>> outer(4, 7)
  8. 11

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

  1. >>> def knights(saying):
  2. ... def inner(quote):
  3. ... return "We are the knights who say: '%s'" % quote
  4. ... return inner(saying)
  5. ...
  6. >>> knights('Ni!')
  7. "We are the knights who say: 'Ni!'"

4.7.9 闭包

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

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

  • inner2() 直接使用外部的 saying 参数,而不是通过另外一个参数获取。

  • knights2() 返回值为 inner2 函数,而不是调用它。

  1. >>> def knights2(saying):
  2. ... def inner2():
  3. ... return "We are the knights who say: '%s'" % saying
  4. ... return inner2
  5. ...

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

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

  1. >>> a = knights2('Duck')
  2. >>> b = knights2('Hasenpfeffer')

那么 ab 会是什么类型?

  1. >>> type(a)
  2. <class 'function'>
  3. >>> type(b)
  4. <class 'function'>

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

  1. >>> a
  2. <function knights2.<locals>.inner2 at 0x10193e158>
  3. >>> b
  4. <function knights2.<locals>.inner2 at 0x10193e1e0>

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

  1. >>> a()
  2. "We are the knights who say: 'Duck'"
  3. >>> b()
  4. "We are the knights who say: 'Hasenpfeffer'"

4.7.10 匿名函数:lambda()函数

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

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

  • words——单词列表

  • func——遍历列表中单词的函数

  1. >>> def edit_story(words, func):
  2. ... for word in words:
  3. ... print(func(word))

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

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

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

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

混合这些“配料”:

  1. >>> edit_story(stairs, enliven)
  2. Thud!
  3. Meow!
  4. Thud!
  5. Hiss!

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

  1. >>>
  2. >>> edit_story(stairs, lambda word: word.capitalize() + '!')
  3. Thud!
  4. Meow!
  5. Thud!
  6. Hiss!
  7. >>>

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

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

4.8 生成器

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

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

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

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

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

这是一个普通的函数:

  1. >>> my_range
  2. <function my_range at 0x10193e268>

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

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

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

  1. >>> for x in ranger:
  2. ... print(x)
  3. ...
  4. 1
  5. 2
  6. 3
  7. 4

4.9 装饰器

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

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

  • *args**kwargs

  • 闭包

  • 作为参数的函数

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

  • 打印输出函数的名字和参数的值

  • 执行含有参数的函数

  • 打印输出结果

  • 返回修改后的函数

看下面的代码:

  1. >>> def document_it(func):
  2. ... def new_function(*args, **kwargs):
  3. ... print('Running function:', func.__name__)
  4. ... print('Positional arguments:', args)
  5. ... print('Keyword arguments:', kwargs)
  6. ... result = func(*args, **kwargs)
  7. ... print('Result:', result)
  8. ... return result
  9. ... return new_function

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

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

  1. >>> def add_ints(a, b):
  2. ... return a + b
  3. ...
  4. >>> add_ints(3, 5)
  5. 8
  6. >>> cooler_add_ints = document_it(add_ints) # 人工对装饰器赋值
  7. >>> cooler_add_ints(3, 5)
  8. Running function: add_ints
  9. Postitional arguments: 3 5
  10. Keyword arguments: {}
  11. Result: 8
  12. 8

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

  1. >>> @document_it
  2. ... def add_ints(a, b):
  3. ... return a + b
  4. ...
  5. >>> add_ints(3, 5)
  6. Running function: add_ints
  7. Positional arguments: (3, 5)
  8. Keyword arguments: {}
  9. Result: 8
  10. 8

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

  1. >>> def square_it(func):
  2. ... def new_function(*args, **kwargs):
  3. ... result = func(*args, **kwargs)
  4. ... return result * result
  5. ... return new_function
  6. ...

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

  1. >>> @document_it
  2. ... @square_it
  3. ... def add_ints(a, b):
  4. ... return a + b
  5. ...
  6. >>> add_ints(3, 5)
  7. Running function: new_function
  8. Positional arguments: (3, 5)
  9. Keyword arguments: {}
  10. Result: 64
  11. 64

交换两个装饰器的顺序:

  1. >>> @square_it
  2. ... @document_it
  3. ... def add_ints(a, b):
  4. ... return a + b
  5. ...
  6. >>> add_ints(3, 5)
  7. Running function: add_ints
  8. Positional arguments: (3, 5)
  9. Keyword arguments: {}
  10. Result: 8
  11. 64

4.10 命名空间和作用域

一个名称在不同的使用情况下可能指代不同的事物。Python 程序有各种各样的命名空间,它指的是在该程序段内一个特定的名称是独一无二的,它和其他同名的命名空间是无关的。

每一个函数定义自己的命名空间。如果在主程序(main)中定义一个变量 x,在另外一个函数中也定义 x 变量,两者指代的是不同的变量。但是,天下也没有完全绝对的事情,需要的话,可以通过多种方式获取其他命名空间的名称。

每个程序的主要部分定义了全局命名空间。因此,在这个命名空间的变量是全局变量

你可以在一个函数内得到某个全局变量的值:

  1. >>> animal = 'fruitbat'
  2. >>> def print_global():
  3. ... print('inside print_global:', animal)
  4. ...
  5. >>> print('at the top level:', animal)
  6. at the top level: fruitbat
  7. >>> print_global()
  8. inside print_global: fruitbat

但是,如果想在函数中得到一个全局变量的值并且改变它,会报错:

  1. >>> def change_and_print_global():
  2. ... print('inside change_and_print_global:', animal)
  3. ... animal = 'wombat'
  4. ... print('after the change:', animal)
  5. ...
  6. >>> change_and_print_global()
  7. Traceback (most recent call last):
  8. File "<stdin>", line 1, in <module>
  9. File "<stdin>", line 2, in change_and_report_it
  10. UnboundLocalError: local variable 'animal' referenced before assignment

实际上,你改变的另外一个同样被命名为 animal 的变量,只不过这个变量在函数内部:

  1. >>> def change_local():
  2. ... animal = 'wombat'
  3. ... print('inside change_local:', animal, id(animal))
  4. ...
  5. >>> change_local()
  6. inside change_local: wombat 4330406160
  7. >>> animal
  8. 'fruitbat'
  9. >>> id(animal)
  10. 4330390832

这里发生了什么?在函数第一行将字符串 fruitbat 赋值给全局变量 animal。函数 change_local() 也有一个叫作 animal 的变量。不同的是,它在自己的局部命名空间。

我们使用 Python 内嵌函数 id() 打印输出每个对象的唯一的 ID 值,证明在函数 change_local() 中的变量 animal 和主程序中的 animal 不是同一个。

为了读取全局变量而不是函数中的局部变量,需要在变量前面显式地加关键字 global(也正是《Python 之禅》中的一句话:明了胜于隐晦):

  1. >>> animal = 'fruitbat'
  2. >>> def change_and_print_global():
  3. ... global animal
  4. ... animal = 'wombat'
  5. ... print('inside change_and_print_global:', animal)
  6. ...
  7. >>> animal
  8. 'fruitbat'
  9. >>> change_and_print_global()
  10. inside change_and_print_global: wombat
  11. >>> animal
  12. 'wombat'

如果在函数中不声明关键字 global,Python 会使用局部命名空间,同时变量也是局部的。函数执行后回到原来的命名空间。

Python 提供了两个获取命名空间内容的函数:

  • locals() 返回一个局部命名空间内容的字典;

  • globals() 返回一个全局命名空间内容的字典。

下面是它们的实例:

  1. >>> animal = 'fruitbat'
  2. >>> def change_local():
  3. ... animal = 'wombat' #局部变量
  4. ... print('locals:',locals())
  5. ...
  6. >>> animal
  7. 'fruitbat'
  8. >>> change_local()
  9. locals: {'animal':'wombat'}
  10. >>> print('globals:', globals()) #表示时格式稍微发生变化
  11. globals:{'animal': 'fruitbat',
  12. '__doc__': None,
  13. 'change_local': <function change_it at 0x1006c0170>,
  14. '__package__': None,
  15. '__name__': '__main__',
  16. '__loader__': <class 'frozenimportlib.BuiltinImporter'>,
  17. '__builtins__': <module 'builtins'>}
  18. >>> animal
  19. 'fruitbat'

函数 change_local() 的局部命名空间只含有局部变量 animal。全局命名空间含有全局变量 animal 以及其他一些东西。

名称中___ 的用法

以两个下划线 __ 开头和结束的名称都是 Python 的保留用法。因此,在自定义的变量中不能使用它们。选择这种命名模式是考虑到开发者一般是不会选择它们作为自己的变量的。

例如,一个函数的名称是系统变量 function.__name__,它的文档字符串是 function.__doc__

  1. >>> def amazing():
  2. ... '''This is the amazing function.
  3. ... Want to see it again?'''
  4. ... print('This function is named:', amazing.__name__)
  5. ... print('And its docstring is:', amazing.__doc__)
  6. ...
  7. >>> amazing()
  8. This function is named: amazing
  9. And its docstring is: This is the amazing function.
  10. Want to see it again?

如同之前 globals 的输出结果所示,主程序被赋值特殊的名字 __main__

4.11 使用tryexcept处理错误

要么做,要么不做,没有尝试这回事。3

——尤达(Yoda)

3“Do, or do not. There is no try.”科幻电影《星球大战》中绝地大师尤达 Yoda 的台词。——译者注

在一些编程语言中,错误是通过特殊的函数返回值指出的,而 Python 使用异常,它是一段只有错误发生时执行的代码。

之前已经接触到一些有关错误的例子,例如读取列表或者元组的越界位置或者字典中不存在的键。所以,当你执行可能出错的代码时,需要适当的异常处理程序用于阻止潜在的错误发生。

在异常可能发生的地方添加异常处理程序,对于用户明确错误是一种好方法。即使不会及时解决问题,至少会记录运行环境并且停止程序执行。如果发生在某些函数中的异常不能被立刻捕捉,它会持续,直到被某个调用函数的异常处理程序所捕捉。在你不能提供自己的异常捕获代码时,Python 会输出错误消息和关于错误发生处的信息,然后终止程序,例如下面的代码段:

  1. >>> short_list = [1, 2, 3]
  2. >>> position = 5
  3. >>> short_list[position]
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. IndexError: list index out of range

与其让错误随机产生,不如使用 tryexcept 提供错误处理程序:

  1. >>> short_list = [1, 2, 3]
  2. >>> position = 5
  3. >>> try:
  4. ... short_list[position]
  5. ... except:
  6. ... print('Need a position between 0 and', len(short_list)-1, ' but got',
  7. ... position)
  8. ...
  9. Need a position between 0 and 2 but got 5

try 中的代码块会被执行。如果存在错误,就会抛出异常,然后执行 except 中的代码;否则,跳过 except 块代码。

像前面那样指定一个无参数的 except 适用于任何异常类型。如果可能发生多种类型的异常,最好是分开进行异常处理。当然,没人强迫你这么做,你可以使一个 except 去捕捉所有的异常,但是这样的处理方式会比较泛化(类似于直接输出发生了一个错误)。当然也可以使用任意数量的异常处理程序。

有时需要除了异常类型以外其他的异常细节,可以使用下面的格式获取整个异常对象:

  1. except exceptiontype as name

下面的例子首先会寻找是否有 IndexError,因为它是由索引一个序列的非法位置抛出的异常类型。将一个 IndexError 异常赋给变量 err,把其他的异常赋给变量 other。示例中会输出所有存储在 other 中的该对象的异常。

  1. >>> short_list = [1, 2, 3]
  2. >>> while True:
  3. ... value = input('Position [q to quit]? ')
  4. ... if value == 'q':
  5. ... break
  6. ... try:
  7. ... position = int(value)
  8. ... print(short_list[position])
  9. ... except IndexError as err:
  10. ... print('Bad index:', position)
  11. ... except Exception as other:
  12. ... print('Something else broke:', other)
  13. ...
  14. Position [q to quit]? 1
  15. 2
  16. Position [q to quit]? 0
  17. 1
  18. Position [q to quit]? 2
  19. 3
  20. Position [q to quit]? 3
  21. Bad index: 3
  22. Position [q to quit]? 2
  23. 3
  24. Position [q to quit]? two
  25. Something else broke: invalid literal for int() with base 10: 'two'
  26. Position [q to quit]? q

输入 3 会抛出异常 IndexError;输入 two 会使函数 int() 抛出异常,被第二个 except 所捕获。

4.12 编写自己的异常

前面一节讨论了异常处理,但是其中讲到的所有异常(例如 IndexError)都是在 Python 或者它的标准库中提前定义好的。根据自己的目的可以使用任意的异常类型,同时也可以自己定义异常类型,用来处理程序中可能会出现的特殊情况。

第 4 章 Python 外壳:代码结构 - 图6 这里需要定义一个的新对象,这会在第 6 章深入说明。如果你对类不是很熟悉,可以学完后面的部分再返回这一节。

一个异常是一个类,即类 Exception 的一个子类。现在编写异常 UppercaseException,在一个字符串中碰到大写字母会被抛出。

  1. >>> class UppercaseException(Exception):
  2. ... pass
  3. ...
  4. >>> words = ['eeenie', 'meenie', 'miny', 'MO']
  5. >>> for word in words:
  6. ... if word.isupper():
  7. ... raise UppercaseException(word)
  8. ...
  9. Traceback (most recent call last):
  10. File "<stdin>", line 3, in <module>
  11. __main__.UppercaseException: MO

即使没有定义 UppercaseException 的行为(注意到只使用 pass),也可以通过继承其父类 Exception 在抛出异常时输出错误提示。

你当然能够访问异常对象本身,并且输出它:

  1. >>> try:
  2. ... raise OopsException('panic')
  3. ... except OopsException as exc:
  4. ... print(exc)
  5. ...
  6. panic

4.13 练习

(1) 将 7 赋值给变量 guess_me,然后写一段条件判断(ifelseelif)的代码:如果 guess_me 小于 7 输出 'too low';大于 7 则输出 'too high';等于 7 则输出 'just right'

(2) 将 7 赋值给变量 guess_me,再将 1 赋值给变量 start。写一段 while 循环代码,比较 startguess_me:如果 start 小于 guess_me,输出 too low;如果等于则输出 'found it!' 并终止循环;如果大于则输出 'oops',然后终止循环。在每次循环结束时自增 start

(3) 使用 for 循环输出列表 [3, 2, 1, 0] 的值。

(4) 使用列表推导生成 0~9(range(10))的偶数列表。

(5) 使用字典推导创建字典 squares,把 0~9(range(10))的整数作为键,每个键的平方作为对应的值。

(6) 使用集合推导创建集合 odd,包含 0~9(range(10))的奇数。

(7) 使用生成器推导返回字符串 'Got ' 和 0~9 内的一个整数,使用 for 循环进行迭代。

(8) 定义函数 good:返回列表 ['Harry','Ron','Hermione']

(9) 定义一个生成器函数 get_odds:返回 0~9 内的奇数。使用 for 循环查找并输出返回的第三个值。

(10) 定义一个装饰器 test:当一个函数被调用时输出 'start',当函数结束时输出 'end'

(11) 定义一个异常 OopsException:编写代码捕捉该异常,并输出 'Caught an oops'

(12) 使用函数 zip() 创建字典 movies:匹配两个列表 titles = ['Creature of Habit', 'Crewel Fate']plots = ['A nun turns into a monster', 'A haunted yarn shop']