Python-入门指南第二版-三-

57 阅读53分钟

Python 入门指南第二版(三)

原文:annas-archive.org/md5/4b0fd2cf0da7c8edae4b5ecfd40159bf

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:字典和集合

如果字典中的单词拼错了,我们怎么知道呢?

史蒂文·赖特

字典

字典 类似于列表,但条目的顺序无关紧要,也不是通过像 0 或 1 这样的偏移来选择。而是为每个 指定一个唯一的 key 来关联。这个键通常是一个字符串,但实际上可以是 Python 的任何不可变类型:布尔值、整数、浮点数、元组、字符串和其他在后面章节中会看到的类型。字典是可变的,因此可以添加、删除和更改它们的键值元素。如果您曾经使用过只支持数组或列表的语言,您会喜欢字典的。

注意

在其他语言中,字典可能被称为 关联数组哈希哈希映射。在 Python 中,字典也被称为 dict,以节省音节并让十几岁的男孩窃笑。

使用 {} 创建

要创建字典,您需要在逗号分隔的 key : value 对之间放置花括号 ({})。最简单的字典是一个完全不包含任何键或值的空字典:

>>> empty_dict = {}
>>> empty_dict
{}

让我们从安布罗斯·彼尔斯的 魔鬼的词典 中摘取一些引文,制作一个小字典:

>>> bierce = {
...     "day": "A period of twenty-four hours, mostly misspent",
...     "positive": "Mistaken at the top of one's voice",
...     "misfortune": "The kind of fortune that never misses",
...     }
>>>

在交互解释器中输入字典的名称将打印其键和值:

>>> bierce
{'day': 'A period of twenty-four hours, mostly misspent',
'positive': "Mistaken at the top of one's voice",
'misfortune': 'The kind of fortune that never misses'}
注意

在 Python 中,如果列表、元组或字典的最后一项后面留有逗号是可以的。当您在花括号中键入键和值时,就像在前面的例子中所做的那样,不需要缩进。这只是为了提高可读性。

使用 dict() 创建

有些人不喜欢输入那么多花括号和引号。您也可以通过将命名参数和值传递给 dict() 函数来创建字典。

传统的方式:

>>> acme_customer = {'first': 'Wile', 'middle': 'E', 'last': 'Coyote'}
>>> acme_customer
{'first': 'Wile', 'middle': 'E', 'last': 'Coyote'}

使用 dict()

>>> acme_customer = dict(first="Wile", middle="E", last="Coyote")
>>> acme_customer
{'first': 'Wile', 'middle': 'E', 'last': 'Coyote'}

第二种方式的一个限制是参数名必须是合法的变量名(不能有空格,不能是保留字):

>>> x = dict(name="Elmer", def="hunter")
  File "<stdin>", line 1
    x = dict(name="Elmer", def="hunter")
                             ^
SyntaxError: invalid syntax

使用 dict() 转换

您也可以使用 dict() 函数将包含两个值的序列转换为字典。有时您可能会遇到这样的键值序列,例如 “锶,90,碳,14。”¹ 每个序列的第一项用作键,第二项用作值。

首先,这里是使用 lol(一个包含两项列表的列表)的一个小例子:

>>> lol = [ ['a', 'b'], ['c', 'd'], ['e', 'f'] ]
>>> dict(lol)
{'a': 'b', 'c': 'd', 'e': 'f'}

我们可以使用包含两项序列的任何序列。以下是其他示例。

一个包含两项元组的列表:

>>> lot = [ ('a', 'b'), ('c', 'd'), ('e', 'f') ]
>>> dict(lot)
{'a': 'b', 'c': 'd', 'e': 'f'}

一个包含两项列表的元组:

>>> tol = ( ['a', 'b'], ['c', 'd'], ['e', 'f'] )
>>> dict(tol)
{'a': 'b', 'c': 'd', 'e': 'f'}

一个包含两个字符字符串的列表:

>>> los = [ 'ab', 'cd', 'ef' ]
>>> dict(los)
{'a': 'b', 'c': 'd', 'e': 'f'}

一个包含两个字符字符串的元组:

>>> tos = ( 'ab', 'cd', 'ef' )
>>> dict(tos)
{'a': 'b', 'c': 'd', 'e': 'f'}

章节 “使用 zip() 迭代多个序列” 介绍了 zip() 函数,它使得创建这些两项序列变得容易。

添加或更改一个项目由 [ key ]

向字典中添加项很简单。只需通过其键引用该项并赋予一个值。如果键已存在于字典中,现有值将被新值替换。如果键是新的,则将其添加到字典中并赋予其值。与列表不同,你不必担心 Python 在分配时抛出超出范围的索引异常。

让我们创建一个蒙提·派森大部分成员的字典,使用他们的姓作为键,名作为值:

>>> pythons = {
...     'Chapman': 'Graham',
...     'Cleese': 'John',
...     'Idle': 'Eric',
...     'Jones': 'Terry',
...     'Palin': 'Michael',
...     }
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Idle': 'Eric',
'Jones': 'Terry', 'Palin': 'Michael'}

我们遗漏了一个成员:出生在美国的那个,特里·吉列姆。这里有一个匿名程序员试图添加他的尝试,但是他搞砸了名字的第一个字:

>>> pythons['Gilliam'] = 'Gerry'
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Idle': 'Eric',
'Jones': 'Terry', 'Palin': 'Michael', 'Gilliam': 'Gerry'}

这里有另一位程序员编写的修复代码,他在多个方面都符合 Python 风格:

>>> pythons['Gilliam'] = 'Terry'
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Idle': 'Eric',
'Jones': 'Terry', 'Palin': 'Michael', 'Gilliam': 'Terry'}

通过使用相同的键('Gilliam'),我们用'Terry'替换了原始值'Gerry'

记住字典键必须是唯一的。这就是为什么我们在这里使用姓氏而不是名字作为键——蒙提·派森的两个成员都有名字 'Terry'!如果你使用一个键多次,最后一个值会覆盖之前的值:

>>> some_pythons = {
...     'Graham': 'Chapman',
...     'John': 'Cleese',
...     'Eric': 'Idle',
...     'Terry': 'Gilliam',
...     'Michael': 'Palin',
...     'Terry': 'Jones',
...     }
>>> some_pythons
{'Graham': 'Chapman', 'John': 'Cleese', 'Eric': 'Idle',
'Terry': 'Jones', 'Michael': 'Palin'}

我们首先将值'Gilliam'分配给键'Terry',然后将其替换为值'Jones'

通过[key]或者使用 get()获取项

这是字典的最常见用法。你指定字典和键以获取对应的值:使用前面部分的some_pythons

>>> some_pythons['John']
'Cleese'

如果字典中不存在该键,则会引发异常:

>>> some_pythons['Groucho']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Groucho'

有两种很好的方法可以避免这种情况。第一种是在开始时使用in来测试键,就像你在上一节看到的那样:

>>> 'Groucho' in some_pythons
False

第二种方法是使用特殊的字典get()函数。你提供字典、键和一个可选值。如果键存在,你将得到其值:

>>> some_pythons.get('John')
'Cleese'

否则,你将得到可选的值(如果你指定了的话):

