Python 入门指南第二版(四)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十一章:模块、包和好东西

在您从底层向上攀登的过程中,您已经从内置数据类型进展到构建越来越大的数据和代码结构。在本章中,您终于学会如何在 Python 中编写现实的完整程序。您将编写自己的模块,并学习如何使用来自 Python 标准库和其他来源的模块。

本书的文本是按层次结构组织的:单词、句子、段落和章节。否则,它会很快变得难以阅读。¹ 代码的组织原则大致相同:数据类型类似于单词;表达式和语句类似于句子;函数类似于段落;模块类似于章节。继续这个类比,在本书中,当我说某些内容将在第八章中解释时,在编程中这就像是引用另一个模块中的代码。

模块和 import 语句

我们将在多个文件中创建和使用 Python 代码。模块只是包含任何 Python 代码的文件。您不需要做任何特殊处理——任何 Python 代码都可以被其他人用作模块。

我们通过使用 Python 的import语句引用其他模块的代码。这使得导入模块中的代码和变量对您的程序可用。

导入一个模块

import语句的最简单用法是import module,其中*module是另一个 Python 文件的名称,不带.py*扩展名。

假设你和几个人想要快速解决午餐问题,但又不想进行长时间的讨论,最后总是由最吵闹的那个人决定。让电脑来决定吧!让我们编写一个单一函数的模块,返回一个随机的快餐选择,以及调用该函数并打印选择的主程序。

模块(fast.py)显示在示例 11-1 中。

示例 11-1。fast.py
from random import choice

places = ['McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():  # see the docstring below?
    """Return random fast food place"""
    return choice(places)

并且示例 11-2 展示了导入它的主程序(称为lunch.py)。

示例 11-2。lunch.py
import fast

place = fast.pick()
print("Let's go to", place)

如果您将这两个文件放在同一个目录中,并指示 Python 将lunch.py作为主程序运行,它将访问fast模块并运行其pick()函数。我们编写了pick()的这个版本,以从字符串列表中返回一个随机结果,因此主程序将获得并打印出这个结果:

$ python lunch.py
Let's go to Burger King
$ python lunch.py
Let's go to Pizza Hut
$ python lunch.py
Let's go to Arbys

我们在两个不同的地方使用了导入:

  • 主程序lunch.py导入了我们的新模块fast

  • 模块文件fast.py从 Python 的标准库模块random中导入了choice函数。

我们在主程序和模块中以两种不同的方式使用了导入:

  • 在第一种情况下,我们导入了整个 fast 模块,但需要使用 fast 作为 pick() 的前缀。在这个 import 语句之后,只要我们在名称前加上 fast.fast.py 中的所有内容对主程序都是可用的。通过用模块的名称限定模块的内容,我们避免了任何糟糕的命名冲突。其他模块可能有一个 pick() 函数,我们不会误调用它。

  • 在第二种情况下,我们在一个模块内部,并且知道这里没有其他名为 choice 的内容,因此直接从 random 模块中导入了 choice() 函数。

我们本可以像示例 11-3 中所示那样编写 fast.py,在 pick() 函数内部导入 random 而不是在文件顶部。

示例 11-3. fast2.py
places = ['McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():
    import random
    return random.choice(places)

像编程的许多方面一样,使用最清晰的风格。使用模块限定的名称(random.choice)更安全,但需要稍微多输入一些字。

如果导入的代码可能在多个地方使用,请考虑从函数外部导入;如果知道其使用将受限制,请从函数内部导入。有些人喜欢将所有导入都放在文件顶部,以明确代码的所有依赖关系。无论哪种方式都可以。

使用另一个名称导入模块

在我们的主 lunch.py 程序中,我们调用了 import fast。但如果你:

  • 还有另一个名为 fast 的模块吗?

  • 想使用一个更易记的名称吗?

  • 门夹到手指想减少打字?

在这些情况下,你可以使用一个别名导入,如示例 11-4 所示。让我们使用别名 f

示例 11-4. fast3.py
import fast as f
place = f.pick()
print("Let's go to", place)

从一个模块中仅导入你想要的内容

你可以导入整个模块,也可以只导入部分。你刚刚看到了后者:我们只想要 random 模块中的 choice() 函数。

像模块本身一样,你可以为每个导入的东西使用别名。

让我们再重新做几次 lunch.py。首先,从 fast 模块中以其原始名称导入 pick()(示例 11-5)。

示例 11-5. fast4.py
from fast import pick
place = pick()
print("Let's go to", place)

现在将其导入为 who_cares(示例 11-6)。

示例 11-6. fast5.py
from fast import pick as who_cares
place = who_cares()
print("Let's go to", place)

我们从单行代码、到多行函数、到独立程序、再到同一目录中的多个模块。如果你没有很多模块,同一目录也可以正常工作。

为了使 Python 应用程序能够更好地扩展,你可以将模块组织成称为的文件和模块层次结构。一个包只是包含 .py 文件的子目录。而且你可以进行多层次的组织,有目录在其中。

我们刚刚写了一个选择快餐的模块。让我们再添加一个类似的模块来提供人生建议。我们将在当前目录中创建一个名为 questions.py 的新主程序。现在在其中创建一个名为 choices 的子目录,并将两个模块放入其中——fast.pyadvice.py。每个模块都有一个返回字符串的函数。

主程序(questions.py)有额外的导入和行(示例 11-7)。

示例 11-7. questions.py
from sources import fast, advice

print("Let's go to", fast.pick())
print("Should we take out?", advice.give())

那个from sources让 Python 在当前目录下查找名为sources的目录。在sources内部,它查找fast.pyadvice.py文件。

第一个模块(choices/fast.py)与以前相同的代码,只是移动到了choices目录中(示例 11-8)。

示例 11-8. choices/fast.py
from random import choice

places = ["McDonalds", "KFC", "Burger King", "Taco Bell",
     "Wendys", "Arbys", "Pizza Hut"]

def pick():
    """Return random fast food place"""
    return choice(places)

第二个模块(choices/advice.py)是新的,但功能与快餐相似(示例 11-9)。

示例 11-9. choices/advice.py
from random import choice

answers = ["Yes!", "No!", "Reply hazy", "Sorry, what?"]

def give():
    """Return random advice"""
    return choice(answers)
注意

如果你的 Python 版本早于 3.3,那么在sources子目录中还需要一件事才能使其成为 Python 包:一个名为*__init__.py*的文件。这可以是一个空文件,但是在 3.3 之前的 Python 中,需要这样做才能将包含它的目录视为包。(这是另一个常见的 Python 面试问题。)

运行主程序questions.py(从当前目录,而不是sources中)来看看会发生什么:

$ python questions.py
Let's go to KFC
Should we take out? Yes!
$ python questions.py
Let's go to Wendys
Should we take out? Reply hazy
$ python questions.py
Let's go to McDonalds
Should we take out? Reply hazy

模块搜索路径

我刚才说过,Python 会在当前目录下查找子目录choices及其模块。实际上,它还会在其他地方查找,并且你可以控制这一过程。

早些时候,我们从标准库的random模块导入了函数choice()。这不在你的当前目录中,因此 Python 还需要在其他地方查找。

要查看 Python 解释器查找的所有位置,导入标准的sys模块并使用它的path列表。这是一个目录名称和 ZIP 存档文件列表,Python 按顺序搜索以找到要导入的模块。

你可以访问并修改这个列表。这是我 Mac 上 Python 3.7 的sys.path值:

>>> import sys
>>> for place in sys.path:
...     print(place)
...

/Library/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload

那个初始的空输出行是空字符串'',它表示当前目录。如果''sys.path的第一位,当你尝试导入某些东西时,Python 首先查找当前目录:import fast会寻找fast.py。这是 Python 的常规设置。此外,当我们创建名为sources的子目录并在其中放置 Python 文件时,它们可以通过import sourcesfrom sources import fast来导入。

将使用第一个匹配项。这意味着如果你定义了一个名为random的模块,并且它在标准库之前的搜索路径中,那么现在就无法访问标准库的random了。

你可以在代码中修改搜索路径。假设你希望 Python 在其他任何位置之前查找*/my/modules*目录:

>>> import sys
>>> sys.path.insert(0, "/my/modules")

相对和绝对导入

到目前为止,在我们的示例中,我们从以下位置导入了我们自己的模块:

  • 当前目录

  • 子目录choices

  • Python 标准库

这在你有与标准模块同名的本地模块时效果很好。你想要哪一个?

Python 支持绝对相对导入。到目前为止你看到的例子都是绝对导入。如果你键入import rougarou,对于搜索路径中的每个目录,Python 会查找名为rougarou.py(一个模块)或名为rougarou(一个包)的文件。

  • 如果rougarou.py与你调用问题的同一目录中,你可以用from . import rougarou来相对于你所在位置导入它。

  • 如果它位于你的上一级目录中:from .. import rougarou

  • 如果它位于名为creatures的同级目录下:from ..creatures import rougarou

...的符号借鉴于 Unix 对当前目录父目录的简写。

对于 Python 导入系统中可能遇到的问题的深入讨论,请参阅Python 导入系统的陷阱

命名空间包

你已经看到可以将 Python 模块打包为:

  • 一个单一的模块.py 文件)

  • (包含模块及可能其他包的目录)

你也可以通过命名空间包在多个目录中分割一个包。假设你想要一个名为critters的包,其中包含每种危险生物(真实或想象中,据说具有背景信息和防护提示)的 Python 模块。随着时间的推移,这可能会变得很庞大,你可能希望按地理位置细分。一种选择是在critters下添加位置子包,并将现有的*.py模块文件移到它们下面,但这会打破其他导入它们的模块。相反,我们可以向上*进行如下操作:

  • critters上创建新的位置目录

  • 在这些新父目录下创建表兄弟目录critters

  • 将现有模块移动到它们各自的目录中。

这需要一些说明。假设我们从这样的文件布局开始:

critters
 ⌞ rougarou.py
 ⌞ wendigo.py

这些模块的正常导入看起来像这样:

from critters import wendigo, rougarou

现在如果我们决定在美国的地点,文件和目录看起来会像这样:

north
 ⌞ critters
   ⌞ wendigo.py
south
 ⌞ critters
   ⌞ rougarou.py

如果northsouth都在你的模块搜索路径中,你可以像它们仍然共存于单目录包一样导入这些模块:

from critters import wendigo, rougarou

模块与对象

你应该把你的代码放入一个模块中,还是放入一个对象中?什么时候适合?

它们在许多方面看起来很相似。一个名为thing的对象或模块,具有称为stuff的内部数据值,让你可以像thing.stuff那样访问该值。stuff可能在创建模块或类时已经定义,也可能是后来分配的。

模块中的所有类、函数和全局变量对外部都是可用的。对象可以使用属性和“dunder”(__ …)命名来隐藏或控制对它们数据属性的访问。

这意味着你可以这样做:

>>> import math
>>> math.pi
3.141592653589793
>>> math.pi = 3.0
>>> math.pi
3.0

你刚刚搞砸了这台计算机上每个人的计算吗?是的!不,开玩笑的。² 这并未影响 Python 的 math 模块。你只是改变了你调用程序导入的 math 模块代码的副本中 pi 的值,并且所有关于你罪行的证据将在程序结束时消失。

你的程序导入的任何模块只有一个副本,即使你多次导入它。你可以用它保存对任何导入代码感兴趣的全局事物。这与类似,尽管你可以从中创建许多对象,但类也只有一个副本。

Python 标准库中的好东西

Python 的一个显著特点是其“即插即用”——一个包含许多有用任务的大型标准库模块。它们被保持分开,以避免膨胀核心语言。当你打算写一些 Python 代码时,经常值得先检查是否已经有标准模块实现了你想要的功能。令人惊讶的是,你会经常遇到标准库中的一些小宝石。Python 还为这些模块提供了权威的 文档,以及一个 教程。Doug Hellmann 的网站 Python Module of the Week 和书籍 The Python Standard Library by Example(Addison-Wesley Professional)也是非常有用的指南。

本书的即将到来的章节涵盖了许多特定于网络、系统、数据库等的标准模块。在本节中,我讨论一些具有通用用途的标准模块。

使用 setdefault()defaultdict() 处理缺失键

你已经看到尝试访问字典的不存在键会引发异常。使用字典的 get() 函数返回一个默认值可以避免异常。setdefault() 函数类似于 get(),但也会在键缺失时向字典中分配一个项目:

>>> periodic_table = {'Hydrogen': 1, 'Helium': 2}
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2}

