Effective Python -- 第 6 章 内置模块(下)

第 6 章 内置模块(下)

第 45 条:应该用 datetime 模块来处理本地时间,而不是用 time 模块-

协调世界时(Coordinated Universal Time,UTC)是一种标准的时间表述方式,它与时区无关。有些计算机,用某一时刻与 UNIX 时间原点之间相差的秒数,来表示那个时刻所对应的时间,对于这些计算机来说,UTC 是一种非常好的计时方式。但是对于普通人来说,使用 UTC 来描述时间,却不太合适,因为我们通常都是根据当前所在的地点来描述时间的。我们会说“正午”( noon)或“早晨 8 点”(8 am),而不会说“离 UTC 时间 15 点还差 7 个小时”(UTC 15:00 minus 7 hours)。如果要在程序里面处理时间,那么可能需要寻找一种方式,以便在 UTC 与当地时间之间进行转换,并以用户容易理解的说法,将其描述出来。

Python 提供了两种时间转换方式。旧的方式,是使用内置的 time 模块,这是一种极易出错的方式。而新的方式,则是采用内置的 datetime 模块。该模块的效果非常好,它得益于 Python 开发者社区所构建的 pytz 软件包。

为了详细了解 datetime 模块的优点和 time 模块的缺点,必须熟悉这两个模块的用法。

1.time 模块

在内置的 time 模块中,有个名叫 localtime 的函数,它可以把 UNIX 时间戳(UNIXtimestamp,也就是某个 UTC 时刻距离 UNIX 计时原点的秒数)转换为与宿主计算机的时区相符的当地时间(例如电脑的时区是太平洋夏令时,Pacific Daylight Time,PDT)。

from time import localtime, strftime
now = 1407694710
local_tuple = localtime(now)
time_format = '%Y-%m-%d %H:%M:%S'
time_str = strftime(time_format, local_tuple)
print(time_str)
>>>
2014-08-10 11:18:30

程序通常还需要做反向处理,也就是说,要把用户输入的本地时间,转换为 UTC 时间。可以用 strptime 函数来解析包含时间信息的字符串,然后调用 mktime 函数,将本地时间转换为 UNIX 时间截。

from time import mktime, strptime
time_tuple = strptime(time_str, time_format)
utc_now = mktime(time_tuple)
print(utc_now)
>>>
1407694710.0

如何把某个时区的当地时间转换为另一个时区的当地时间呢?例如,坐飞机从旧金山到达纽约之后,想知道现在是旧金山的几点钟。

想通过直接操作 time、localtime 和 strptime 函数的返回值来进行时区转换,不是一个好办法。由于时区会随着当地法规而变化,所以手工管理起来太过复杂,在处理全球各个城市的航班起降问题时,显得尤其困难。

许多操作系统都提供了时区配置文件,如果时区信息发生变化,它们就会自动更新。可以在 Python 程序中借助 time 模块来使用这些时区信息。例如,下面这段代码会以太平洋夏令时(PDT)为标准,把航班从旧金山的起飞时间解析出来。

parse_format = '%Y-%m-%d %H:%M:%S %Z'
depart_sfo = '2014-05-01 15:45:16 PDT'
time_tuple - strptime(depart_sfo, parse_format)
time_str = strftime(time_format, time_tuple)
print(time_str)
>>>
2014-05-01 15:45:16

可以看到,strptime 函数可以正确解析 PDT 时间,那么,它是不是也能解析电脑所支持的其他时区呢?实际上是不行的。用纽约所在的美国东部夏令时(EasternDaylight Time,EDT)来实验一下,就会发现,strptime 函数抛出了异常。

arrival_nyc = '2014-05-01 23:33:24 EDT'
time_tuple = strptime(arrival_nyc, time_format)
>>>
valueError: unconverted data remains: EDT

之所以会出现这个问题,是因为 time 模块需要依赖操作系统而运作。该模块的实际行为,取决于底层的 C 函数如何与宿主操作系统相交互。这种工作方式,使得 time 模块的功能不够稳定。它无法协调地处理各种本地时区,因此,应该尽量少用这个模块。如果一定要使用 time 模块,那就只应该用它在 UTC 与宿主计算机的当地时区之间进行转换。对于其他类型的转换来说,还是使用 datetime 模块比较好。

2.datetime 模块