>>> some_pythons.get('Groucho', 'Not a Python')
'Not a Python'

否则,你将得到None(在交互式解释器中显示为空):

>>> some_pythons.get('Groucho')
>>>

使用 keys()获取所有的键

你可以使用keys()来获取字典中的所有键。我们将在接下来的几个示例中使用一个不同的样本字典:

>>> signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'}
>>> signals.keys()
dict_keys(['green', 'yellow', 'red'])
注意

在 Python 2 中,keys()仅返回一个列表。而 Python 3 返回dict_keys(),它是键的可迭代视图。这在处理大型字典时很方便,因为它不会使用时间和内存来创建和存储一个可能不会使用的列表。但通常你确实需要一个列表。在 Python 3 中,你需要调用list()dict_keys对象转换为列表。

>>> list( signals.keys() )
['green', 'yellow', 'red']

在 Python 3 中,你还需要使用list()函数将values()items()的结果转换为普通的 Python 列表。我在这些例子中使用了这个。

使用 values()获取所有的值

要获取字典中的所有值,使用 values():

>>> list( signals.values() )
['go', 'go faster', 'smile for the camera']

使用 items()获取所有的键-值对

当你想从字典中获取所有的键-值对时,使用items()函数:

>>> list( signals.items() )
[('green', 'go'), ('yellow', 'go faster'), ('red', 'smile for the camera')]

每个键和值都作为元组返回,例如('green', 'go')

使用 len()获取长度

计算你的键值对数:

>>> len(signals)
3

使用{**a, **b}合并字典

从 Python 3.5 开始,有一种新的方法可以合并字典,使用**魔法,这在第九章中有着非常不同的用途:

>>> first = {'a': 'agony', 'b': 'bliss'}
>>> second = {'b': 'bagels', 'c': 'candy'}
>>> {**first, **second}
{'a': 'agony', 'b': 'bagels', 'c': 'candy'}

实际上,您可以传递多于两个字典:

>>> third = {'d': 'donuts'}
>>> {**first, **third, **second}
{'a': 'agony', 'b': 'bagels', 'd': 'donuts', 'c': 'candy'}

这些是复制。 如果您希望得到键和值的完整副本,并且与它们的原始字典没有关联,请参阅deepcopy()的讨论(“使用 deepcopy()复制一切”)。

使用 update()合并字典

您可以使用update()函数将一个字典的键和值复制到另一个字典中。

让我们定义包含所有成员的pythons字典:

>>> pythons = {
...     'Chapman': 'Graham',
...     'Cleese': 'John',
...     'Gilliam': 'Terry',
...     'Idle': 'Eric',
...     'Jones': 'Terry',
...     'Palin': 'Michael',
...     }
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael'}

我们还有一个名为others的其他幽默人物字典:

>>> others = { 'Marx': 'Groucho', 'Howard': 'Moe' }

现在,又来了另一个匿名程序员,他决定将others的成员作为蒙提·派森的成员:

>>> pythons.update(others)
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael',
'Marx': 'Groucho', 'Howard': 'Moe'}

如果第二个字典与它要合并的字典具有相同的键,会发生什么? 第二个字典的值将覆盖第一个字典的值:

>>> first = {'a': 1, 'b': 2}
>>> second = {'b': 'platypus'}
>>> first.update(second)
>>> first
{'a': 1, 'b': 'platypus'}

使用 del 按键删除项目

先前来自我们的匿名程序员的pythons.update(others)代码在技术上是正确的,但事实上是错误的。 虽然others的成员风趣而著名,但并非蒙提·派森的成员。 让我们撤销最后两次添加:

>>> del pythons['Marx']
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael',
'Howard': 'Moe'}
>>> del pythons['Howard']
>>> pythons
{'Chapman': 'Graham', 'Cleese': 'John', 'Gilliam': 'Terry',
'Idle': 'Eric', 'Jones': 'Terry', 'Palin': 'Michael'}

使用 pop()按键获取项目并删除它

这结合了get()del。 如果给pop()传递一个键,并且它存在于字典中,则返回匹配的值并删除键值对。 如果不存在,则会引发异常:

>>> len(pythons)
6
>>> pythons.pop('Palin')
'Michael'
>>> len(pythons)
5
>>> pythons.pop('Palin')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Palin'

但是,如果给pop()传递第二个默认参数(与get()一样),一切都会很好,字典不会更改:

>>> pythons.pop('First', 'Hugo')
'Hugo'
>>> len(pythons)
5

使用 clear()删除所有项目

要从字典中删除所有键和值,请使用clear()或只需重新分配一个空字典({})给名称:

>>> pythons.clear()
>>> pythons
{}
>>> pythons = {}
>>> pythons
{}

使用 in 测试键是否存在

如果您想知道字典中是否存在某个键,请使用in。 让我们再次重新定义pythons字典,这次省略一个或两个名称:

>>> pythons = {'Chapman': 'Graham', 'Cleese': 'John',
... 'Jones': 'Terry', 'Palin': 'Michael', 'Idle': 'Eric'}

现在让我们看看里面有谁:

>>> 'Chapman' in pythons
True
>>> 'Palin' in pythons
True

这次我们记得添加特里·吉列姆了吗?

>>> 'Gilliam' in pythons
False

唉。

使用=分配

与列表一样,如果您对字典进行更改,则将反映在引用它的所有名称中:

>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': 'smile for the camera'}
>>> save_signals = signals
>>> signals['blue'] = 'confuse everyone'
>>> save_signals
{'green': 'go',
'yellow': 'go faster',
'red': 'smile for the camera',
'blue': 'confuse everyone'}

使用 copy()复制

要实际从一个字典复制键和值到另一个字典,并避免这种情况,您可以使用copy()

>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': 'smile for the camera'}
>>> original_signals = signals.copy()
>>> signals['blue'] = 'confuse everyone'
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': 'smile for the camera',
'blue': 'confuse everyone'}
>>> original_signals
{'green': 'go',
'yellow': 'go faster',
'red': 'smile for the camera'}
>>>

这是一个复制,并且在字典值是不可变的情况下有效。 如果不是,您需要deepcopy()

使用 deepcopy()复制一切

假设前面示例中red的值是一个列表而不是一个单独的字符串:

>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': ['stop', 'smile']}
>>> signals_copy = signals.copy()
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'smile']}
>>> signals_copy
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'smile']}
>>>

让我们更改red列表中的一个值:

>>> signals['red'][1] = 'sweat'
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'sweat']}
>>> signals_copy
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'sweat']}

您将获得按名称更改的通常行为。 copy()方法直接复制了值,这意味着signal_copy对于'red'得到了与signals相同的列表值。

解决方案是deepcopy()