如果键原先 在字典中,新值就会被使用:

>>> carbon = periodic_table.setdefault('Carbon', 12)
>>> carbon
12
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

如果我们尝试为 现有 键分配不同的默认值,将返回原始值且不会发生任何更改:

>>> helium = periodic_table.setdefault('Helium', 947)
>>> helium
2
>>> periodic_table
{'Hydrogen': 1, 'Helium': 2, 'Carbon': 12}

defaultdict() 类似,但在创建字典时就指定了任何新键的默认值。它的参数是一个函数。在这个例子中,我们传递了函数 int,它将被调用为 int() 并返回整数 0

>>> from collections import defaultdict
>>> periodic_table = defaultdict(int)

现在任何缺失的值将是整数 (int), 其值为 0:

>>> periodic_table['Hydrogen'] = 1
>>> periodic_table['Lead']
0
>>> periodic_table
defaultdict(<class 'int'>, {'Hydrogen': 1, 'Lead': 0})

defaultdict() 的参数是一个返回要分配给缺失键的值的函数。在下面的例子中,当需要时执行 no_idea() 来返回一个值:

>>> from collections import defaultdict
>>>
>>> def no_idea():
...     return 'Huh?'
...
>>> bestiary = defaultdict(no_idea)
>>> bestiary['A'] = 'Abominable Snowman'
>>> bestiary['B'] = 'Basilisk'
>>> bestiary['A']
'Abominable Snowman'
>>> bestiary['B']
'Basilisk'
>>> bestiary['C']
'Huh?'

你可以使用 int()list()dict() 函数来返回这些类型的默认空值:int() 返回 0list() 返回一个空列表 ([]),dict() 返回一个空字典 ({})。如果省略参数,新键的初始值将设置为 None

顺便说一下,您可以使用lambda在调用内部定义您的默认生成函数:

>>> bestiary = defaultdict(lambda: 'Huh?')
>>> bestiary['E']
'Huh?'

使用int是制作自己的计数器的一种方法:

>>> from collections import defaultdict
>>> food_counter = defaultdict(int)
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
...     food_counter[food] += 1
...
>>> for food, count in food_counter.items():
...     print(food, count)
...
eggs 1
spam 3

在上面的示例中,如果food_counter是一个普通的字典而不是defaultdict,每次尝试增加字典元素food_counter[food]时,Python 都会引发一个异常,因为它不会被初始化。我们需要做一些额外的工作,如下所示:

>>> dict_counter = {}
>>> for food in ['spam', 'spam', 'eggs', 'spam']:
...     if not food in dict_counter:
...         dict_counter[food] = 0
...     dict_counter[food] += 1
...
>>> for food, count in dict_counter.items():
...     print(food, count)
...
spam 3
eggs 1

使用 Counter()计算项数

说到计数器,标准库中有一个可以执行前面示例工作以及更多工作的计数器:

>>> from collections import Counter
>>> breakfast = ['spam', 'spam', 'eggs', 'spam']
>>> breakfast_counter = Counter(breakfast)
>>> breakfast_counter
Counter({'spam': 3, 'eggs': 1})

函数most_common()以降序返回所有元素,或者如果给定了计数,则仅返回前count个元素:

>>> breakfast_counter.most_common()
[('spam', 3), ('eggs', 1)]
>>> breakfast_counter.most_common(1)
[('spam', 3)]

您可以组合计数器。首先,让我们再次看看breakfast_counter中有什么:

>>> breakfast_counter
>>> Counter({'spam': 3, 'eggs': 1})

这一次,我们创建了一个名为lunch的新列表,以及一个名为lunch_counter的计数器:

>>> lunch = ['eggs', 'eggs', 'bacon']
>>> lunch_counter = Counter(lunch)
>>> lunch_counter
Counter({'eggs': 2, 'bacon': 1})

我们组合两个计数器的第一种方法是通过加法,使用+

>>> breakfast_counter + lunch_counter
Counter({'spam': 3, 'eggs': 3, 'bacon': 1})

正如您所预期的,您可以使用-从另一个计数器中减去一个计数器。早餐吃什么而午餐不吃呢?

>>> breakfast_counter - lunch_counter
Counter({'spam': 3})

好的,现在我们可以吃午餐了,但是我们早餐不能吃什么呢?

>>> lunch_counter - breakfast_counter
Counter({'bacon': 1, 'eggs': 1})

类似于第八章中的集合,您可以使用交集运算符&获取共同的项:

>>> breakfast_counter & lunch_counter
Counter({'eggs': 1})

交集选择了具有较低计数的共同元素('eggs')。这是有道理的:早餐只提供了一个鸡蛋,所以这是共同的计数。

最后,您可以使用并集运算符|获取所有项:

>>> breakfast_counter | lunch_counter
Counter({'spam': 3, 'eggs': 2, 'bacon': 1})

项目'eggs'再次是两者共同的。与加法不同,联合操作并未将它们的计数相加,而是选择计数较大的那个。

使用 OrderedDict()按键排序

这是使用 Python 2 解释器运行的示例:

>>> quotes = {
...     'Moe': 'A wise guy, huh?',
...     'Larry': 'Ow!',
...     'Curly': 'Nyuk nyuk!',
...     }
>>> for stooge in quotes:
...  print(stooge)
...
Larry
Curly
Moe
注意

从 Python 3.7 开始,字典会按照它们被添加的顺序保留键。OrderedDict对于早期版本非常有用,因为它们具有不可预测的顺序。本节中的示例仅在您使用的 Python 版本早于 3.7 时才相关。

OrderedDict()记住键添加的顺序,并从迭代器中以相同的顺序返回它们。尝试从一个(, )元组序列创建一个OrderedDict

>>> from collections import OrderedDict
>>> quotes = OrderedDict([
...     ('Moe', 'A wise guy, huh?'),
...     ('Larry', 'Ow!'),
...     ('Curly', 'Nyuk nyuk!'),
...     ])
>>>
>>> for stooge in quotes:
...     print(stooge)
...
Moe
Larry
Curly

栈+队列==deque

一个deque(发音为deck)是一个双端队列,具有栈和队列的特性。当你想要从序列的任一端添加或删除项时,它非常有用。在这里,我们从单词的两端向中间工作,以查看它是否是回文。函数popleft()从 deque 中删除最左边的项并返回它;pop()则删除最右边的项并返回它。它们一起从两端向中间工作。只要末尾字符匹配,它就会持续弹出,直到达到中间位置:

>>> def palindrome(word):
...     from collections import deque
...     dq = deque(word)
...     while len(dq) > 1:
...        if dq.popleft() != dq.pop():
...            return False
...     return True
...
...
>>> palindrome('a')
True
>>> palindrome('racecar')
True
>>> palindrome('')
True
>>> palindrome('radar')
True
>>> palindrome('halibut')
False

我将其用作双端队列的简单说明。如果你真的想要一个快速的回文检查器,只需将字符串与其反转比较就简单得多了。Python 没有字符串的 reverse() 函数,但它确实有一种通过切片来反转字符串的方法,如下例所示:

>>> def another_palindrome(word):
...     return word == word[::-1]
...
>>> another_palindrome('radar')
True
>>> another_palindrome('halibut')
False

用 itertools 遍历代码结构

itertools包含特殊用途的迭代器函数。每次在forin循环中调用时,它返回一个项目,并在调用之间记住其状态。

chain() 将其参数视为单个可迭代对象运行:

>>> import itertools
>>> for item in itertools.chain([1, 2], ['a', 'b']):
...     print(item)
...
1
2
a
b

cycle()是一个无限迭代器,循环遍历其参数:

>>> import itertools
>>> for item in itertools.cycle([1, 2]):
...     print(item)
...
1
2
1
2
.
.
.

等等。

accumulate() 计算累积值。默认情况下,它计算总和:

>>> import itertools
>>> for item in itertools.accumulate([1, 2, 3, 4]):
...     print(item)
...
1
3
6
10

您可以将一个函数作为accumulate()的第二个参数提供,它将被用于代替加法。该函数应该接受两个参数并返回一个单一的结果。这个例子计算一个累积乘积:

>>> import itertools
>>> def multiply(a, b):
...     return a * b
...
>>> for item in itertools.accumulate([1, 2, 3, 4], multiply):
...     print(item)
...
1
2
6
24