在内置的 datetime 模块中,有个名叫 datetime 的类,它也能像刚才所讲的 time 模块那样,用来在 Python 程序中描述时间。与 time 模块类似,datetime 可以把 UTC 格式的当前时间,转换为本地时间。

下面这段代码,会把 UTC 格式的当前时间,转换为计算机所用的本地时间(例如采用太平洋夏令时):

from datetime import datetime, timezone
now = datetime(2014, 8, 10, 18, 18, 30)
now_utc = now.replace(tzinfo=timezone.utc)
now_local = now_utc.astimezone()
print(now_local)
>>>
2014-08-10 11:18:30-07:00

datetime 模块还可以把本地时间轻松地转换成 UTC 格式的 UNIX 时间戳。

time_str = '2014-08-10 11:18:30'
now = datetime.strptime(time_str, time_format)
time_tuple = now.timetuple()
utc_now = mktime(time_tuple)
print(utc_now)
>>>
1407694710.0

与 time 模块不同的是,datetime 模块提供了一套机制,能够把某一种当地时间可靠地转换为另外一种当地时间。然而,在默认情况下,只能通过 datetime 中的 tzinfo 类及相关方法,来使用这套时区操作机制,因为它并没有提供 UTC 之外的时区定义。

所幸 Python 开发者社区提供了 pytz 模块,填补了这一空缺。该模块可以从 PythonPackage Index 下载(https://pypi.python.org/pypi/pytz/)。pytz 模块带有完整的数据库,其中包含了开发者可能会用到的每一种时区定义信息。

为了有效地使用 pyz 模块,总是应该先把当地时间转换为 UTC,然后针对 UTC 值进行 datetime 操作(例如,执行与时区偏移有关的操作),最后再把 UTC 转回当地时间。

例如,可以用下面这段代码,把航班到达纽约的时间,转换为 UTC 格式的 datetime 对象。某些函数调用语句,看上去似乎显得多余,但实际上,为了正确使用 pytz 模块,必须编写这些语句。

arrival_nyc = '2014-05-01 23:33:24'
nyc_dt_naive = datetime.strptime(arrival_nyc, time_format)
eastern = pytz.timezone('US/Eastern')
nyc_dt = eastern.localize(nyc_dt_naive)
utc_dt = pytz.utc.normalize(nyc_dt.astimezone(pytz.utc))
print(utc_dt)
>>>
2014-05-02 03:33:24+00:00

得到 UTC 格式的 datetime 之后,再把它转换成旧金山当地时间。

pacific = pytz.timezone( 'US/Pacific')
sf_dt = pacific.normalize(utc_dt.astimezone(pacific)
print(sf_dt)
>>>
2014-05-01 20:33:24-07:00

还可以把这个时间,轻松地转换成尼泊尔(Nepal)当地之间。

nepal = pytz.timezone('Asia/Katmandu')
nepal_dt = nepal.normalize(utc_dt.astimezone(nepal))
print(nepal_dt)
>>>
2014-05-02 09:18:24+05:45

无论宿主计算机运行何种操作系统,都可以通过 datetime 模块和 pytz 模块,在各种环境下协调一致地完成时区转换操作。

总结

  • 不要用 time 模块在不同时区之间进行转换。
  • 如果要在不同时区之间,可靠地执行转换操作,那就应该把内置的 datetime 模块与开发者社区提供的 pytz 模块搭配起来使用。
  • 开发者总是应该先把时间表示成 UTC 格式,然后对其执行各种转换操作,最后再把它转回本地时间。

第 46 条:使用内置算法与数据结构

如果 Python 程序要处理的数量比较可观,那么代码的执行速度会受到复杂算法的拖累。然而这并不能证明 Python 是一门执行速度很低的语言,因为这种情况很可能是算法和数据结构选择不佳而导致的。

幸运的是 Python 的标准程序库里面,内置了各种算法与数据结构,以供开发者使用。这些常见的算法与数据结构,不仅执行速度比较快,而且还可以简化编程工作。其中某些实用工具,是很难由开发者自己正确实现出来的。所以,应该直接使用这些 Python 自带的功能,而不要重新去实现它们,以节省时间和精力。

1.双向队列

collections 模块中的 deque 类,是一种双向队列(double-ended queue,双端队列)。从该队列的头部或尾部插入或移除一个元素,只需消耗常数级别的时间。这一特性,使得它非常适合用来表示先进先出(first-in-first-out,FIFO)的队列。

fifo = deque()
fifo.append(l)      # Producer
x = fifo.popleft()  # Consumer

内置的 list 类型,也可以像队列那样,按照一定的顺序来存放元素。从 list 尾部插入或移除元素,也仅仅需要常数级别的时间。但是,从 list 头部插人或移除元素,却会耗费线性级别的时间,这与 deque 的常数级时间相比,要慢得多。

2.有序字典

标准的字典是无序的。也就是说,在拥有相同键值对的两个 dict 上面迭代,可能会出现不同的迭代顺序。标准的字典之所以会出现这种奇怪的现象,是由其快速哈希表(fast hash table)的实现方式而导致的。

a = {l}
a['foo'] = 1
a['bar'] = 2

# Randomly populate 'b' to cause hash conflicts
while True:
    z = randint(99, 1013)
    b = {}
    for i in range(z):
        b[i] = i
    b['foo'] = 1
    b['bar'] = 2
    for i in range(z):
        del b[i]
    if str(b) != str(a):
        break
print(a)
print(b)
print('Equal?', a == b)
>>>
{'foo': 1, 'bar': 2}
{'bar': 2, 'foo': 1}
Equal? True

collections 模块中的 OrderedDict 类,是一种特殊的字典,它能够按照键的插入顺序,来保留键值对在字典中的次序。在 OrderedDict 上面根据键来迭代,其行为是确定的。这种确定的行为,可以极大地简化测试与调试工作。

a = OrderedDict()
a['foo'] = 1
a['bar'] = 2
b = OrderedDict()
b['foo'] = 'red'
b['bar'] = 'blue'

for value1, value2 in zip(a.values(), b.values()):
    print(value1, value2)
>>>
1 red
2 blue

3.带有默认值的字典

字典可用来保存并记录一些统计数据。但是,由于字典里面未必有要查询的那个键,所以在用字典保存计数器的时候,就必须要用稍微麻烦一些的方式,才能够实现这种简单的功能。

stats = {}
key ='my_counter'
if key not in stats:
    stats[key] = 0
stats[key] += 1

可以用 collections 模块中的 defaultdict 类来简化上述代码。如果字典里面没有待访问的键,那么它就会把某个默认值与这个键自动关联起来。于是,只需提供返回默认值的函数即可,字典会用该函数为每一个默认的键指定默认值。在本例中,使用内置的 int 函数来创建字典,这使得该字典能够以 0 为默认值。现在,计数器的实现代码就变得非常简单了。

stats = defaultdict(int)
stats['my_counter'] +=1

4.堆队列(优先级队列)

堆(heap)是一种数据结构,很适合用来实现优先级队列。heapq 模块提供了 heappush、heappop 和 nsmallest 等一些函数,能够在标准的 list 类型之中创建堆结构。

各种优先级的元素,都可以按任意顺序插入堆中。

a = []
heappush(a, 5)
heappush(a, 3)
heappush(a, 7)
heappush(a, 4)

这些元素总是会按照优先级从高到低的顺序,从堆中弹出(数值较小的元素,优先级较高)。

print(heappop(a), heappop(a), heappop(a), heappop(a))
>>>
3 4 5 7

用 heapq 把这样的 list 制作好之后,我们可以在其他场合使用它。只要访问堆中下标为 0 的那个元素,就总是能够查出最小值。

a = []
heappush(a, 5)
heappush(a, 3)
heappush(a, 7)
heappush(a, 4)
assert a[0] == nsmallest(1, a)[0] == 3

在这种 list 上面调用 sort 方法之后,该 list 依然能够保持堆的结构。

print('Before:', a)
a.sort()
print('After: ', a)
>>>
Before: [3, 4, 7, 5]
After:  [3, 4, 5, 7]

这些 heapq 操作所耗费的时间,与列表长度的对数成正比。如果在普通的 Python 列表上面执行相关操作,那么将会耗费线性级别的时间。

5.二分查找

在 list 上面使用 index 方法来搜索某个元素,所耗的时间会与列表的长度呈线性比例。

x = list(range(10**6))
i = x.index(991234)

bisect 模块中的 bisect_left 等函数,提供了高效的二分折半搜索算法,能够在一系列排好顺序的元素之中搜寻某个值。由 bisect_left 函数所返回的索引,表示待搜寻的值在序列中的插入点。

i = bisect_left(x, 991234)

二分搜索算法的复杂度,是对数级别的。这就意味着,用 bisect 来搜索包含一百万个元素的列表,与用 index 来搜索包含 14 个元素的列表,所耗的时间差不多。由此可见,这种对数级别的算法,要比线性级别的算法快很多。

6.与迭代器有关的工具

内置的 itertools 模块中,包含大量的函数,可以用来组合并操控迭代器。虽然这些工具未必都能够直接在 Python 2 中使用,但是模块的文档里面提供了一些简单的教程,开发者可以根据这些教程,在 Python 2 中轻松地构建与之相仿的功能。请在交互式的 Python 界面中输入 help(itertools),以查看详细的信息。

itertools函数分为三大类:

  • 能够把迭代器连接起来的函数:
    • chain:将多个迭代器按顺序连成一个迭代器。
    • cycle:无限地重复某个迭代器中的各个元素。
    • tec:把一个迭代器拆分成多个平行的迭代器。
    • zip_longest:与内置的 zip 函数相似,但是它可以应对长度不同的迭代器。
  • 能够从迭代器中过滤元素的函数:
    • islice:在不进行复制的前提下,根据索引值来切割迭代器。
    • takewhile:在判定函数(predicate function,谓词函数)为 True 的时候,从迭代器中逐个返回元素。
    • dropwhile:从判定函数初次为 False 的地方开始,逐个返回迭代器中的元素。
    • filterfalse:从迭代器中逐个返回能令判定函数为 False 的所有元素。其效果与内置的 filter 函数相反。
  • 能够把迭代器中的元素组合起来的函数:
    • product:根据迭代器中的元素计算笛卡儿积(Cartesian product),并将其返回。可以用 product 来改写深度嵌套的列表推导操作。
    • permutations:用迭代器中的元素构建长度为N的各种有序排列,并将所有排列形式返回给调用者。
    • combination:用迭代器中的元素构建长度为 N 的各种无序组合,并将所有组合形式返回给调用者。

除了上面提到的这些,itertools 模块里面还有其他一些函数及教程。如果你发现自己要编写一段非常麻烦的迭代程序,那就应该先花些时间来阅读 itertools 的文档,看看里面有没有现成的工具可供使用。

总结

  • 应该用 Python 内置的模块来描述各种算法和数据结构。
  • 开发者不应该自己去重新实现那些功能,因为很难把它写好。

第 47 条:在重视精确度的场合,应该使用 decimal

Python 语言很适合用来编写与数值型数据打交道的代码。Python 的整数类型,可以表达任意长度的值,其双精度浮点数类型,也遵循 IEEE 754 标准。此外, Python 还提供了标准的复数类型,用来表示虚数值。然而这些数值类型,并不能覆盖每一种情况。

例如,要根据通话时长和费率,来计算用户拨打国际长度电话所应支付的费用。假如用户打了 3 分 42 秒,从美国打往南极洲的电话,每分钟 1.45 美元,那么,这次通话的费用是多少呢?

可能认为,只要使用浮点数,就能算出合理的结果。

rate = 1.45
seconds = 3*60 + 42
cost = rate * seconds / 60
print(cost)
>>>
5.364999999999999

但是,把计算结果向分位取整之后,却发现,round 函数把分位右侧的那些数字完全舍去了。实际上,不足 1 分钱的部分,是应该按 1 分钱收取的,希望 round 函数把该值上调为 5.37,而不是下调为 5.36。

print(round(cost, 2))
>>>
5.36

假设还要对那种通话时长很短,而且费率很低的电话呼叫进行计费。下面这段代码,按照每分钟 0.05 美元的费率,来计算长度为 5 秒的通话费用:

rate = 0.05
seconds = 5
cost = rate * seconds / 60
print(cost)
>>>
0.004166666666666667

由于计算出来的数值很小,所以 round 函数会把它下调为 0,而这当然不是我们想要的结果。

print(round(cost, 2))
>>>
0.0

内置的 decimal 模块中,有个 Decimal 类,可以解决上面那些问题。该类默认提供 28 个小数位,以进行定点(fixed point)数学运算。如果有需要,还可以把精确度调得更高一些。Decimal 类解决了 IEEE 754 浮点数所产生的精度问题,而且开发者还可以更加精准地控制该类的舍入行为。

例如,可以把刚才计算美国与南极洲长途电话费的那个程序,用 Decimal 类改写。改写之后,程序会算出精确的结果,而不会像原来那样,只求出近似值。

rate = Decimal('1.45')
seconds = Decimal('222')  # 3*60 + 42
cost = rate * seconds / Decimal('60')
print(cost)
>>>
5.365

Decimal 类提供了一个内置的函数,它可以按照开发者所要求的精度及舍人方式,来准确地调整数值。

rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
print(rounded)
>>>
5.37

这个 quantize 方法,也能对那种时长很短、费用很低的电话,正确地进行计费。用 Decimal 类来改写之前的那段代码。改写之后,计算出来的电话费用,还是不足 1 分钱:

rate = Decimal('0.05')
seconds = Decimal('5')
cost = rate * seconds / Decimal('60')
print(cost)
>>>
0.004166666666666666666666666667

但是,可以在调用 quantize 方法时,指定合理的舍入方式,从而确保该方法能够把不足 1 分钱的部分,上调为 1 分钱。

rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
print(rounded)
>>>
0.01

虽然 Decimal 类很适合执行定点数的运算,但它在精确度方面仍有局限,例如, 1/3 这个数,就只能用近似值来表示。如果要用精度不受限制的方式来表达有理数,那么可以考虑使用 Fraction 类,该类包含在内置的 fractions 模块里。

总结

  • 对于编程中可能用到的每一种数值,都可以拿对应的 Python 内置类型,或内置模块中的类表示。
  • Decimal 类非常适合用在那种对精度要求很高,且对舍入行为要求很严的场合,例如,涉及货币计算的场合。

第 48 条:学会安装由 Python 开发者社区所构建的模块

Python 有个中央仓库(https://pypi.python.org),里面存放着各种模块,以供程序开发者安装并使用。这些模块都是由 Python 社区构建并维护的,模块作者与大家一样,都是 Python 程序员。如果你碰到了一个自己不太熟悉的编程难题,那就应该先去 Python Package Index(简称 PyPI)看看。PyPI 里面的代码,很有可能会帮你尽快找到答案。

为了安装由 Package Index 所提供的模块,需要使用名为 pip 的命令行工具。在 Python 3.4 及后续版本中,pip 是默认安装好的,开发者也可以通过 python -m pip 命令来使用该工具。对于较早的 Python 版本来说,我们可以在 Python Packaging 的网站(https://packaging.python.org)上面找到 pip 工具的安装方式。

有了 pip 工具,就可以非常方便地安装新模块了。例如,本章前面曾经用到了 pytz 模块,现在来安装这个模块:

$ pip3 install pytz
Downloading/unpacking pytz
  Downloading pytz-2014.4.tar.bz2 (159kB): 159kB downloaded
  Running setup.py (...) egg_info for package pytz

Installing collected packages: pytz
  Running setup.py install for pytz

Successfully installed pytz
Cleaning up...

在上面这个范例中,用 pip3 命令安装了 Python 3 版本的 pytz 软件包。如果使用不带 3 的 pip 命令,那就会安装 Python 2 版本的 pytz 软件包(在类 Linux 系统下似乎如此,如 Ubuntu,但是如果是 Windows 系统,如 Windows 10 下,只安装了 Python 3 的某个版本,可以直接使用 Python,对应的就是 Python 3,pip 也是如此)。大部分流行的软件包,都同时提供了Python 2 版本和 Python 3 版本。pip 工具也可以同 pyvenv 工具结合起来,以确定用户在使用你的项目时,必须安装哪些软件包。

PyPI 中的每个模块,都有软件许可协议。大部分软件包,尤其是流行的软件包,都采用自由的或开源的协议(详情参见 http:/lopensource.org)。大多数情况下,这些协议都允许开发者在自己的程序中,包含本模块的一份拷贝。

总结

  • Python Package Index(PyPI)包含了许多常用的软件包,它们都是由 Python 开发者社区来构建并维护的。
  • pip 是个命令行工具,可以从 PyPI 中安装软件包。
  • Python 3.4 及后续版本,默认装有 pip,使用早前 Python 版本的开发者,必须自行安装 pip。
  • 大部分 PyPI 模块,都是自由软件或开源软件。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值