>>> import copy
>>> signals = {'green': 'go',
... 'yellow': 'go faster',
... 'red': ['stop', 'smile']}
>>> signals_copy = copy.deepcopy(signals)
>>> signals
{'green': 'go',
'yellow': 'go faster',
'red': ['stop', 'smile']}
>>> signals_copy
{'green': 'go',
'yellow':'go faster',
'red': ['stop', 'smile']}
>>> signals['red'][1] = 'sweat'
>>> signals
{'green': 'go',
'yellow': 'go faster',
red': ['stop', 'sweat']}
>>> signals_copy
{'green': 'go',
'yellow': 'go faster',
red': ['stop', 'smile']}

比较字典

与上一章的列表和元组一样,字典可以使用简单的比较运算符==!=进行比较:

>>> a = {1:1, 2:2, 3:3}
>>> b = {3:3, 1:1, 2:2}
>>> a == b
True

其他运算符不起作用:

>>> a = {1:1, 2:2, 3:3}
>>> b = {3:3, 1:1, 2:2}
>>> a <= b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<=' not supported between instances of 'dict' and 'dict'

Python 逐一比较键和值。它们最初创建的顺序无关紧要。在这个例子中,ab是相等的,除了键1a中具有列表值[1, 2],而在b中具有列表值[1, 1]

>>> a = {1: [1, 2], 2: [1], 3:[1]}
>>> b = {1: [1, 1], 2: [1], 3:[1]}
>>> a == b
False

使用forin进行迭代

遍历字典(或其keys()函数)会返回键。在这个例子中,键是桌游 Clue(在北美以外称为 Cluedo)中的卡片类型:

>>> accusation = {'room': 'ballroom', 'weapon': 'lead pipe',
...               'person': 'Col. Mustard'}
>>> for card in accusation:  #  or, for card in accusation.keys():
...     print(card)
...
room
weapon
person

要迭代值而不是键,可以使用字典的values()函数:

>>> for value in accusation.values():
...     print(value)
...
ballroom
lead pipe
Col. Mustard

要返回键和值作为元组,可以使用items()函数:

>>> for item in accusation.items():
...     print(item)
...
('room', 'ballroom')
('weapon', 'lead pipe')
('person', 'Col. Mustard')

可以一步到位地将元组分配给元组。对于由items()返回的每个元组,将第一个值(键)分配给card,第二个值(值)分配给contents

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

字典推导

为了不被那些资产阶级列表所抛弃,字典也有推导。最简单的形式看起来很熟悉:

{*`key_expression`* : *`value_expression`* for *`expression`* in *`iterable`*}
>>> word = 'letters'
>>> letter_counts = {letter: word.count(letter) for letter in word}
>>> letter_counts
{'l': 1, 'e': 2, 't': 2, 'r': 1, 's': 1}

我们正在循环遍历字符串'letters'中的每个字母,并计算该字母出现的次数。两次使用word.count(letter)是浪费时间的,因为我们必须两次计算所有e和所有t。但是当我们第二次计算e时,我们不会有任何损害,因为我们只是替换已经存在的字典条目;对于计算t的情况也是如此。因此,以下方式可能会更符合 Python 的风格:

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

与前面的例子不同,字典的键的顺序不同,因为对set(word)进行迭代会以不同的顺序返回字母,而迭代字符串word则以不同的顺序返回。

与列表推导类似,字典推导也可以有if测试和多个for子句:

{*`key_expression`* : *`value_expression`* for *`expression`* in *`iterable`* if *`condition`*}
>>> vowels = 'aeiou'
>>> word = 'onomatopoeia'
>>> vowel_counts = {letter: word.count(letter) for letter in set(word)
 if letter in vowels}
>>> vowel_counts
{'e': 1, 'i': 1, 'o': 4, 'a': 2}

查看PEP-274以获取更多字典推导的例子。

集合

集合类似于一个字典,它的值被丢弃,只留下键。与字典一样,每个键必须是唯一的。当你只想知道某个东西是否存在时,可以使用集合,而不需要其他信息。它是一组键的袋子。如果你想将一些信息附加到键上作为值,请使用字典。

在某些过去的时代和某些地方,集合理论是与基本数学一起教授的内容。如果你的学校跳过了它(或者你当时在看窗外),图 8-1 展示了集合并和交的概念。

假设你对有一些共同键的两个集合取并集。因为集合必须仅包含每个项的一个副本,两个集合的并集将只包含每个键的一个副本。集合是一个没有元素的集合。在图 8-1 中,空集合的一个例子是以X开头的女性名字。

inp2 0801

图 8-1. 集合的常见操作

使用set()创建集合

要创建一个集合,可以使用set()函数或将一个或多个逗号分隔的值放在花括号中,如下所示:

>>> empty_set = set()
>>> empty_set
set()
>>> even_numbers = {0, 2, 4, 6, 8}
>>> even_numbers
{0, 2, 4, 6, 8}
>>> odd_numbers = {1, 3, 5, 7, 9}
>>> odd_numbers
{1, 3, 5, 7, 9}

集合是无序的。

注意

因为 [] 创建一个空列表,你可能期望 {} 创建一个空集合。相反,{} 创建一个空字典。这也是为什么解释器将空集合打印为 set() 而不是 {}。为什么?字典在 Python 中先出现,并且拿下了花括号的所有权。拥有是法律的九分之一。²

使用 set() 进行转换:

你可以从列表、字符串、元组或字典创建一个集合,丢弃任何重复的值。

首先,让我们看看一个字符串中某些字母出现多次的情况:

>>> set( 'letters' )
{'l', 'r', 's', 't', 'e'}

注意集合只包含一个 'e''t',即使 'letters' 包含两个每个。

现在,让我们从列表创建一个集合:

>>> set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] )
{'Dancer', 'Dasher', 'Mason-Dixon', 'Prancer'}

这次,从元组创建一个集合:

>>> set( ('Ummagumma', 'Echoes', 'Atom Heart Mother') )
{'Ummagumma', 'Atom Heart Mother', 'Echoes'}

当你给 set() 一个字典时,它只使用键:

>>> set( {'apple': 'red', 'orange': 'orange', 'cherry': 'red'} )
{'cherry', 'orange', 'apple'}

使用 len() 获取长度:

让我们数一数我们的驯鹿:

>>> reindeer = set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] )
>>> len(reindeer)
4

使用 add() 添加一个项目:

使用集合的 add() 方法将另一个项目添加到集合中:

>>> s = set((1,2,3))
>>> s
{1, 2, 3}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

使用 remove() 删除一个项目:

你可以通过值从集合中删除一个值:

>>> s = set((1,2,3))
>>> s.remove(3)
>>> s
{1, 2}

使用 for 和 in 进行迭代:

像字典一样,你可以遍历集合中的所有项目:

>>> furniture = set(('sofa', 'ottoman', 'table'))
>>> for piece in furniture:
...     print(piece)
...
ottoman
table
sofa

使用 in 测试一个值:

这是集合的最常见用法。我们将创建一个名为 drinks 的字典。每个键是混合饮料的名称,相应的值是该饮料成分的集合:

>>> drinks = {
...     'martini': {'vodka', 'vermouth'},
...     'black russian': {'vodka', 'kahlua'},
...     'white russian': {'cream', 'kahlua', 'vodka'},
...     'manhattan': {'rye', 'vermouth', 'bitters'},
...     'screwdriver': {'orange juice', 'vodka'}
...     }

即使两者都用花括号({})括起来,一个集合只是一堆值,而字典包含 对。

哪些饮料含有伏特加?

>>> for name, contents in drinks.items():
...     if 'vodka' in contents:
...         print(name)
...
screwdriver
martini
black russian
white russian