itertools 模块还有许多其他函数,尤其是一些组合和排列函数,在需要时可以节省时间。

使用pprint()进行漂亮打印

我们所有的例子都使用print()(或者在交互式解释器中仅使用变量名)来打印东西。有时,结果很难读取。我们需要一个漂亮打印机,比如pprint()

>>> from pprint import pprint
>>> quotes = OrderedDict([
...     ('Moe', 'A wise guy, huh?'),
...     ('Larry', 'Ow!'),
...     ('Curly', 'Nyuk nyuk!'),
...     ])
>>>

简单的print()只是将东西倒出来:

>>> print(quotes)
OrderedDict([('Moe', 'A wise guy, huh?'), ('Larry', 'Ow!'),
 ('Curly', 'Nyuk nyuk!')])

但是,pprint()试图对齐元素以提高可读性:

>>> pprint(quotes)
{'Moe': 'A wise guy, huh?',
 'Larry': 'Ow!',
 'Curly': 'Nyuk nyuk!'}

获取随机数

我们在本章的开头玩了random.choice()。它从给定的序列(列表、元组、字典、字符串)中返回一个值:

>>> from random import choice
>>> choice([23, 9, 46, 'bacon', 0x123abc])
1194684
>>> choice( ('a', 'one', 'and-a', 'two') )
'one'
>>> choice(range(100))
68
>>> choice('alphabet')
'l'

使用sample()函数一次获取多个值:

>>> from random import sample
>>> sample([23, 9, 46, 'bacon', 0x123abc], 3)
[1194684, 23, 9]
>>> sample(('a', 'one', 'and-a', 'two'), 2)
['two', 'and-a']
>>> sample(range(100), 4)
[54, 82, 10, 78]
>>> sample('alphabet', 7)
['l', 'e', 'a', 't', 'p', 'a', 'b']

要从任意范围获取一个随机整数,您可以使用choice()sample()range(),或者使用randint()randrange()

>>> from random import randint
>>> randint(38, 74)
71
>>> randint(38, 74)
60
>>> randint(38, 74)
61

randrange()range()一样,有起始(包含)和结束(不包含)整数的参数,还有一个可选的整数步长:

>>> from random import randrange
>>> randrange(38, 74)
65
>>> randrange(38, 74, 10)
68
>>> randrange(38, 74, 10)
48

最后,获取一个在 0.0 到 1.0 之间的随机实数(浮点数):

>>> from random import random
>>> random()
0.07193393312692198
>>> random()
0.7403243673826271
>>> random()
0.9716517846775018

更多电池:获取其他 Python 代码

有时,标准库没有您需要的功能,或者没有以正确的方式执行。有一个完整的开源、第三方 Python 软件世界。良好的资源包括以下内容:

你可以在activestate找到许多较小的代码示例。

本书几乎所有的 Python 代码都使用您计算机上的标准 Python 安装,其中包括所有内置函数和标准库。某些地方特别提到了requests在第一章中;更多细节请参见第十八章。附录 B 展示了如何安装第三方 Python 软件,以及许多其他开发细节。

即将发生的事情

下一章是一个实用章节,涵盖 Python 中许多数据操作的方面。您将遇到二进制bytesbytearray数据类型,在文本字符串中处理 Unicode 字符,并使用正则表达式搜索文本字符串。

要做的事情

11.1 创建一个名为zoo.py的文件。在其中,定义一个名为hours()的函数,打印字符串'Open 9-5 daily'。然后,使用交互解释器导入zoo模块并调用其hours()函数。

11.2 在交互解释器中,将zoo模块作为menagerie导入,并调用其hours()函数。

11.3 仍然在解释器中,直接从zoo中导入hours()函数并调用它。

11.4 将hours()函数作为info导入并调用它。

11.5 创建一个名为plain的字典,其键值对为'a': 1'b': 2'c': 3,然后打印它。

11.6 从上一个问题中列出的相同对创建一个名为fancyOrderedDict并打印它。它是否按照plain的顺序打印?

11.7 创建一个名为dict_of_listsdefaultdict,并传递list作为参数。用一次赋值操作将列表dict_of_lists['a']并附加值'something for a'。打印dict_of_lists['a']

¹ 至少,比它现在的阅读性少一点。

² 还是会?布娃哈哈。

第二部分:Python 实践篇

第十二章:整理和处理数据

如果你折磨数据足够久,自然会招认。

罗纳德·科斯

到目前为止,我们主要讨论了 Python 语言本身——其数据类型、代码结构、语法等等。本书的其余部分是关于将这些应用到现实世界问题的内容。

在本章中,你将学习许多实用的数据处理技术。有时,这被称为数据清洗,或者更商业化的ETL(提取/转换/加载)在数据库世界中。尽管编程书籍通常不会明确涵盖这个主题,程序员们花费了大量时间来将数据塑造成符合其目的的正确形式。

数据科学这一专业在过去几年变得非常流行。《哈佛商业评论》的一篇文章称数据科学家是“21 世纪最性感的职业”。如果这意味着需求量大且薪资丰厚,那就好,但也有足够的单调乏味。数据科学超越了数据库的 ETL 需求,通常涉及机器学习,以发掘人眼看不到的洞察力。

我将从基本的数据格式开始,然后介绍最有用的新数据科学工具。

数据格式大致分为两类:文本二进制。Python 的字符串用于文本数据,本章包含了我们迄今为止跳过的字符串信息:

  • Unicode字符

  • 正则表达式模式匹配。

然后,我们转向二进制数据,以及 Python 的另外两种内置类型:

  • 字节用于不可变的八位值

  • 字节数组用于可变的字节

文本字符串:Unicode

你在第五章中看到了 Python 字符串的基础知识。现在是深入了解 Unicode 的时候了。

Python 3 的字符串是 Unicode 字符序列,而不是字节数组。这是从 Python 2 最大的语言变化。

到目前为止,本书中的所有文本示例都是普通的 ASCII(美国标准信息交换码)。ASCII 是在六十年代定义的,在鲑鱼头发流行之前。当时的计算机大小如冰箱,稍微聪明一点。

计算机存储的基本单元是字节,它可以存储 256 个独特的值在它的八中。出于各种原因,ASCII 只使用了七位(128 个独特的值):26 个大写字母、26 个小写字母、10 个数字、一些标点符号、一些间隔字符和一些不可打印的控制码。

不幸的是,世界上的字母比 ASCII 提供的还要多。你可以在餐馆吃热狗,但在咖啡馆永远也买不到 Gewürztraminer¹。已经尝试过许多方法将更多的字母和符号塞入八位中,有时你会看到它们。其中只有一些包括:

  • Latin-1,或者ISO 8859-1

  • Windows 代码页1252

这些字符都使用了所有的八位,但即使如此也不够,特别是在需要非欧洲语言时。Unicode 是一个持续进行的国际标准,用于定义所有世界语言的字符,以及数学和其他领域的符号。还有表情符号!

Unicode 为每个字符提供了一个唯一的编号,无论是什么平台、什么程序、什么语言。

Unicode 联盟

Unicode 代码图表页面包含所有当前定义的字符集的链接及其图像。最新版本(12.0)定义了超过 137,000 个字符,每个字符都有唯一的名称和标识号码。Python 3.8 可以处理所有这些字符。这些字符被分为称为平面的八位集合。前 256 个平面是基本多语言平面。详细信息请参阅关于Unicode 平面的维基百科页面。

Python 3 Unicode 字符串

如果您知道字符的 Unicode ID 或名称,可以在 Python 字符串中使用它。以下是一些示例:

  • \u后跟个十六进制数字²指定 Unicode 的 256 个基本多语言平面中的一个字符。前两个数字是平面编号(00FF),后两个数字是平面内字符的索引。平面00是老旧的 ASCII,该平面内的字符位置与 ASCII 相同。

  • 对于高平面中的字符,我们需要更多的位数。Python 中这些字符的转义序列是\U后跟个十六进制字符;最左边的数字需要是0

  • 对于所有字符,\N{*`name`*} 允许您通过其标准名称指定它。Unicode 字符名称索引页面列出了这些名称。

Python 的 unicodedata 模块具有双向转换的功能:

  • lookup()—接受一个不区分大小写的名称,并返回一个 Unicode 字符。

  • name()—接受一个 Unicode 字符并返回其大写名称。

在下面的示例中,我们将编写一个测试函数,该函数接受一个 Python Unicode 字符,查找其名称,然后根据名称再次查找字符(应该与原始字符匹配):

>>> def unicode_test(value):
...     import unicodedata
...     name = unicodedata.name(value)
...     value2 = unicodedata.lookup(name)
...     print('value="%s", name="%s", value2="%s"' % (value, name, value2))
...

让我们尝试一些字符,首先是一个普通的 ASCII 字母:

>>> unicode_test('A')
value="A", name="LATIN CAPITAL LETTER A", value2="A"

ASCII 标点符号:

>>> unicode_test('$')
value="$", name="DOLLAR SIGN", value2="$"

Unicode 货币字符:

>>> unicode_test('\u00a2')
value="¢", name="CENT SIGN", value2="¢"

另一个 Unicode 货币字符:

>>> unicode_test('\u20ac')
value="€", name="EURO SIGN", value2="€"

你可能遇到的唯一问题是字体显示文本的限制。很少有字体包含所有 Unicode 字符的图像,可能会为缺失的字符显示一些占位符字符。例如,这是 SNOWMAN 的 Unicode 符号,类似于装饰符字体中的符号:

>>> unicode_test('\u2603')
value="☃", name="SNOWMAN", value2="☃"

假设我们想要在 Python 字符串中保存单词 café。一种方法是从文件或网站复制并粘贴它,然后希望它能正常工作:

>>> place = 'café'
>>> place
'café'

这有效是因为我从使用 UTF-8 编码的源复制和粘贴了文本。

如何指定最后的 é 字符?如果你查看 E 的字符索引,你会看到名称 E WITH ACUTE, LATIN SMALL LETTER 具有值 00E9。让我们用刚才玩过的 name()lookup() 函数来检查。首先给出获取名称的代码:

>>> unicodedata.name('\u00e9')
'LATIN SMALL LETTER E WITH ACUTE'

接下来,给出查找代码的名称:

>>> unicodedata.lookup('E WITH ACUTE, LATIN SMALL LETTER')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: "undefined character name 'E WITH ACUTE, LATIN SMALL LETTER'"
注意