我们想要一些伏特加,但我们对乳糖不耐受,并且认为苦艾酒味道像煤油:

>>> for name, contents in drinks.items():
...     if 'vodka' in contents and not ('vermouth' in contents or
...         'cream' in contents):
...         print(name)
...
screwdriver
black russian

我们将在下一节中简要重写这段话。

组合和操作符

如果你想要检查集合值的组合怎么办?假设你想要找到任何含有橙汁或苦艾酒的饮料?让我们使用 集合交集操作符,即 &

>>> for name, contents in drinks.items():
...     if contents & {'vermouth', 'orange juice'}:
...         print(name)
...
screwdriver
martini
manhattan

& 操作符的结果是一个集合,其中包含与比较的两个列表中都出现的所有项。如果这些成分都不在 contents 中,则 & 返回一个空集合,被视为 False

现在,让我们重新写前一节的示例,在这个示例中,我们想要伏特加,但不要奶油和苦艾酒:

>>> for name, contents in drinks.items():
...     if 'vodka' in contents and not contents & {'vermouth', 'cream'}:
...         print(name)
...
screwdriver
black russian

让我们把这两种饮料的成分集合保存在变量中,以节省我们娇贵的手指在接下来的示例中的打字:

>>> bruss = drinks['black russian']
>>> wruss = drinks['white russian']

下面是所有集合操作符的示例。有些有特殊的标点符号,有些有特殊的功能,还有些两者兼有。让我们使用测试集合 a(包含 12)和 b(包含 23):

>>> a = {1, 2}
>>> b = {2, 3}

正如你之前看到的,使用特殊标点符号 & 获取 交集(两个集合共同的成员)。集合 intersection() 函数也是如此。

>>> a & b
{2}
>>> a.intersection(b)
{2}

此代码片段使用我们保存的饮料变量:

>>> bruss & wruss
{'kahlua', 'vodka'}

在这个例子中,使用特殊标点符号 & 获取 并集(两个集合的成员)或集合 union() 函数。

>>> a | b
{1, 2, 3}
>>> a.union(b)
{1, 2, 3}

这是酒精版本:

>>> bruss | wruss
{'cream', 'kahlua', 'vodka'}

差集(第一个集合的成员但不是第二个集合的成员)可以通过使用字符-difference()函数获得:

>>> a - b
{1}
>>> a.difference(b)
{1}
>>> bruss - wruss
set()
>>> wruss - bruss
{'cream'}

到目前为止,最常见的集合操作是并集、交集和差集。我在接下来的示例中包括了其他操作,但你可能永远不会用到它们。

异或(一个集合中的项目或另一个,但不是两者都有)使用^symmetric_difference()

>>> a ^ b
{1, 3}
>>> a.symmetric_difference(b)
{1, 3}

这找到我们两种俄罗斯饮料中独特的成分:

>>> bruss ^ wruss
{'cream'}

你可以通过使用<=issubset()来检查一个集合是否是另一个的子集(第一个集合的所有成员也在第二个集合中):

>>> a <= b
False
>>> a.issubset(b)
False

给黑俄罗斯加奶油会变成白俄罗斯,所以wrussbruss的超集:

>>> bruss <= wruss
True

任何集合都是其自身的子集吗?是的。³

>>> a <= a
True
>>> a.issubset(a)
True

要成为一个真子集,第二个集合需要包含第一个集合的所有成员及更多。通过使用<,如下例所示:

>>> a < b
False
>>> a < a
False
>>> bruss < wruss
True

超集是子集的反义词(第二个集合的所有成员也是第一个集合的成员)。这使用>=issuperset()

>>> a >= b
False
>>> a.issuperset(b)
False
>>> wruss >= bruss
True

任何集合都是其自身的超集:

>>> a >= a
True
>>> a.issuperset(a)
True

最后,你可以通过使用>来找到一个真子集(第一个集合包含第二个集合的所有成员,且更多)如下所示:

>>> a > b
False
>>> wruss > bruss
True

你不能是你自己的真超集:

>>> a > a
False

集合推导式

没有人想被忽略,所以即使是集合也有推导式。最简单的版本看起来像你刚刚看到的列表和字典推导式:

{ expression for expression in iterable }

它还可以具有可选的条件测试:

{ expression for expression in iterable if condition }

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

使用frozenset()创建不可变集合

如果你想创建一个不能被更改的集合,可以使用frozenset()函数和任何可迭代参数:

>>> frozenset([3, 2, 1])
frozenset({1, 2, 3})
>>> frozenset(set([2, 1, 3]))
frozenset({1, 2, 3})
>>> frozenset({3, 1, 2})
frozenset({1, 2, 3})
>>> frozenset( (2, 3, 1) )
frozenset({1, 2, 3})

它真的冻结了吗?

>>> fs = frozenset([3, 2, 1])
>>> fs
frozenset({1, 2, 3})
>>> fs.add(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'frozenset' object has no attribute 'add'

是的,非常冷。

到目前为止的数据结构

回顾一下,你可以创建

  • 通过使用方括号([])创建列表

  • 通过逗号和可选括号创建元组

  • 通过使用花括号({})创建字典或集合

除了集合,你可以通过使用方括号访问单个元素。对于列表和元组,方括号中的值是整数偏移量。对于字典,它是一个键。对于这三个,结果是一个值。对于集合,要么有,要么没有;没有索引或键:

>>> marx_list = ['Groucho', 'Chico', 'Harpo']
>>> marx_tuple = ('Groucho', 'Chico', 'Harpo')
>>> marx_dict = {'Groucho': 'banjo', 'Chico': 'piano', 'Harpo': 'harp'}
>>> marx_set = {'Groucho', 'Chico', 'Harpo'}
>>> marx_list[2]
'Harpo'
>>> marx_tuple[2]
'Harpo'
>>> marx_dict['Harpo']
'harp'
>>> 'Harpo' in marx_list
True
>>> 'Harpo' in marx_tuple
True
>>> 'Harpo' in marx_dict
True
>>> 'Harpo' in marx_set
True

创建更大的数据结构

我们从简单的布尔值、数字和字符串逐步进阶到列表、元组、集合和字典。你可以将这些内置数据结构组合成更大、更复杂的结构。让我们从三个不同的列表开始:

>>> marxes = ['Groucho', 'Chico', 'Harpo']
>>> pythons = ['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin']
>>> stooges = ['Moe', 'Curly', 'Larry']

我们可以创建一个包含每个列表作为元素的元组:

>>> tuple_of_lists = marxes, pythons, stooges
>>> tuple_of_lists
(['Groucho', 'Chico', 'Harpo'],
['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin'],
['Moe', 'Curly', 'Larry'])

我们可以创建一个包含这三个列表的列表:

>>> list_of_lists = [marxes, pythons, stooges]
>>> list_of_lists
[['Groucho', 'Chico', 'Harpo'],
['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin'],
['Moe', 'Curly', 'Larry']]

最后,让我们创建一个字典的列表。在这个例子中,让我们使用喜剧组的名字作为键,成员列表作为值:

>>> dict_of_lists = {'Marxes': marxes, 'Pythons': pythons, 'Stooges': stooges}
>> dict_of_lists
{'Marxes': ['Groucho', 'Chico', 'Harpo'],
'Pythons': ['Chapman', 'Cleese', 'Gilliam', 'Jones', 'Palin'],
'Stooges': ['Moe', 'Curly', 'Larry']}

你的唯一限制是数据类型本身的限制。例如,字典键必须是不可变的,因此列表、字典或集合不能作为另一个字典的键。但元组可以。例如,你可以通过 GPS 坐标(纬度、经度和高度;请参见 第二十一章 获取更多映射示例)来索引感兴趣的站点:

>>> houses = {
 (44.79, -93.14, 285): 'My House',
 (38.89, -77.03, 13): 'The White House'
 }

即将发生

回到代码结构。你将学习如何将代码包装在 函数 中,以及在出现 异常 时如何处理。

要做的事情

8.1 制作一个英语到法语的字典 e2f 并打印出来。以下是你的起始词汇:dogchiencatchatwalrusmorse

8.2 使用你的三词字典 e2f,打印法语单词 walrus 的英文对应词。

8.3 使用 e2f 创建一个法语到英语的字典 f2e。使用 items 方法。

8.4 打印法语单词 chien 的英文对应词。

8.5 打印从 e2f 中得到的英语单词集合。

8.6 制作一个名为 life 的多级字典。使用以下字符串作为最顶层键:'animals''plants''other'。使 'animals' 键参考另一个具有键 'cats''octopi''emus' 的字典。使 'cats' 键参考一个包含值 'Henri''Grumpy''Lucy' 的字符串列表。使所有其他键参考空字典。

8.7 打印 life 的顶层键。

8.8 打印 life['animals'] 的键。

8.9 打印 life['animals']['cats'] 的值。

8.10 使用字典推导来创建字典 squares。使用 range(10) 返回键,并使用每个键的平方作为其值。

8.11 使用集合推导来创建集合 odd,其中包含 range(10) 中的奇数。

8.12 使用生成器推导来返回字符串 'Got 'range(10) 中的数字。通过使用 for 循环进行迭代。

8.13 使用 zip() 从键元组 ('optimist', 'pessimist', 'troll') 和值元组 ('The glass is half full', 'The glass is half empty', 'How did you get a glass?') 创建一个字典。

8.14 使用 zip() 创建一个名为 movies 的字典,将以下列表配对:titles = ['Creature of Habit', 'Crewel Fate', 'Sharks On a Plane']plots = ['A nun turns into a monster', 'A haunted yarn shop', 'Check your exits']

¹ 同时,Strontium-Carbon 游戏的最终得分。

² 根据律师和驱魔师的说法。

³ 尽管,借用格劳乔·马尔克斯的话,“我不想加入那种会接受我作为成员的俱乐部。”

第九章:函数

函数越小,管理越大。

C. 北科特·帕金森

到目前为止,我们所有的 Python 代码示例都是小片段。这些对于小任务很好,但没人想一直重复输入片段。我们需要一种方法将更大的代码组织成可管理的片段。

代码重用的第一步是函数:一个命名的代码片段,独立于所有其他代码。函数可以接受任意数量和类型的输入参数,并返回任意数量和类型的输出结果

你可以用一个函数做两件事情:

  • 定义它,带有零个或多个参数

  • 调用它,然后得到零个或多个结果

用 def 定义一个函数

要定义一个 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这样的测试以及诸如while这样的循环的结合,使你能够做一些以前无法做到的事情。

参数和参数

此时,是时候在括号里放些东西了。让我们定义一个名为echo()的函数,其中有一个名为anything的参数。它使用return语句两次将anything的值发送回调用者,之间用空格隔开:

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

现在让我们用字符串'Rumplestiltskin'调用echo()

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

当你调用带有参数的函数时,你传递的这些值称为参数。当你带有参数调用函数时,这些参数的值被复制到函数内部的对应参数中。

注意

换句话说:它们在函数外被称为参数,但在函数内部称为参数

在上一个示例中,函数echo()被调用时带有参数字符串'Rumplestiltskin'。此值在echo()内部复制到参数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.

函数可以接受任意数量(包括零个)任意类型的输入参数。它可以返回任意数量(包括零个)任意类型的输出结果。如果函数没有显式调用return,调用者将得到None的结果。

>>> print(do_nothing())
None

None 很有用

None是 Python 中的特殊值,表示当没有内容时占据的位置。它与布尔值False不同,尽管在布尔值评估时看起来是假的。以下是一个例子:

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

要区分None和布尔值False,请使用 Python 的is运算符:

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

这似乎是一个微妙的区别,但在 Python 中很重要。您将需要None来区分缺失值和空值。请记住,零值整数或浮点数,空字符串(''),列表([]),元组((,)),字典({})和集合(set())都为False,但与None不同。

让我们编写一个快速函数,打印其参数是NoneTrue还是False

>>> def whatis(thing):
...     if thing is None:
...         print(thing, "is None")
...     elif thing:
...         print(thing, "is True")
...     else:
...         print(thing, "is False")
...

让我们运行一些健全性测试:

>>> whatis(None)
None is None
>>> whatis(True)
True is True
>>> whatis(False)
False is False

一些真实的值如何?

>>> whatis(0)
0 is False
>>> whatis(0.0)
0.0 is False
>>> whatis('')
 is False
>>> whatis("")
 is False
>>> whatis('''''')
 is False
>>> whatis(())
() is False
>>> whatis([])
[] is False
>>> whatis({})
{} is False
>>> whatis(set())
set() is False
>>> whatis(0.00001)
1e-05 is True
>>> whatis([0])
[0] is True
>>> whatis([''])
[''] is True
>>> whatis(' ')
 is True

位置参数

与许多语言相比,Python 处理函数参数的方式非常灵活。最熟悉的类型是位置参数,其值按顺序复制到相应的参数中。

此函数从其位置输入参数构建字典并返回它:

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

尽管非常普遍,位置参数的缺点是您需要记住每个位置的含义。如果我们忘记并将menu()以葡萄酒作为最后一个参数而不是第一个参数进行调用,那么餐点会非常不同:

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

关键字参数

为了避免位置参数混淆,您可以按照与函数定义中不同顺序的参数名称指定参数:

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

您可以混合位置和关键字参数。让我们首先指定葡萄酒,但使用关键字参数为主菜和甜点:

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

如果您同时使用位置和关键字参数调用函数,则位置参数需要首先出现。

指定默认参数值

您可以为参数指定默认值。如果调用者未提供相应的参数,则使用默认值。这个听起来平淡无奇的特性实际上非常有用。使用前面的例子:

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

这次,尝试调用menu()而不带dessert参数:

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

如果提供了参数,则使用该参数而不是默认值:

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

默认参数值在函数定义时计算,而不是在运行时。对于新手(有时甚至不太新的)Python 程序员来说,常见的错误是使用可变数据类型(如列表或字典)作为默认参数。

在以下测试中,buggy()函数预计每次都将使用一个新的空result列表运行,并将arg参数添加到其中,然后打印一个单项列表。然而,这里有一个错误:它只有在第一次调用时为空。第二次调用时,result仍然保留了上一次调用的一个项目:

>>> def buggy(arg, result=[]):
...     result.append(arg)
...     print(result)
...
>>> buggy('a')
['a']
>>> buggy('b')   # expect ['b']
['a', 'b']

如果写成这样,它会起作用:

>>> def works(arg):
...     result = []
...     result.append(arg)
...     return result
...
>>> works('a')
['a']
>>> works('b')
['b']

修复方法是传递其他内容以指示第一次调用:

>>> def nonbuggy(arg, result=None):
...     if result is None:
...         result = []
...     result.append(arg)
...     print(result)
...
>>> nonbuggy('a')
['a']
>>> nonbuggy('b')
['b']

这有时是 Python 工作面试的问题。你已经被警告了。

使用*爆炸/聚合位置参数

如果你在 C 或 C++中编程过,你可能会认为 Python 程序中的星号(*)与指针有关。不,Python 没有指针。

当在函数中与参数一起使用时,星号将可变数量的位置参数组合成单个参数值元组。在下面的例子中,args是由传递给函数print_args()的零个或多个参数组成的参数元组:

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

如果没有参数调用函数,则在*args中什么都得不到:

>>> print_args()
Positional tuple: ()

无论您给它什么,它都将被打印为args元组:

>>> print_args(3, 2, 1, 'wait!', 'uh...')
Positional 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 中这是一种常见的习惯用法。在函数内部使用*args也很常见,就像前面的例子中描述的那样,尽管严格来说它被称为参数,可以称为*params

总结:

  • 您可以将位置参数传递给函数,它们将在内部与位置参数匹配。这是你在本书中到目前为止看到的内容。

  • 您可以将元组参数传递给函数,其中它将成为元组参数的一部分。这是前面一个示例的简单情况。

  • 您可以将位置参数传递给函数,并将它们聚合在参数*args中,该参数解析为元组args。这在本节中已经描述过。

  • 您还可以将名为args的元组参数“爆炸”为函数内部的位置参数*args,然后将其重新聚合到元组参数args中:

>>> print_args(2, 5, 7, 'x')
Positional tuple: (2, 5, 7, 'x')
>>> args = (2,5,7,'x')
>>> print_args(args)
Positional tuple: ((2, 5, 7, 'x'),)
>>> print_args(*args)
Positional tuple: (2, 5, 7, 'x')

您只能在函数调用或定义中使用*语法:

>>> *args
  File "<stdin>", line 1
SyntaxError: can't use starred expression here

所以:

  • 在函数外部,*args 将元组 args 展开为逗号分隔的位置参数。

  • 在函数内部,*args 将所有位置参数收集到一个名为 args 的元组中。你可以使用名称 *paramsparams,但通常在外部参数和内部参数中都使用 *args 是常见的做法。

有音感共鸣的读者也可能会在外部听到 *args 声称为 puff-args,在内部听到 inhale-args,因为值要么被展开要么被汇集。

用 ** 扩展/汇集关键字参数

你可以使用两个星号 (**) 将关键字参数组合成一个字典,其中参数名是键,它们的值是对应的字典值。下面的示例定义了函数 print_kwargs() 来打印它的关键字参数:

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

现在试着使用一些关键字参数调用它:

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

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

参数顺序是:

  • 必需的位置参数

  • 可选的位置参数 (*args)

  • 可选的关键字参数 (**kwargs)

args 一样,你不需要将这个关键字参数称为 kwargs,但这是常见用法:¹

** 语法仅在函数调用或定义中有效:²

>>> **kwparams
  File "<stdin>", line 1
    **kwparams
     ^
SyntaxError: invalid syntax

总结:

  • 你可以向函数传递关键字参数,函数内部将它们与关键字参数匹配。这就是你迄今为止看到的内容。

  • 你可以将字典参数传递给一个函数,函数内部会解析这些字典参数。这是前面讨论的一个简单情况。

  • 你可以向函数传递一个或多个关键字参数 (name=value),并在函数内部作为 **kwargs 收集它们,这已经在本节中讨论过了。

  • 在函数外部,**kwargs 展开 字典 kwargsname=value 参数。

  • 在函数内部,**kwargs 收集 name=value 参数到单个字典参数 kwargs 中。

如果听觉幻觉有所帮助,想象每个星号都在函数外面爆炸一下,而每个星号聚集在里面时会有一点吸气的声音。

仅关键字参数

可以传入一个与位置参数同名的关键字参数,这可能不会得到你想要的结果。Python 3 允许你指定 仅关键字参数。顾名思义,它们必须作为 name=value 而不是作为位置参数 value 提供。函数定义中的单个 * 意味着接下来的参数 startend 如果我们不想使用它们的默认值,必须作为命名参数提供:

>>> def print_data(data, *, start=0, end=100):
...     for value in (data[start:end]):
...         print(value)
...
>>> data = ['a', 'b', 'c', 'd', 'e', 'f']
>>> print_data(data)
a
b
c
d
e
f
>>> print_data(data, start=4)
e
f
>>> print_data(data, end=2)
a
b

可变与不可变参数

记住,如果你将同一个列表分配给两个变量,你可以通过任何一个变量来修改它?而如果这两个变量都引用像整数或字符串之类的不可变对象时就不行了?那是因为列表是可变的,而整数和字符串是不可变的。

在将参数传递给函数时,需要注意相同的行为。如果一个参数是可变的,它的值可以通过相应的参数在函数内部被改变:³

>>> outside = ['one', 'fine', 'day']
>>> def mangle(arg):
...    arg[1] = 'terrible!'
...
>>> outside
['one', 'fine', 'day']
>>> mangle(outside)
>>> outside
['one', 'terrible!', 'day']

最好的做法是,呃,不要这样做。⁴ 要么记录参数可能被更改,要么通过return返回新值。

文档字符串

可读性很重要,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(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 术语中称为dunder)在许多地方用于命名 Python 内部变量,因为程序员不太可能在自己的变量名中使用它们。

函数是一等公民

我提到了 Python 的口头禅,一切皆为对象。这包括数字、字符串、元组、列表、字典——还有函数。在 Python 中,函数是一等公民。你可以将它们赋值给变量,将它们用作其他函数的参数,并从函数中返回它们。这使得你能够在 Python 中做一些在许多其他语言中难以或不可能实现的事情。

要测试这个,让我们定义一个简单的函数叫做answer(),它没有任何参数;它只是打印数字42

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

如果你运行这个函数,你知道会得到什么:

>>> answer()
42

现在让我们定义另一个名为run_something的函数。它有一个名为func的参数,一个要运行的函数。进入函数后,它只是调用这个函数:

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

如果我们将answer传递给run_something(),我们正在使用函数作为数据,就像其他任何东西一样:

>>> run_something(answer)
42

注意,你传递的是answer,而不是answer()。在 Python 中,那些括号意味着调用这个函数。没有括号,Python 只是将函数视为任何其他对象一样对待。这是因为,像 Python 中的一切其他东西一样,它一个对象:

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

让我们尝试运行一个带有参数的函数。定义一个名为add_args()的函数,打印其两个数值参数arg1arg2的和:

>>> 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参数,而arg1arg2得到了在参数列表中跟随的值。然后,运行func(arg1, arg2)使用这些参数执行该函数,因为括号告诉 Python 这样做。

让我们通过向函数名add_args和参数59传递给run_something_with_args()来测试它:

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

在函数run_something_with_args()中,函数名参数add_args被赋给了参数func5被赋给了参数arg19被赋给了参数arg2。这最终执行了:

add_args(5, 9)

您可以将此与*args**kwargs技术结合使用。

让我们定义一个测试函数,它接受任意数量的位置参数,通过使用sum()函数计算它们的总和,然后返回该总和:

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

我之前没有提到sum()。它是一个内置的 Python 函数,用于计算其可迭代数值(int 或 float)参数中值的总和。

让我们定义新函数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

您可以将函数用作列表、元组、集合和字典的元素。函数是不可变的,因此您也可以将它们用作字典键。

内部函数

你可以在一个函数内定义另一个函数:

>>> 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()示例构建的。让我们称其为knights2(),因为我们没有想象力,并将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')

好的,那么ab是什么?

>>> 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—应用于 words 中每个单词的函数

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

现在我们需要一个单词列表和一个应用到每个单词的函数。对于单词,这里是我家猫(假设的情况下)如果(假设的情况下)错过了其中一个楼梯可能发出的一系列(假设的)声音:

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

而对于函数,这将使每个单词大写并追加一个感叹号,非常适合猫类小报的头条新闻:

>>> def enliven(word):   # give that prose more punch
...     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 函数有零个或多个逗号分隔的参数,后跟一个冒号(:),然后是函数的定义。我们给这个 lambda 一个参数 word。你不像调用 def 创建的函数那样在 lambda 函数中使用括号。

通常,使用真正的函数如 enliven() 要比使用 lambda 函数更清晰。Lambda 函数主要用于在否则需要定义许多小函数并记住它们名称的情况下非常有用。特别是在图形用户界面中,你可以用 lambda 来定义 回调函数;详见第二十章的示例。

生成器

一个 生成器 是 Python 的序列创建对象。利用它,你可以在不一次性创建和存储整个序列于内存中的情况下遍历可能非常庞大的序列。生成器通常是迭代器的数据源。如果你还记得,在之前的代码示例中我们已经使用了其中一个,range(),来生成一系列整数。在 Python 2 中,range() 返回一个列表,这限制了它的内存使用。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
3
4
注意

生成器只能运行一次。列表、集合、字符串和字典存在于内存中,但生成器会即时创建其值,并逐个通过迭代器分发它们。它不记住它们,因此你无法重新启动或备份生成器。

如果你尝试再次迭代这个生成器,你会发现它已经耗尽了:

>>> for try_again in ranger:
...     print(try_again)
...
>>>

生成器推导式

您已经看到了用于列表、字典和集合的理解。生成器理解 看起来像那些,但是用圆括号而不是方括号或花括号括起来。这就像是生成器函数的简写版本,隐式地执行 yield 并返回生成器对象:

>>> genobj = (pair for pair in zip(['a', 'b'], ['1', '2']))
>>> genobj
<generator object <genexpr> at 0x10308fde0>
>>> for thing in genobj:
...     print(thing)
...
('a', '1')
('b', '2')

装饰器

有时,您希望修改现有函数而不更改其源代码。一个常见的例子是添加调试语句以查看传递的参数是什么。

装饰器 是一个接受一个函数作为输入并返回另一个函数的函数。让我们深入探讨一下我们的 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)  # manual decorator assignment
>>> cooler_add_ints(3, 5)
Running function: add_ints
Positional 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