Unicode 字符名称索引页上列出的名称已经重新格式化,使其在显示时可以很好地排序。要将它们转换为真实的 Unicode 名称(Python 使用的名称),去掉逗号并将逗号后面的部分移动到开头。因此,将 E WITH ACUTE, LATIN SMALL LETTER 改为 LATIN SMALL LETTER E WITH ACUTE

>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
'é'

现在我们可以通过代码或名称指定字符串 café

>>> place = 'caf\u00e9'
>>> place
'café'
>>> place = 'caf\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> place
'café'

在前面的代码片段中,我们直接在字符串中插入了 é,但我们也可以通过附加构建字符串:

>>> u_umlaut = '\N{LATIN SMALL LETTER U WITH DIAERESIS}'
>>> u_umlaut
'ü'
>>> drink = 'Gew' + u_umlaut + 'rztraminer'
>>> print('Now I can finally have my', drink, 'in a', place)
Now I can finally have my Gewürztraminer in a café

字符串 len() 函数计算 Unicode 字符 数量,而不是字节数:

>>> len('$')
1
>>> len('\U0001f47b')
1
注意

如果你知道 Unicode 的数值 ID,你可以使用标准的 ord()chr() 函数快速转换整数 ID 和单字符 Unicode 字符串:

>>> chr(233)
'é'
>>> chr(0xe9)
'é'
>>> chr(0x1fc6)
'ῆ'

UTF-8

在正常的字符串处理中,你不需要担心 Python 如何存储每个 Unicode 字符。

但是,当你与外界交换数据时,你需要一些东西:

  • 编码 字符字符串为字节的方法

  • 解码 字节到字符字符串的方法

如果 Unicode 中的字符少于 65,536 个,我们可以将每个 Unicode 字符 ID 塞入两个字节中。不幸的是,字符太多了。我们可以将每个 ID 编码为四个字节,但这将使常见文本字符串的内存和磁盘存储空间需求增加四倍。

Ken Thompson 和 Rob Pike,Unix 开发者熟悉的名字,设计了一夜之间在新泽西餐馆的餐垫上的 UTF-8 动态编码方案。它每个 Unicode 字符使用一到四个字节:

  • ASCII 占一个字节

  • 大多数拉丁衍生(但不包括西里尔语)语言需要两个字节

  • 基本多语言平面的其余部分需要三个字节

  • 其余部分包括一些亚洲语言和符号需要四个字节

UTF-8 是 Python、Linux 和 HTML 中的标准文本编码。它快速、全面且运行良好。如果你在代码中始终使用 UTF-8 编码,生活将比试图在各种编码之间跳转要容易得多。

注意

如果你从网页等其他源复制粘贴创建 Python 字符串,请确保源以 UTF-8 格式编码。经常看到将以 Latin-1 或 Windows 1252 编码的文本复制到 Python 字符串中,这将导致后来出现无效字节序列的异常。

编码

你可以将字符串 编码字节。字符串 encode() 函数的第一个参数是编码名称。选择包括 Table 12-1 中的那些。

表 12-1. 编码

编码名称描述
'ascii'七比特 ASCII 编码
'utf-8'八位变长编码,几乎总是你想要使用的
'latin-1'也称为 ISO 8859-1
'cp-1252'常见的 Windows 编码
'unicode-escape'Python Unicode 文本格式,\uxxxx\Uxxxxxxxx

你可以将任何东西编码为 UTF-8。让我们将 Unicode 字符串'\u2603'赋给名称snowman

>>> snowman = '\u2603'

snowman是一个 Python Unicode 字符串,只有一个字符,无论内部存储它需要多少字节:

>>> len(snowman)
1

接下来,让我们将这个 Unicode 字符编码为一个字节序列:

>>> ds = snowman.encode('utf-8')

正如我之前提到的,UTF-8 是一种变长编码。在这种情况下,它用三个字节来编码单个snowman Unicode 字符:

>>> len(ds)
3
>>> ds
b'\xe2\x98\x83'

现在,len()返回字节数(3),因为ds是一个bytes变量。

你可以使用除 UTF-8 之外的其他编码,但如果 Unicode 字符串不能被处理,你会得到错误。例如,如果你使用ascii编码,除非你的 Unicode 字符恰好是有效的 ASCII 字符,否则会失败:

>>> ds = snowman.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\u2603'
in position 0: ordinal not in range(128)

encode()函数接受第二个参数,帮助你避免编码异常。它的默认值,在前面的例子中你可以看到,是'strict';如果遇到非 ASCII 字符,它会引发一个UnicodeEncodeError。还有其他编码方式。使用'ignore'来丢弃任何无法编码的内容:

>>> snowman.encode('ascii', 'ignore')
b''

使用'replace'来用?替换未知字符:

>>> snowman.encode('ascii', 'replace')
b'?'

使用'backslashreplace'来生成一个 Python Unicode 字符字符串,比如unicode-escape

>>> snowman.encode('ascii', 'backslashreplace')
b'\\u2603'

如果你需要一个 Unicode 转义序列的可打印版本,你可以使用这个方法。

使用'xmlcharrefreplace'来生成 HTML 安全字符串:

>>> snowman.encode('ascii', 'xmlcharrefreplace')
b'&#9731;'

我在“HTML 实体”中提供了更多 HTML 转换的细节。

解码

我们将字节字符串解码为 Unicode 文本字符串。每当我们从外部来源(文件、数据库、网站、网络 API 等)获取文本时,它都会被编码为字节字符串。棘手的部分是知道实际使用了哪种编码方式,这样我们才能逆向操作并获取 Unicode 字符串。

问题在于字节字符串本身没有说明使用了哪种编码。我之前提到过从网站复制粘贴的危险。你可能访问过一些奇怪字符的网站,本应是普通的 ASCII 字符。

让我们创建一个名为place的 Unicode 字符串,其值为'café'

>>> place = 'caf\u00e9'
>>> place
'café'
>>> type(place)
<class 'str'>

用 UTF-8 格式编码,存入一个名为place_bytesbytes变量:

>>> place_bytes = place.encode('utf-8')
>>> place_bytes
b'caf\xc3\xa9'
>>> type(place_bytes)
<class 'bytes'>

注意place_bytes有五个字节。前三个与 ASCII 相同(UTF-8 的优势),最后两个编码了'é'。现在让我们将该字节字符串解码回 Unicode 字符串:

>>> place2 = place_bytes.decode('utf-8')
>>> place2
'café'

这个方法的原因是我们编码为 UTF-8 并解码为 UTF-8。如果我们告诉它从其他编码解码会怎样呢?

>>> place3 = place_bytes.decode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3:
ordinal not in range(128)

ASCII 解码器因为字节值0xc3在 ASCII 中是非法的而抛出了异常。有些 8 位字符集编码允许 128(十六进制80)到 255(十六进制FF)之间的值合法,但与 UTF-8 不同。

>>> place4 = place_bytes.decode('latin-1')
>>> place4
'café'
>>> place5 = place_bytes.decode('windows-1252')
>>> place5
'café'

唔。

这个故事的教训是:只要可能,请使用 UTF-8 编码。它适用,被到处支持,可以表示每个 Unicode 字符,并且快速解码和编码。

注意

尽管您可以指定任何 Unicode 字符,这并不意味着您的计算机将显示所有这些字符。这取决于您使用的字体,该字体可能对许多字符显示空白或填充图像。苹果为 Unicode 联盟创建了最后的应急字体,并在其自己的操作系统中使用它。这个Wikipedia 页面有更多细节。另一种包含从\u0000\uffff以及更多字符的字体是Unifont

HTML 实体

Python 3.4 增加了另一种转换 Unicode 的方法,但是使用 HTML 字符实体。³ 这可能比查找 Unicode 名称更容易,特别是在您在网络上工作时:

>>> import html
>>> html.unescape("&egrave;")
'è'

这种转换也适用于编号实体,十进制或十六进制:

>>> import html
>>> html.unescape("&#233;")
'é'
>>> html.unescape("&#xe9;")
'é'

您甚至可以将命名实体转换导入为字典并自行进行转换。删除字典键的初始'&'(您也可以删除最后的;,但似乎两种方式都可以工作):

>>> from html.entities import html5
>>> html5["egrave"]
'è'
>>> html5["egrave;"]
'è'

要从单个 Python Unicode 字符向 HTML 实体名称的另一方向转换,请首先使用ord()获取字符的十进制值:

>>> import html
>>> char = '\u00e9'
>>> dec_value = ord(char)
>>> html.entities.codepoint2name[dec_value]
'eacute'

对于超过一个字符的 Unicode 字符串,请使用这两步转换:

>>> place = 'caf\u00e9'
>>> byte_value = place.encode('ascii', 'xmlcharrefreplace')
>>> byte_value
b'caf&#233;'
>>> byte_value.decode()
'caf&#233;'

表达式place.encode('ascii', 'xmlcharrefreplace')返回 ASCII 字符,但是作为类型bytes(因为它是*编码的)。需要以下byte_value.decode()来将byte_value转换为 HTML 兼容字符串。

标准化

一些 Unicode 字符可以用多种 Unicode 编码表示。它们看起来一样,但由于具有不同的内部字节序列,它们不能进行比较。例如,在'café'中,急性重音'é'可以用多种方式制作单个字符'é'

>>> eacute1 = 'é'                              # UTF-8, pasted
>>> eacute2 = '\u00e9'                         # Unicode code point
>>> eacute3 = \                                # Unicode name
...     '\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> eacute4 = chr(233)                         # decimal byte value
>>> eacute5 = chr(0xe9)                        # hex byte value
>>> eacute1, eacute2, eacute3, eacute4, eacute5
('é', 'é', 'é', 'é', 'é')
>>> eacute1 == eacute2 == eacute3 == eacute4 == eacute5
True

尝试几个健全性检查:

>>> import unicodedata
>>> unicodedata.name(eacute1)
'LATIN SMALL LETTER E WITH ACUTE'
>>> ord(eacute1)             # as a decimal integer
233
>>> 0xe9                     # Unicode hex integer
233

现在让我们通过将一个普通的e与一个重音符号结合来制作一个带重音的e

>>> eacute_combined1 = "e\u0301"
>>> eacute_combined2 = "e\N{COMBINING ACUTE ACCENT}"
>>> eacute_combined3 = "e" + "\u0301"
>>> eacute_combined1, eacute_combined2, eacute_combined3
('é', 'é', 'é'))
>>> eacute_combined1 == eacute_combined2 == eacute_combined3
True
>>> len(eacute_combined1)
2