命名空间和作用域

渴望这个人的艺术和那个人的视野

威廉·莎士比亚

一个名称可以根据其使用的位置引用不同的事物。Python 程序有各种命名空间 —— 某个名称在其中是唯一的,并且与其他命名空间中相同名称无关。

每个函数定义其自己的命名空间。如果您在主程序中定义了一个名为 x 的变量,并在函数中定义了另一个名为 x 的变量,它们将指代不同的事物。但是,墙壁是可以打破的:如果需要,可以通过各种方式访问其他命名空间中的名称。

程序的主要部分定义了全局命名空间;因此,在该命名空间中的变量是全局变量

您可以从函数内部获取全局变量的值:

>>> animal = 'fruitbat'
>>> def print_global():
...     print('inside print_global:', animal)
...
>>> print('at the top level:', animal)
at the top level: fruitbat
>>> print_global()
inside print_global: fruitbat

但是,如果您尝试在函数内获取全局变量的值并且更改它,您将会得到一个错误:

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

如果您只是更改它,则还会更改另一个名为 animal 的变量,但此变量位于函数内部:

>>> def change_local():
...     animal = 'wombat'
...     print('inside change_local:', animal, id(animal))
...
>>> change_local()
inside change_local: wombat 4330406160
>>> animal
'fruitbat'
>>> id(animal)
4330390832

发生了什么?第一行将字符串 'fruitbat' 分配给名为 animal 的全局变量。change_local() 函数中也有一个名为 animal 的变量,但它在其局部命名空间中。

我在这里使用了 Python 函数 id() 来打印每个对象的唯一值,并证明 change_local() 内部的变量 animal 不同于程序主层级的 animal

在函数内部访问全局变量而不是局部变量,你需要显式使用 global 关键字(你知道这是必须的:显式优于隐式):

>>> animal = 'fruitbat'
>>> def change_and_print_global():
...     global animal
...     animal = 'wombat'
...     print('inside change_and_print_global:', animal)
...
>>> animal
'fruitbat'
>>> change_and_print_global()
inside change_and_print_global: wombat
>>> animal
'wombat'

如果在函数内部不使用 global,Python 使用局部命名空间,变量是局部的。函数执行完成后,它就消失了。

Python 提供了两个函数来访问你的命名空间的内容:

  • locals() 返回本地命名空间内容的字典。

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

并且它们在使用中:

>>> animal = 'fruitbat'
>>> def change_local():
...     animal = 'wombat'  # local variable
...     print('locals:', locals())
...
>>> animal
'fruitbat'
>>> change_local()
locals: {'animal': 'wombat'}
>>> print('globals:', globals()) # reformatted a little for presentation
globals: {'animal': 'fruitbat',
'__doc__': None,
'change_local': <function change_local at 0x1006c0170>,
'__package__': None,
'__name__': '__main__',
'__loader__': <class '_frozen_importlib.BuiltinImporter'>,
'__builtins__': <module 'builtins'>}
>>> animal
'fruitbat'

change_local() 内的局部命名空间仅包含局部变量 animal。全局命名空间包含独立的全局变量 animal 和许多其他内容。

在名称中使用 _ 和 __

以两个下划线 (__) 开头和结尾的名称保留供 Python 使用,因此你不应该将它们用于自己的变量。选择这种命名模式是因为看起来不太可能被应用开发人员选为其自己的变量名。

例如,函数的名称在系统变量 function.__name__ 中,其文档字符串在 function.__doc__ 中:

>>> def amazing():
...     '''This is the amazing function.
...     Want to see it again?'''
...     print('This function is named:', amazing.__name__)
...     print('And its docstring is:', amazing.__doc__)
...
>>> amazing()
This function is named: amazing
And its docstring is: This is the amazing function.
 Want to see it again?