我们用两个字符构建了一个 Unicode 字符,它看起来与原始的'é'相同。但正如他们在芝麻街上所说的那样,其中一个与其他不同:

>>> eacute1 == eacute_combined1
False

如果您有来自不同来源的两个不同的 Unicode 文本字符串,一个使用eacute1,另一个使用eacute_combined1,它们看起来相同,但是神秘地不起作用。

您可以使用unicodedata模块中的normalize()函数修复这个问题:

>>> import unicodedata
>>> eacute_normalized = unicodedata.normalize('NFC', eacute_combined1)
>>> len(eacute_normalized)
1
>>> eacute_normalized == eacute1
True
>>> unicodedata.name(eacute_normalized)
'LATIN SMALL LETTER E WITH ACUTE'

'NFC'的意思是组合的正常形式

更多信息

如果您想了解更多关于 Unicode 的信息,这些链接特别有帮助:

文本字符串:正则表达式

第五章讨论了简单的字符串操作。掌握了这些基础知识后,你可能已经在命令行上使用了简单的“通配符”模式,比如 UNIX 命令 ls *.py,意思是列出所有以 .py 结尾的文件名

是时候通过使用正则表达式来探索更复杂的模式匹配了。这些功能在标准模块 re 中提供。你定义一个要匹配的字符串模式,以及要匹配的字符串。对于简单的匹配,用法如下:

>>> import re
>>> result = re.match('You', 'Young Frankenstein')

在这里,'You' 是我们要查找的模式'Young Frankenstein'(我们要搜索的字符串)。match() 检查是否以模式开头。

对于更复杂的匹配,你可以先编译你的模式以加快后续的匹配速度:

>>> import re
>>> youpattern = re.compile('You')

然后,你可以对编译后的模式执行匹配:

>>> import re
>>> result = youpattern.match('Young Frankenstein')
注意

因为这是一个常见的 Python 陷阱,我在这里再次强调:match() 只匹配从开头开始的模式。search() 则可以在任何位置匹配模式。

match() 不是比较模式和源的唯一方法。以下是你可以使用的几种其他方法(我们在下面的各节中讨论每一种方法):

  • search() 如果有的话返回第一个匹配项。

  • findall() 返回所有非重叠匹配项的列表(如果有的话)。

  • split()中匹配模式并返回字符串片段列表。

  • sub() 还需要另一个替换参数,并将中与模式匹配的所有部分更改为替换

注意

这里大多数正则表达式示例都使用 ASCII,但 Python 的字符串函数,包括正则表达式,可以处理任何 Python 字符串和任何 Unicode 字符。

使用 match() 找到确切的起始匹配

字符串 'Young Frankenstein' 是否以 'You' 开头?以下是带有注释的代码:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('You', source)  # match starts at the beginning of source
>>> if m:  # match returns an object; do this to see what matched
...     print(m.group())
...
You
>>> m = re.match('^You', source) # start anchor does the same
>>> if m:
...     print(m.group())
...
You

'Frank' 怎么样?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('Frank', source)
>>> if m:
...     print(m.group())
...

这次,match() 没有返回任何内容,因此 if 语句没有运行 print 语句。

正如我在 “新功能:我是海象” 中提到的,在 Python 3.8 中,你可以使用所谓的海象操作符简化这个例子:

>>> import re
>>> source = 'Young Frankenstein'
>>> if m := re.match('Frank', source):
...     print(m.group())
...

现在让我们使用 search() 来查看 'Frank' 是否出现在源字符串中:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.search('Frank', source)
>>> if m:
...      print(m.group())
...
Frank

让我们改变模式,再次尝试使用 match() 进行起始匹配:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('.*Frank', source)
>>> if m:  # match returns an object
...     print(m.group())
...
Young Frank

这里简要解释了我们新的 '.*Frank' 模式的工作原理:

  • . 表示任何单个字符

  • * 表示前一个内容的零个或多个.* 在一起表示任意数量的字符(甚至是零个)。

  • Frank 是我们想要匹配的短语,某个地方。

match() 返回与 .*Frank 匹配的字符串:'Young Frank'

使用 search() 找到第一个匹配项

你可以使用 search() 在字符串 'Young Frankenstein' 中找到模式 'Frank' 的任何位置,而不需要使用 .* 通配符:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.search('Frank', source)
>>> if m:  # search returns an object
...     print(m.group())
...
Frank

使用 findall() 查找所有匹配项

前面的例子只查找了一个匹配。但是如果你想知道字符串中单字母 'n' 的实例数量呢?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n', source)
>>> m   # findall returns a list
['n', 'n', 'n', 'n']
>>> print('Found', len(m), 'matches')
Found 4 matches

后面跟着任何字符的 'n' 是怎样的?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n.', source)
>>> m
['ng', 'nk', 'ns']

注意它没有匹配最后的 'n'。我们需要说 'n' 后面的字符是可选的,用 ?

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n.?', source)
>>> m
['ng', 'nk', 'ns', 'n']

使用 split() 在匹配处分割

下一个示例展示了如何通过模式而不是简单字符串(正常字符串 split() 方法会执行的方式)将字符串分割成列表:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.split('n', source)
>>> m    # split returns a list
['You', 'g Fra', 'ke', 'stei', '']

使用 sub() 替换匹配项

这类似于字符串 replace() 方法,但用于模式而不是字面字符串:

>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.sub('n', '?', source)
>>> m   # sub returns a string
'You?g Fra?ke?stei?'

模式:特殊字符

许多正则表达式的描述从如何定义它们的所有细节开始。我认为这是一个错误。正则表达式是一个不那么小的语言,有太多细节无法一次掌握。它们使用了很多标点符号,看起来像卡通人物在咒骂。

掌握了这些表达式 (match()search()findall()sub())之后,让我们深入了解如何构建它们的细节。你制作的模式适用于这些函数中的任何一个。

你已经了解了基础知识:

  • 匹配所有非特殊字符的文字

  • \n 外的任何单个字符用 .

  • 任意数量的前一个字符(包括零)用 *

  • 前一个字符的可选(零次或一次)用 ?

首先,特殊字符显示在 表 12-2 中。

表 12-2. 特殊字符

模式匹配项
\d单个数字
\D单个非数字字符
\w字母数字字符
\W非字母数字字符
\s空白字符
\S非空白字符
\b单词边界(在 \w\W 之间,顺序不限)
\B非单词边界

Python 的 string 模块有预定义的字符串常量,我们可以用它们进行测试。让我们使用 printable,其中包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格字符和标点符号:

>>> import string
>>> printable = string.printable
>>> len(printable)
100
>>> printable[0:50]
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
>>> printable[50:]
'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'

printable 中哪些字符是数字?

>>> re.findall('\d', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

哪些字符是数字、字母或下划线?

>>> re.findall('\w', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '_']

哪些是空格?

>>> re.findall('\s', printable)
[' ', '\t', '\n', '\r', '\x0b', '\x0c']

按顺序,这些是:普通空格、制表符、换行符、回车符、垂直制表符和换页符。

正则表达式不仅限于 ASCII。\d 将匹配任何 Unicode 所谓的数字,而不仅仅是 ASCII 字符 '0''9'。让我们从 FileFormat.info 添加两个非 ASCII 小写字母:

在这个测试中,我们将加入以下内容:

  • 三个 ASCII 字母

  • 三个标点符号不应该与 \w 匹配

  • Unicode 带抑音的拉丁小写字母 E (\u00ea)

  • Unicode 带抑音的拉丁小写字母 E (\u0115)

>>> x = 'abc' + '-/*' + '\u00ea' + '\u0115'

如预期,这个模式仅找到了字母:

>>> re.findall('\w', x)
['a', 'b', 'c', 'ê', 'ĕ']

模式:使用限定符

现在让我们制作“标点披萨”,使用正则表达式的主要模式限定符,这些限定符在 表 12-3 中介绍。

在表中,expr和其他斜体字表示任何有效的正则表达式。

表 12-3。模式说明符

模式匹配
abc字面abc
( expr )expr
expr1 &#124; expr2expr1expr2
.\n外的任意字符
^源字符串的开头
$源字符串的结尾
prev ?零或一个prev
prev *零或多个prev,尽可能多地匹配
prev *?零或多个prev,尽可能少地匹配
prev +一个或多个prev,尽可能多地匹配
prev +?一个或多个prev,尽可能少地匹配
prev { m }m个连续的prev
prev { m, n }mn个连续的prev,尽可能多地匹配
prev { m, n }?mn个连续的prev,尽可能少地匹配
[ abc ]abc(等同于a&#124;b&#124;c
[^ abc ]abc
prev (?= next )若紧随其后则prev
prev (?! next )若不紧随其后则prev
(?<= prev ) next若之前有prevnext
(?<! prev ) next若不紧随其前则next

当试图阅读这些示例时,你的眼睛可能永久地交叉了。首先,让我们定义我们的源字符串:

>>> source = '''I wish I may, I wish I might
... Have a dish of fish tonight.'''

现在我们应用不同的正则表达式模式字符串来尝试在source字符串中匹配某些内容。

注意

在以下示例中,我使用普通的引号字符串表示模式。在本节稍后,我将展示如何使用原始模式字符串(在初始引号前加上r)来避免 Python 正常字符串转义与正则表达式转义之间的冲突。因此,为了更安全,所有以下示例中的第一个参数实际上应该是原始字符串。

首先,在任意位置找到wish

>>> re.findall('wish', source)
['wish', 'wish']

接下来,在任意位置找到wishfish

>>> re.findall('wish|fish', source)
['wish', 'wish', 'fish']

查找开头的wish

>>> re.findall('^wish', source)
[]

查找开头的I wish

>>> re.findall('^I wish', source)
['I wish']

查找结尾的fish

>>> re.findall('fish$', source)
[]

最后,在结尾找到fish tonight.

>>> re.findall('fish tonight.$', source)
['fish tonight.']

字符^$称为锚点^锚定搜索到搜索字符串的开始,而$锚定到结尾。. $匹配行尾的任意字符,包括句号,所以它起作用了。为了更精确,我们应该转义句点以确实匹配它:

>>> re.findall('fish tonight\.$', source)
['fish tonight.']

从找到wf后面跟着ish开始:

>>> re.findall('[wf]ish', source)
['wish', 'wish', 'fish']

查找一个或多个wsh的连续序列:

>>> re.findall('[wsh]+', source)
['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']

找到以非字母数字字符跟随的ght

>>> re.findall('ght\W', source)
['ght\n', 'ght.']

找到以I开头的wish

>>> re.findall('I (?=wish)', source)
['I ', 'I ']

最后,wish之前有I

>>> re.findall('(?<=I) wish', source)
[' wish', ' wish']

我之前提到过,有几种情况下正则表达式模式规则与 Python 字符串规则相冲突。以下模式应匹配以fish开头的任何单词:

>>> re.findall('\bfish', source)
[]

为什么不这样做呢?如第五章中所述,Python 为字符串使用了一些特殊的转义字符。例如,\b 在字符串中表示退格,但在正则表达式的迷你语言中表示单词的开头。通过在定义正则表达式字符串时始终在其前加上 r 字符,可以避免意外使用转义字符,这样将禁用 Python 转义字符,如下所示:

>>> re.findall(r'\bfish', source)
['fish']

模式:指定 match() 输出

在使用 match()search() 时,所有匹配项都作为结果对象 mm.group() 返回。如果将模式括在括号中,则匹配将保存到自己的组中,并作为 m.groups() 的元组可用,如下所示:

>>> m = re.search(r'(. dish\b).*(\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')

如果使用此模式 (?P< name > expr ),它将匹配 expr,并将匹配保存在组 name 中:

>>> m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')
>>> m.group('DISH')
'a dish'
>>> m.group('FISH')
'fish'

二进制数据

文本数据可能会有挑战,但二进制数据可能会更加有趣。您需要了解诸如字节顺序(计算机处理器如何将数据分解为字节)和整数的符号位等概念。您可能需要深入了解二进制文件格式或网络数据包,以提取甚至更改数据。本节向您展示了在 Python 中进行二进制数据处理的基础知识。

bytes 和 bytearray

Python 3 引入了以下八位整数序列,可能值为 0 到 255,有两种类型:

  • bytes 不可变,类似于字节元组

  • bytearray 可变,类似于字节列表

以名为 blist 的列表开始,下一个示例创建了名为 the_bytesbytes 变量和名为 the_byte_arraybytearray 变量:

>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes
b'\x01\x02\x03\xff'
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
注意

bytes 值的表示以 b 和引号字符开头,后跟诸如 \x02 或 ASCII 字符的十六进制序列,并以匹配的引号字符结束。Python 将十六进制序列或 ASCII 字符转换为小整数,但显示也有效的 ASCII 编码的字节值作为 ASCII 字符:

>>> b'\x61'
b'a'
>>> b'\x01abc\xff'
b'\x01abc\xff'

下一个示例演示了您不能更改 bytes 变量:

>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes[1] = 127
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment

bytearray 变量温和且可变:

>>> blist = [1, 2, 3, 255]
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[1] = 127
>>> the_byte_array
bytearray(b'\x01\x7f\x03\xff')

这些操作会生成一个包含 0 到 255 的 256 元素结果:

>>> the_bytes = bytes(range(0, 256))
>>> the_byte_array = bytearray(range(0, 256))

在打印 bytesbytearray 数据时,Python 使用 \xxx 表示不可打印字节及其 ASCII 等效字符,对于可打印字符则显示其 ASCII 值(以及一些常见的转义字符,例如 \n 而非 \x0a)。以下是手动重新格式化以显示每行 16 个字节的 the_bytes 的打印表示:

>>> the_bytes
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f
\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
!"#$%&\'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~\x7f
\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f
\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f
\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf
\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf
\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef
\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'

这可能会令人困惑,因为它们是字节(小整数),而不是字符。

使用 struct 转换二进制数据

正如您所见,Python 有许多用于操作文本的工具。用于二进制数据的工具则较少。标准库包含了处理类似于 C 和 C++ 中结构体的数据的 struct 模块。使用 struct,您可以将二进制数据转换为 Python 数据结构,反之亦然。

我们来看看如何处理来自 PNG 文件的数据——一种常见的图像格式,通常与 GIF 和 JPEG 文件一起出现。我们将编写一个小程序,从一些 PNG 数据中提取图像的宽度和高度。

我们将使用奥莱利的商标——在 图 12-1 中展示的小眼睛猫熊。

https://github.com/OpenDocCN/ibooker-python-zh/raw/master/docs/intd-py-2e/img/inp2_1201.png

图 12-1. 奥莱利猫熊

此图像的 PNG 文件可在 维基百科 上找到。在 第 14 章 我才会介绍如何读取文件,因此我下载了这个文件,编写了一个小程序将其值作为字节打印出来,并只在一个名为 data 的 Python bytes 变量中键入了前 30 个字节的值,用于接下来的示例中。(PNG 格式规范指出宽度和高度存储在前 24 字节中,因此我们现在不需要更多。)

>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...     b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
...     width, height = struct.unpack('>LL', data[16:24])
...     print('Valid PNG, width', width, 'height', height)
... else:
...     print('Not a valid PNG')
...
Valid PNG, width 154 height 141

以下是此代码的功能:

  • data 包含来自 PNG 文件的前 30 个字节。为了适应页面,我用 + 和续行符(\)连接了两个字节字符串。

  • valid_png_header 包含标记有效 PNG 文件起始的八字节序列。

  • width 从第 16 至 19 字节提取,height 从第 20 至 23 字节提取。

>LL 是格式字符串,指示 unpack() 如何解释其输入字节序列并将其组装成 Python 数据类型。以下是详细说明:

  • > 表示整数以 大端 格式存储。

  • 每个 L 指定一个四字节无符号长整数。

你可以直接检查每个四字节值:

>>> data[16:20]
b'\x00\x00\x00\x9a'
>>> data[20:24]0x9a
b'\x00\x00\x00\x8d'

大端整数将最重要的字节放在左边。因为宽度和高度都小于 255,它们适合每个序列的最后一个字节。你可以验证这些十六进制值是否与预期的十进制值匹配:

>>> 0x9a
154
>>> 0x8d
141

当你想反向操作并将 Python 数据转换为字节时,请使用 struct pack() 函数:

>>> import struct
>>> struct.pack('>L', 154)
b'\x00\x00\x00\x9a'
>>> struct.pack('>L', 141)
b'\x00\x00\x00\x8d'

表 12-4 和 12-5 显示了 pack()unpack() 的格式说明符。

字节顺序说明符在格式字符串中优先。

表 12-4. 字节顺序说明符

格式说明符字节顺序
<小端
>大端

表 12-5. 格式说明符

格式说明符描述字节
x跳过一个字节1
b有符号字节1
B无符号字节1
h有符号短整数2
H无符号短整数2
i有符号整数4
I无符号整数4
l有符号长整数4
L无符号长整数4
Q无符号长长整数8
f单精度浮点数4
d双精度浮点数8
pcount 和字符1 + count
s字符count

类型说明符跟在字节顺序字符之后。任何说明符前都可以加一个数字,表示 count5B 等同于 BBBBB

你可以使用 count 前缀代替 >LL

>>> struct.unpack('>2L', data[16:24])
(154, 141)

我们使用切片 data[16:24] 直接抓取感兴趣的字节。我们也可以使用 x 标识符来跳过不感兴趣的部分:

>>> struct.unpack('>16x2L6x', data)
(154, 141)

这意味着:

  • 使用大端整数格式 (>)

  • 跳过 16 个字节 (16x)

  • 读取八个字节——两个无符号长整数 (2L)

  • 跳过最后六个字节 (6x)

其他二进制数据工具

一些第三方开源软件包提供了以下更具声明性的方法来定义和提取二进制数据:

附录 B 中详细介绍了如何下载和安装外部包,例如这些。在下一个示例中,您需要安装 construct。这是您需要做的全部工作:

$ pip install construct

以下是如何通过使用 construct 从我们的 data 字节串中提取 PNG 尺寸的方法:

>>> from construct import Struct, Magic, UBInt32, Const, String
>>> # adapted from code at https://github.com/construct
>>> fmt = Struct('png',
...     Magic(b'\x89PNG\r\n\x1a\n'),
...     UBInt32('length'),
...     Const(String('type', 4), b'IHDR'),
...     UBInt32('width'),
...     UBInt32('height')
...     )
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
...     b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> result = fmt.parse(data)
>>> print(result)
Container:
 length = 13
 type = b'IHDR'
 width = 154
 height = 141
>>> print(result.width, result.height)
154, 141

使用 binascii() 转换字节/字符串

标准的 binascii 模块具有将二进制数据与各种字符串表示形式(十六进制(基数 16)、Base64、uuencoded 等)之间转换的函数。例如,在下一段代码中,让我们将那八字节的 PNG 头部打印为一系列十六进制值,而不是 Python 用来显示 bytes 变量的混合 ASCII 和 \x xx 转义的方式:

>>> import binascii
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> print(binascii.hexlify(valid_png_header))
b'89504e470d0a1a0a'

嘿,这个东西也可以反向操作:

>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))
b'\x89PNG\r\n\x1a\n'

位运算符

Python 提供了类似 C 语言的位级整数操作符。表 12-6 总结了这些操作符,并包括对整数变量 x(十进制 5,二进制 0b0101)和 y(十进制 1,二进制 0b0001)的示例。

表 12-6. 位级整数运算符

操作符描述示例十进制结果二进制结果
&x & y10b0001
&#124;x &#124; y50b0101
^异或x ^ y40b0100
~反转位~x-6二进制表示取决于整数大小
<<左移x << 1100b1010
>>右移x >> 120b0010

这些操作符的工作方式类似于 第 8 章 中的集合操作符。& 操作符返回两个参数中相同的位,| 返回两个参数中设置的位。^ 操作符返回一个参数中的位,而不是两者都有的位。~ 操作符反转其单个参数中的所有位;这也反转了符号,因为整数的最高位在 二进制补码 算术中表示其符号(1 = 负数),这种算法在所有现代计算机中使用。<<>> 操作符只是将位向左或向右移动。向左移动一位与乘以二相同,向右移动相当于除以二。

一个珠宝类比

Unicode 字符串就像魅力手链,而字节则像串珠。

即将到来

接下来是另一个实用章节:如何处理日期和时间。

待办事项

12.1 创建一个名为mystery的 Unicode 字符串,并将其赋值为'\U0001f984'。打印mystery及其 Unicode 名称。