正如你在前面的 globals 打印中看到的那样,主程序被赋予特殊名称 __main__

递归

到目前为止,我们已经调用了一些直接执行某些操作的函数,并可能调用其他函数。但如果一个函数调用自身呢?⁵ 这就是递归。就像使用 whilefor 的无限循环一样,你不希望出现无限递归。我们仍然需要担心时空连续性的裂缝吗?

Python 再次拯救了宇宙,如果你深入太多,它会引发异常:

>>> def dive():
...     return dive()
...
>>> dive()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in dive
  File "<stdin>", line 2, in dive
  File "<stdin>", line 2, in dive
 [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

当处理像列表的列表的列表这样的“不均匀”数据时,递归非常有用。假设你想要“展平”列表的所有子列表,⁶ 无论嵌套多深,生成器函数正是你需要的:

>>> def flatten(lol):
...     for item in lol:
...         if isinstance(item, list):
...             for subitem in flatten(item):
...                 yield subitem
...         else:
...             yield item
...
>>> lol = [1, 2, [3,4,5], [6,[7,8,9], []]]
>>> flatten(lol)
<generator object flatten at 0x10509a750>
>>> list(flatten(lol))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Python 3.3 添加了 yield from 表达式,允许生成器将一些工作交给另一个生成器。我们可以用它来简化 flatten()

>>> def flatten(lol):
...     for item in lol:
...         if isinstance(item, list):
...             yield from flatten(item)
...         else:
...             yield item
...
>>> lol = [1, 2, [3,4,5], [6,[7,8,9], []]]
>>> list(flatten(lol))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

异步函数

关键字 asyncawait 被添加到 Python 3.5 中,用于定义和运行异步函数。它们是:

  • 相对较新

  • 足够不同,以至于更难理解

  • 随着时间的推移,它会变得更重要且更为人熟知

出于这些原因,我已将这些及其他异步主题的讨论移到了附录 C 中的 Appendix C。

现在,你需要知道,如果在函数的def行之前看到async,那么这是一个异步函数。同样,如果在函数调用之前看到await,那么该函数是异步的。

异步函数和普通函数的主要区别在于异步函数可以“放弃控制”,而不是一直运行到完成。

异常

在某些语言中,错误通过特殊的函数返回值来指示。当事情出错时,Python 使用异常:当关联的错误发生时执行的代码。

你已经看到了一些例子,比如访问列表或元组时使用超出范围的位置,或者使用不存在的键访问字典。当你运行在某些情况下可能失败的代码时,还需要适当的异常处理程序来拦截任何潜在的错误。

在任何可能发生异常的地方添加异常处理是一个好习惯,以便让用户知道发生了什么。你可能无法修复问题,但至少可以记录情况并优雅地关闭程序。如果异常发生在某个函数中并且没有在那里捕获,它会冒泡直到在某个调用函数中找到匹配的处理程序。如果不提供自己的异常处理程序,Python 会打印错误消息和关于错误发生位置的一些信息,然后终止程序,如以下代码片段所示:

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

使用 try 和 except 处理错误

做,或者不做。没有尝试。

尤达

不要留事情交给机会,使用try包装你的代码,并使用except提供错误处理:

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

执行try块内的代码。如果出现错误,将引发异常并执行except块内的代码。如果没有错误,则跳过except块。

在这里使用没有参数的纯except来指定是一个通用的异常捕获。如果可能会出现多种类型的异常,最好为每种类型提供单独的异常处理程序。没有人强迫你这样做;你可以使用裸露的except来捕获所有异常,但你对它们的处理可能是通用的(类似于打印发生了一些错误)。你可以使用任意数量的特定异常处理程序。

有时候,你希望除了类型以外的异常详情。如果使用以下形式,你会在变量name中得到完整的异常对象:

except *`exceptiontype`* as *`name`*

以下示例首先查找IndexError,因为当你向序列提供非法位置时,会引发该异常类型。它将IndexError异常保存在变量err中,将其他任何异常保存在变量other中。示例打印other中存储的所有内容,以展示你在该对象中得到的内容:

>>> short_list = [1, 2, 3]
>>> while True:
...     value = input('Position [q to quit]? ')
...     if value == 'q':
...         break
...     try:
...         position = int(value)
...         print(short_list[position])
...     except IndexError as err:
...         print('Bad index:', position)
...     except Exception as other:
...         print('Something else broke:', other)
...
Position [q to quit]? 1
2
Position [q to quit]? 0
1
Position [q to quit]? 2
3
Position [q to quit]? 3
Bad index: 3
Position [q to quit]? 2
3
Position [q to quit]? two
Something else broke: invalid literal for int() with base 10: 'two'
Position [q to quit]? q

输入位置3引发了预期的IndexError。输入two使int()函数感到恼火,我们在第二个通用的except代码中处理了它。

创建自定义异常

上一节讨论了处理异常,但所有的异常(如IndexError)都是在 Python 或其标准库中预定义的。你可以为自己的程序使用其中任何一个。你也可以定义自己的异常类型来处理可能在你自己的程序中出现的特殊情况。

注意

这需要定义一个新的对象类型,使用一个——这是我们直到第十章才会详细讨论的内容。所以,如果你对类不熟悉,可能需要稍后再回到本节。

异常是一个类。它是类Exception的子类。让我们创建一个叫做UppercaseException的异常,在字符串中遇到大写字母时引发它:

>>> class UppercaseException(Exception):
...     pass
...
>>> words = ['eenie', 'meenie', 'miny', 'MO']
>>> for word in words:
...     if word.isupper():
...         raise UppercaseException(word)
...
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
__main__.UppercaseException: MO

我们甚至没有为UppercaseException定义任何行为(注意我们只是使用了pass),让它的父类Exception来决定在引发异常时打印什么。

你可以访问异常对象本身并打印它:

>>> try:
...     raise OopsException('panic')
... except OopsException as exc:
...     print(exc)
...
panic

即将出现

对象!在一本关于面向对象语言的书中,我们必须介绍它们。

要做的事情

9.1 定义一个名为good()的函数,返回以下列表:['Harry', 'Ron', 'Hermione']

9.2 定义一个名为get_odds()的生成器函数,返回range(10)中的奇数。使用for循环找到并打印第三个返回的值。

9.3 定义一个名为test的装饰器,在调用函数时打印'start',在函数结束时打印'end'

9.4 定义一个名为OopsException的异常。引发这个异常看看会发生什么。然后,编写代码捕捉这个异常并打印'Caught an oops'

¹ 虽然ArgsKwargs听起来像海盗鹦鹉的名字。

² 或者,如 Python 3.5 中的字典合并形式{**a, **b},就像你在第八章看到的那样。

³ 就像那些青少年陷入危险的电影中他们学会了“电话是从房子里打来的!”

⁴ 就像那个老医生笑话:“当我这样做时很痛。” “那么,就别这样做。”

⁵ 这就像说,“如果我每次希望有一美元,我就能有一美元。”

⁶ 又是一个 Python 面试问题。收集整套吧!

⁷ 这是北半球主义吗?澳大利亚人和新西兰人说东西乱了会说“north”吗?