12.2 使用 UTF-8 对mystery进行编码,并将结果存入名为pop_bytesbytes变量中。打印pop_bytes

12.3 使用 UTF-8 将pop_bytes解码为字符串变量pop_string。打印pop_stringpop_string等于mystery吗?

12.4 当您处理文本时,正则表达式非常方便。我们将以多种方式应用它们到我们特色的文本样本中。这是一首名为“Ode on the Mammoth Cheese”的诗,由詹姆斯·麦金泰尔于 1866 年写作,致敬于一个重七千磅的奶酪,在安大略省制作并发送国际巡回展。如果您不想全部输入,请使用您喜爱的搜索引擎并将单词剪切并粘贴到 Python 程序中,或者直接从Project Gutenberg获取。将文本字符串命名为mammoth

例子 12-1. mammoth.txt
We have seen thee, queen of cheese,
Lying quietly at your ease,
Gently fanned by evening breeze,
Thy fair form no flies dare seize.

All gaily dressed soon you'll go
To the great Provincial show,
To be admired by many a beau
In the city of Toronto.

Cows numerous as a swarm of bees,
Or as the leaves upon the trees,
It did require to make thee please,
And stand unrivalled, queen of cheese.

May you not receive a scar as
We have heard that Mr. Harris
Intends to send you off as far as
The great world's show at Paris.

Of the youth beware of these,
For some of them might rudely squeeze
And bite your cheek, then songs or glees
We could not sing, oh! queen of cheese.

We'rt thou suspended from balloon,
You'd cast a shade even at noon,
Folks would think it was the moon
About to fall and crush them soon.

12.5 导入re模块以使用 Python 的正则表达式函数。使用re.findall()打印所有以c开头的单词。

12.6 找出所有以c开头的四字单词。

12.7 找出所有以r结尾的单词。

12.8 找出所有包含恰好三个连续元音字母的单词。

12.9 使用unhexlify将这个十六进制字符串(从两个字符串组合成一个以适应页面)转换为名为gifbytes变量:

'47494638396101000100800000000000ffffff21f9' +
'0401000000002c000000000100010000020144003b'

12.10 gif中的字节定义了一个像素的透明 GIF 文件,这是最常见的图形文件格式之一。合法的 GIF 以 ASCII 字符GIF89a开头。gif是否符合这个规范?

12.11 GIF 的像素宽度是从字节偏移量 6 开始的 16 位小端整数,高度也是相同大小,从偏移量 8 开始。提取并打印这些值以供gif使用。它们都是1吗?

¹ 这种酒在德国有一个分音符号,但在去法国的途中在阿尔萨斯地区失去了它。

² 基数 16,由字符0-9A-F指定。

³ 参见 HTML5 命名字符引用图表

第十三章:日历和钟表

“一!”在钟楼塔上的钟敲响,

前不久不过六十分钟前

十二点的钟声响起。

Frederick B. Needham,《钟的轮回》

我有日历但我从未准时过。

玛丽莲·梦露

程序员花费了令人惊讶的精力处理日期和时间。让我们谈谈他们遇到的一些问题,然后介绍一些最佳实践和技巧,使情况稍微不那么混乱。

日期可以用多种方式表示——实际上太多了。即使在使用罗马日历的英语中,你也会看到简单日期的许多变体:

  • July 21 1987

  • 21 Jul 1987

  • 21/7/1987

  • 7/21/1987

在其他问题中,日期表示可能会产生歧义。在前面的示例中,很容易确定 7 是月份,21 是月中的日期,因为月份不会达到 21 日。但是 1/6/2012 是指 1 月 6 日还是 6 月 1 日呢?

月份名称在罗马日历中在不同的语言中会有所变化。甚至年份和月份在其他文化中的定义也可能有所不同。

时间也有它们自己的烦恼来源,特别是由于时区和夏令时。如果你看一下时区地图,这些区域遵循的是政治和历史边界,而不是每 15 度(360 度 / 24)经度上的清晰分界线。并且各国在一年中开始和结束夏令时的日期也不同。南半球国家在北半球朋友结束夏令时时,他们自己的时钟也在前进,反之亦然。

Python 的标准库包括许多日期和时间模块,包括:datetimetimecalendardateutil 等。它们有些重叠,并且有些令人困惑。

闰年

闰年是时间的一个特殊问题。你可能知道每四年是一个闰年(夏季奥运会和美国总统选举)。你还知道每 100 年不是闰年,但每 400 年是吗?下面是测试各年份是否为闰年的代码:

>>> import calendar
>>> calendar.isleap(1900)
False
>>> calendar.isleap(1996)
True
>>> calendar.isleap(1999)
False
>>> calendar.isleap(2000)
True
>>> calendar.isleap(2002)
False
>>> calendar.isleap(2004)
True

对于好奇的人:

  • 一年有 365.242196 天(绕太阳一周后,地球大约从其轴上旋转四分之一)

  • 每四年增加一天。现在平均一年有 365.242196 - 0.25 = 364.992196 天

  • 每 100 年减少一天。现在平均一年有 364.992196 + 0.01 = 365.002196 天

  • 每 400 年增加一天。现在平均一年有 365.002196 - 0.0025 = 364.999696 天

暂且这样吧!我们不会谈论闰秒

datetime 模块

标准的 datetime 模块处理(这应该不会让你感到惊讶)日期和时间。它定义了四个主要的对象类,每个类都有许多方法:

  • date 用于年、月和日

  • time 用于小时、分钟、秒和小数

  • datetime 用于日期和时间的组合

  • timedelta 用于日期和/或时间间隔

你可以通过指定年、月和日来创建一个 date 对象。这些值随后可作为属性使用:

>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> halloween
datetime.date(2019, 10, 31)
>>> halloween.day
31
>>> halloween.month
10
>>> halloween.year
2019

您可以使用其 isoformat() 方法打印一个 date

>>> halloween.isoformat()
'2019-10-31'

iso 指的是 ISO 8601,这是一个国际标准,用于表示日期和时间。它从最一般的(年)到最具体的(日)进行排序。因此,它也正确排序:按年、月、日排序。我通常选择这种格式来表示程序中的日期,并用于按日期保存数据的文件名。下一节描述了更复杂的 strptime()strftime() 方法,用于解析和格式化日期。

此示例使用 today() 方法生成今天的日期:

>>> from datetime import date
>>> now = date.today()
>>> now
datetime.date(2019, 4, 5)

这个示例利用了一个 timedelta 对象,将一些时间间隔添加到一个 date 中:

>>> from datetime import timedelta
>>> one_day = timedelta(days=1)
>>> tomorrow = now + one_day
>>> tomorrow
datetime.date(2019, 4, 6)
>>> now + 17*one_day
datetime.date(2019, 4, 22)
>>> yesterday = now - one_day
>>> yesterday
datetime.date(2019, 4, 4)

date 的范围是从 date.min(年=1,月=1,日=1)到 date.max(年=9999,月=12,日=31)。因此,您不能将其用于历史或天文计算。

datetime 模块的 time 对象用于表示一天中的时间:

>>> from datetime import time
>>> noon = time(12, 0, 0)
>>> noon
datetime.time(12, 0)
>>> noon.hour
12
>>> noon.minute
0
>>> noon.second
0
>>> noon.microsecond
0

参数从最大的时间单位(小时)到最小的(微秒)进行。如果您没有提供所有参数,time 将假定其余的都是零。顺便说一句,仅因为您可以存储和检索微秒,并不意味着您可以精确到微秒从计算机检索时间。次秒测量的准确性取决于硬件和操作系统中的许多因素。

datetime 对象包含日期和时间。您可以直接创建一个,例如接下来的一个,用于 2019 年 1 月 2 日凌晨 3 点 04 分,加上 5 秒和 6 微秒:

>>> from datetime import datetime
>>> some_day = datetime(2019, 1, 2, 3, 4, 5, 6)
>>> some_day
datetime.datetime(2019, 1, 2, 3, 4, 5, 6)

datetime 对象还有一个 isoformat() 方法:

>>> some_day.isoformat()
'2019-01-02T03:04:05.000006'

那个中间的 T 分隔了日期和时间部分。

datetime 有一个 now() 方法返回当前日期和时间:

>>> from datetime import datetime
>>> now = datetime.now()
>>> now
datetime.datetime(2019, 4, 5, 19, 53, 7, 580562)
>>> now.year
2019
>>> now.month
4
>>> now.day
5
>>> now.hour
19
>>> now.minute
53
>>> now.second
7
>>> now.microsecond
580562

您可以将 date 对象和 time 对象组合成 datetime

>>> from datetime import datetime, time, date
>>> noon = time(12)
>>> this_day = date.today()
>>> noon_today = datetime.combine(this_day, noon)
>>> noon_today
datetime.datetime(2019, 4, 5, 12, 0)

您可以使用 date()time() 方法从 datetime 中提取 datetime

>>> noon_today.date()
datetime.date(2019, 4, 5)
>>> noon_today.time()
datetime.time(12, 0)

使用时间模块

令人困惑的是,Python 有一个带有 time 对象的 datetime 模块,以及一个单独的 time 模块。此外,time 模块有一个名为——等待它——time() 的函数。

表示绝对时间的一种方法是计算自某个起始点以来的秒数。Unix 时间 使用自 1970 年 1 月 1 日午夜以来的秒数。这个值通常称为 时代,通常是在系统之间交换日期和时间的最简单方法。

time 模块的 time() 函数返回当前时间的时代值:

>>> import time
>>> now = time.time()
>>> now
1554512132.778233

自 1970 年元旦以来已经过去了超过十亿秒。时间都去哪了?

您可以通过使用 ctime() 将时代值转换为字符串:

>>> time.ctime(now)
'Fri Apr  5 19:55:32 2019'

在下一节中,您将看到如何生成更吸引人的日期和时间格式。

epoch 值是与不同系统交换日期和时间的有用的最小公分母,如 JavaScript。然而,有时候你需要实际的天数、小时等,time 提供了 struct_time 对象。localtime() 提供系统时区的时间,而 gmtime() 提供 UTC 的时间:

>>> time.localtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=5, tm_hour=19,
tm_min=55, tm_sec=32, tm_wday=4, tm_yday=95, tm_isdst=1)
>>> time.gmtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=6, tm_hour=0,
tm_min=55, tm_sec=32, tm_wday=5, tm_yday=96, tm_isdst=0)

我的 19:55(中部时区,夏令时)在 UTC 的下一天的 00:55(以前称为 格林威治时间Zulu 时间)。如果省略 localtime()gmtime() 的参数,它们将使用当前时间。

struct_time 中的一些 tm_... 值可能有些模糊,因此请查看 表 13-1 获取更多详细信息。

表格 13-1. struct_time 的值

索引名称意义
0tm_year年份00009999
1tm_mon月份112
2tm_mday月份的某一天131
3tm_hour小时023
4tm_min分钟059
5tm_sec061
6tm_wday星期几0(周一)到 6(周日)
7tm_yday年内天数1366
8tm_isdst夏令时?0 = 否,1 = 是,-1 = 不明

如果你不想输入所有 tm_... 的名称,struct_time 也像一个命名元组一样(见 “命名元组”),所以你可以使用前面表格中的索引:

>>> import time
>>> now = time.localtime()
>>> now
time.struct_time(tm_year=2019, tm_mon=6, tm_mday=23, tm_hour=12,
tm_min=12, tm_sec=24, tm_wday=6, tm_yday=174, tm_isdst=1)
>>> now[0]
2019
print(list(now[x] for x in range(9)))
[2019, 6, 23, 12, 12, 24, 6, 174, 1]

mktime() 则反过来,将 struct_time 对象转换为 epoch 秒数:

>>> tm = time.localtime(now)
>>> time.mktime(tm)
1554512132.0

这与我们之前的 now() 的 epoch 值不完全匹配,因为 struct_time 对象仅保留到秒。

注意

一些建议:在可能的情况下,使用协调世界时 (UTC) 而不是时区。UTC 是一个绝对时间,独立于时区。如果你有一个服务器,将其时间设置为 UTC;不要使用本地时间。

更多建议:尽量避免使用夏令时。如果可以避免使用夏令时,一个时期的一个小时会消失(“提前”),而在另一个时期会出现两次(“倒退”)。出于某种原因,许多组织在计算机系统中使用本地时间和夏令时,但每年两次都会被那个神秘的小时弄得迷惑不解。

读写日期和时间

isoformat() 并不是写入日期和时间的唯一方式。你已经在 time 模块中看到了 ctime() 函数,你可以用它来将 epoch 转换为字符串:

>>> import time
>>> now = time.time()
>>> time.ctime(now)
'Fri Apr  5 19:58:23 2019'

你还可以通过使用 strftime() 将日期和时间转换为字符串。这作为 datetimedatetime 对象的一个方法提供,也作为 time 模块的一个函数提供。strftime() 使用格式字符串来指定输出,你可以在 表 13-2 中看到。

表格 13-2. strftime() 的输出格式说明符

格式字符串日期/时间单元范围
%Y年份1900-…
%m月份01-12
%B月份名称January, …
%b月份缩写Jan, …
%d日期01-31
%A星期名称Sunday, …
a缩写星期Sun, …
%H小时(24 小时制)00-23
%I小时(12 小时制)01-12
%p上午/下午AM, PM
%M分钟00-59
%S00-59

数字在左侧补零。

这是由time模块提供的strftime()函数。它将struct_time对象转换为字符串。我们首先定义格式字符串fmt,稍后再次使用它:

>>> import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> t = time.localtime()
>>> t
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=13, tm_hour=15,
tm_min=23, tm_sec=46, tm_wday=2, tm_yday=72, tm_isdst=1)
>>> time.strftime(fmt, t)
"It's Wednesday, March 13, 2019, local time 03:23:46PM"

如果我们尝试使用date对象,只有日期部分会生效,时间默认为午夜:

>>> from datetime import date
>>> some_day = date(2019, 7, 4)
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> some_day.strftime(fmt)
"It's Thursday, July 04, 2019, local time 12:00:00AM"

对于time对象,只有时间部分会被转换:

>>> from datetime import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> some_time = time(10, 35)
>>> some_time.strftime(fmt)
"It's Monday, January 01, 1900, local time 10:35:00AM"

您不会想使用time对象的日部分,因为它们没有意义。

要反向转换并将字符串转换为日期或时间,请使用具有相同格式字符串的strptime()。没有正则表达式模式匹配;字符串的非格式部分(没有%)需要完全匹配。让我们指定一个与--匹配的格式,例如2019-01-29。如果要解析的日期字符串中有空格而不是破折号会发生什么?

>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019 01 29", fmt)
Traceback (most recent call last):
 File "<stdin>",
 line 1, in <module>
 File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_strptime.py",
 line 571, in _strptime_time
 tt = _strptime(data_string, format)[0]
 File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_strptime.py",
 line 359, in _strptime(data_string, format))
ValueError: time data '2019 01 29' does not match format '%Y-%m-%d

如果我们给strptime()输入一些破折号,它会开心吗?

>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019-01-29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=1, tm_yday=29, tm_isdst=-1)

或者修复fmt字符串以匹配日期字符串:

>>> import time
>>> fmt = "%Y %m %d"
>>> time.strptime("2019 01 29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=1, tm_yday=29, tm_isdst=-1)

即使字符串似乎与其格式匹配,如果值超出范围,将引发异常(文件名因空间而被截断):

>>> time.strptime("2019-13-29", fmt)
Traceback (most recent call last):
 File "<stdin>",
 line 1, in <module>
 File ".../3.7/lib/python3.7/_strptime.py",
 line 571, in _strptime_time
 tt = _strptime(data_string, format)[0]
 File ".../3.7/lib/python3.7/_strptime.py",
 line 359, in _strptime(data_string, format))
ValueError: time data '2019-13-29' does not match format '%Y-%m-%d

名称特定于您的locale——操作系统的国际化设置。如果您需要打印不同的月份和日期名称,请通过使用setlocale()来更改您的 locale;它的第一个参数是用于日期和时间的locale.LC_TIME,第二个参数是一个字符串,结合语言和国家缩写。让我们邀请一些国际朋友参加万圣节聚会。我们将用美国英语、法语、德语、西班牙语和冰岛语(冰岛人真的有精灵)打印月份、日期和星期几:

>>> import locale
>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> for lang_country in ['en_us', 'fr_fr', 'de_de', 'es_es', 'is_is',]:
...     locale.setlocale(locale.LC_TIME, lang_country)
...     halloween.strftime('%A, %B %d')
...
'en_us'
'Thursday, October 31'
'fr_fr'
'Jeudi, octobre 31'
'de_de'
'Donnerstag, Oktober 31'
'es_es'
'jueves, octubre 31'
'is_is'
'fimmtudagur, október 31'
>>>

您从哪里找到这些lang_country的魔法值?这有点奇怪,但您可以尝试这样做以获取所有这些值(有几百个):

>>> import locale
>>> names = locale.locale_alias.keys()

names中,让我们获取看起来可以与setlocale()一起使用的区域设置名称,例如我们在前面示例中使用的那些——两个字符的语言代码,后面跟着下划线和两个字符的国家代码

>>> good_names = [name for name in names if \
len(name) == 5 and name[2] == '_']

前五个长什么样?

>>> good_names[:5]
['sr_cs', 'de_at', 'nl_nl', 'es_ni', 'sp_yu']

因此,如果您想要所有的德语语言区域设置,请尝试这样做:

>>> de = [name for name in good_names if name.startswith('de')]
>>> de
['de_at', 'de_de', 'de_ch', 'de_lu', 'de_be']
注意

如果运行set_locale()并出现错误

locale.Error: unsupported locale setting

该 locale 不受您的操作系统支持。您需要弄清楚您的操作系统需要添加什么。即使 Python 告诉您(使用locale.locale_alias.keys())这是一个好的 locale,我在使用cy_gb(威尔士语,英国)locale 时在 macOS 上遇到过此错误,即使它之前接受了is_is(冰岛语)的情况也是如此。

所有的转换

图 13-1(来自 Python wiki)总结了所有标准 Python 时间转换。

https://github.com/OpenDocCN/ibooker-python-zh/raw/master/docs/intd-py-2e/img/inp2_1301.png

图 13-1. 日期和时间转换

替代模块

如果你觉得标准库模块混乱,或者缺少你想要的特定转换,那么有许多第三方替代方案。以下是其中几个:

arrow

将许多日期和时间函数与简单的 API 结合起来。

dateutil

几乎可以解析任何日期格式,并且良好处理相对日期和时间。

iso8601

填补了 ISO8601 格式在标准库中的空白。

fleming

许多时区功能。

maya

直观的日期、时间和间隔接口。

dateinfer

从日期/时间字符串中猜测正确的格式字符串。

即将到来

文件和目录也需要关爱。

要做的事情

13.1 将当前日期作为字符串写入文本文件 today.txt

13.2 将文本文件 today.txt 读入字符串 today_string 中。

13.3 解析来自 today_string 的日期。

13.4 创建你的生日的日期对象。

13.5 你的生日是星期几?

13.6 你将(或者已经)十千天岁时是哪天?

¹ 大约是 Unix 诞生的起点,忽略那些烦人的闰秒。

为了在Windows安装ADB工具,你可以按照以下步骤进行操作: 1. 首先,下载ADB工具包并解压缩到你自定义的安装目录。你可以选择将其解压缩到任何你喜欢的位置。 2. 打开运行窗口,可以通过按下Win+R键来快速打开。在运行窗口中输入"sysdm.cpl"并按下回车键。 3. 在系统属性窗口中,选择"高级"选项卡,然后点击"环境变量"按钮。 4. 在环境变量窗口中,选择"系统变量"部分,并找到名为"Path"的变量。点击"编辑"按钮。 5. 在编辑环境变量窗口中,点击"新建"按钮,并将ADB工具的安装路径添加到新建的路径中。确保路径正确无误后,点击"确定"按钮。 6. 返回到桌面,打开命令提示符窗口。你可以通过按下Win+R键,然后输入"cmd"并按下回车键来快速打开命令提示符窗口。 7. 在命令提示符窗口中,输入"adb version"命令来验证ADB工具是否成功安装。如果显示版本信息,则表示安装成功。 这样,你就成功在Windows安装ADB工具。你可以使用ADB工具来执行各种操作,如枚举设备、进入/退出ADB终端、文件传输、运行命令、查看系统日志等。具体的操作方法可以参考ADB工具的官方文档或其他相关教程。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* [windows环境安装adb驱动](https://blog.csdn.net/zx54633089/article/details/128533343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Windows安装使用ADB简单易懂教程](https://blog.csdn.net/m0_37777700/article/details/129836351)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值