Python CookBook 学习笔记
Table of Contents
第三章 数字日期和时间¶
3.1 数字的四舍五入¶
实际上好像四舍五不一定入
问题:你想对浮点数执行指定精度的舍入运算。
解决方案:对于简单的舍入运算,使用内置的 round(value, ndigits) 函数即可。比如:
>>> round(1.23, 1)
1.2
>>> round(1.27, 1)
1.3
>>> round(-1.27, 1)
-1.3
>>> round(1.25361,3)
1.254
当一个值刚好在两个边界的中间的时候, round 函数返回离它最近的偶数。 也就是说,对1.5或者2.5的舍入运算都会得到2。传给 round() 函数的 ndigits 参数可以是负数,这种情况下, 舍入运算会作用在十位、百位、千位等上面。比如:
>>> a = 1627731
>>> round(a, -1)
1627730
>>> round(a, -2)
1627700
>>> round(a, -3)
1628000
讨论:不要将舍入和格式化输出搞混淆了。如果需要输出一定宽度的数,你不需要使用 round() 函数。只需要在格式化的时候指定精度。同样,不要试着去舍入浮点值来”修正”表面上看起来正确的问题。比如,你可能倾向于这样做:Dont't do this
>>> a = 2.1
>>> b = 4.2
>>> c = a + b
>>> c
6.300000000000001
>>> c = round(c, 2) # "Fix" result (???)
>>> c
6.3
对于大多数使用到浮点的程序,没有必要也不推荐这样做。 尽管在计算的时候会有一点点小的误差,但是这些小的误差是能被理解与容忍的。 如果不能允许这样的小误差(比如涉及到金融领域),那么就得考虑使用 decimal 模块了
3.2 执行精确的浮点数运算¶
问题:你需要对浮点数执行精确的计算操作,并且不希望有任何小误差的出现。
解决方案:浮点数的一个普遍问题是它们并不能精确的表示十进制数。并且,即使是最简单的数学运算也会产生小的误差。这些错误是由底层 CPU 和 IEEE 754 标准通过浮点单位去执行算术时的特征。由于 Python 的浮点数据类型使用底层表示存储数据,因此没办法去避免这样的误差。如果需要更加精确(并能容忍一定的性能损耗),你可以使用 decimal 模块:
>>> from decimal import Decimal
>>> a = Decimal('4.2')
>>> b = Decimal('2.1')
>>> a + b
Decimal('6.3')
>>> print(a + b)
6.3
>>> (a + b) == Decimal('6.3')
True
Decimal 对象会像普通浮点数一样的工作,用起来跟普通数字没什么两样。decimal 模块的一个主要特征是允许你控制计算的每一方面,包括数字位数和四舍五入运算。先得创建一个本地上下文并更改它的设置。
>>> from decimal import localcontext
>>> a = Decimal('1.3')
>>> b = Decimal('1.7')
>>> print(a / b)
0.7647058823529411764705882353
>>> with localcontext() as ctx:
... ctx.prec = 3
... print(a / b)
...
0.765
>>> with localcontext() as ctx:
... ctx.prec = 50
... print(a / b)
...
0.76470588235294117647058823529411764705882352941176
讨论:decimal 模块实现了 IBM 的”通用小数运算规范”。
Python 新手会倾向于使用 decimal 模块来处理浮点数的精确运算。然而,先理解你的应用程序目的是非常重要的。如果你是在做科学计算或工程领域的计算、电脑绘图,或者是科学领域的大多数运算, 那么使用普通的浮点类型是比较普遍的做法。
- 在真实世界中很少会要求精确到普通浮点数能提供的17位精度。 因此,计算过程中的那么一点点的误差是被允许的。
- 第二点,原生的浮点数计算要快的多,有时候你在执行大量运算的时候速度也是非常重要的。
注意下 减法删除 以及 大数和小数 的加法运算所带来的影响。
>>> nums = [1.23e+18, 1, -1.23e+18]
>>> sum(nums) # Notice how 1 disappears
0.0
上面的错误可以利用 math.fsum() 所提供的更精确计算能力来解决:
>>> import math
>>> math.fsum(nums)
1.0
总的来说, decimal 模块主要用在涉及到金融的领域,平时不太可能需要。
3.3 数字的格式化输出¶
问题:你需要将数字格式化后输出,并控制数字的位数、对齐、千位分隔符和其他的细节。
解决方案:格式化输出单个数字的时候,可以使用内置的 format() 函数。
>>> x = 1234.56789
>>> format(x, '0,.1f')
'1,234.6'
如果你想使用指数记法,将f改成e或者E(取决于指数输出的大小写形式)。比如:
>>> format(x, 'e')
'1.234568e+03'
>>> format(x, '0.2E')
'1.23E+03'
指定宽度和精度的一般形式是 '[<>^]?width[,]?(.digits)?', 其中 width 和 digits 为整数,?代表可选部分。 同样的格式也被用在字符串的 format() 方法中。比如:
>>> 'The value is {:0,.2f}'.format(x)
'The value is 1,234.57'
讨论:数字格式化输出通常是比较简单的。上面演示的技术同时适用于浮点数和 decimal 模块中的 Decimal 数字对象。当指定数字的位数后,结果值会根据 round() 函数同样的规则进行四舍五入后返回。包含千位符的格式化跟本地化没有关系。 如果你需要根据地区来显示千位符,你需要自己去调查下 locale 模块中的函数了。 你同样也可以使用字符串的 translate() 方法来交换千位符。
>>> swap_separators = { ord('.'):',', ord(','):'.' }
>>> format(x, ',').translate(swap_separators)
'1.234,56789'
不要使用 % 格式化输出
3.4 二八十六进制整数¶
问题:二进制、八进制、十六进制的相互转换
解决方案:为了将整数转换为二进制、八进制或十六进制的文本串, 可以分别使用 bin() , oct() 或 hex() 函数。另外,如果你不想输出 0b , 0o 或者 0x 的前缀的话,可以使用 format() 函数。
>>> format(x, 'b')
'10011010010'
>>> format(x, 'o')
'2322'
>>> format(x, 'x')
'4d2'
整数是有符号的,所以如果你在处理负数的话,输出结果会包含一个负号。
>>> x = -1234
>>> format(x, 'b')
'-10011010010'
>>> format(x, 'x')
'-4d2'
复数转换的二进制码并不是计算机中实际存储的补码,如果要转换为补码,需要增加一个指示最大位长度的值。
>>> x = -1234
>>> format(2**32 + x, 'b')
'11111111111111111111101100101110'
>>> format(2**32 + x, 'x')
'fffffb2e'
为了以不同的进制转换整数字符串,使用带有进制的 int() 函数即可。
>>> int('4d2', 16)
1234
>>> int('10011010010', 2)
1234
讨论:使用八进制的程序员有一点需要注意下。 Python 指定八进制数的语法跟其他语言稍有不同。需确保八进制数的前缀是 0o
>>> os.chmod('script.py', 0o755)
3.5 字节到大整数的打包与解包¶
问题:你有一个字节字符串并想将它解压成一个整数。或者,你需要将一个大整数转换为一个字节字符串。
解决方案:为了将 bytes 解析为整数,使用 int.from_bytes() 方法,并像下面这样指定字节顺序:
>>> len(data)
16
>>> int.from_bytes(data, 'little')
69120565665751139577663547927094891008
>>> int.from_bytes(data, 'big')
94522842520747284487117727783387188
为了将一个大整数转换为一个字节字符串,使用 int.to_bytes() 方法,
>>> x = 94522842520747284487117727783387188
>>> x.to_bytes(16, 'big')
b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004'
>>> x.to_bytes(16, 'little')
b'4\x00#\x00\x01\xef\xcd\x00\xab\x90x\x00V4\x12\x00'
讨论:作为一种替代方案,你可能想使用 6.11 小节中所介绍的 struct 模块来解压字节。 这样也行得通,不过利用 struct 模块来解压对于整数的大小是有限制的。 因此,你可能想解压多个字节串并将结果合并为最终的结果,就像下面这样:
>>> data
b'\x00\x124V\x00x\x90\xab\x00\xcd\xef\x01\x00#\x004'
>>> import struct
>>> hi, lo = struct.unpack('>QQ', data)
>>> (hi << 64) + lo
94522842520747284487117727783387188
字节顺序规则 (little, big) 仅仅指定了构建整数时的字节的低位高位排列方式。 我们从下面精心构造的 16进制数的表示中可以很容易的看出来:
>>> x = 0x01020304
>>> x.to_bytes(4, 'big')
b'\x01\x02\x03\x04'
>>> x.to_bytes(4, 'little')
b'\x04\x03\x02\x01'
使用 int.bit_length() 方法来决定需要多少字节位来存储这个值
>>> x = 523 ** 23
>>> x
335381300113661875107536852714019056160355655333978849017944067
>>> x.to_bytes(16, 'little')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OverflowError: int too big to convert
>>> x.bit_length()
208
>>> nbytes, rem = divmod(x.bit_length(), 8)
>>> if rem:
... nbytes += 1
...
>>>
>>> x.to_bytes(nbytes, 'little')
b'\x03X\xf1\x82iT\x96\xac\xc7c\x16\xf3\xb9\xcf...\xd0'
3.6 复数的数学运算¶
问题:需要使用复数来执行一些计算操作
解决方案:使用函数 complex(real, imag) 或者是带有后缀 j 的浮点数来指定。
>>> a = complex(2, 4)
>>> b = 3 - 5j
获取实部、虚部和共轭复数 a.real
, a.imag
, a.conjugate()
, 常见的数学运算和实数无异,如果要执行其他的复数函数比如正弦、余弦或平方根,使用 cmath 模块:
>>> import cmath
>>> cmath.sin(a)
(24.83130584894638-11.356612711218174j)
讨论:Python 中大部分与数学相关的模块都能处理复数,比如 NumPy 可以很容易的构造一个复数数组并在这个数组上执行各种操作。Python 的标准数学函数确实不能产生复数值,如果想使用复数必须使用 cmath 或其他支持复数的库。
3.7 无穷大与NaN¶
问题:你想创建或测试正无穷、负无穷或NaN(非数字)的浮点数。
解决方案:可以使用 float() 来创建它们。a = float('inf')
使用 math.isinf() 和 math.isnan() 函数进行测试
>>> math.isinf(a)
True
讨论:无穷大数在执行数学计算的时候会传播,有些操作是未定义的并会返回一个NaN结果。
>>> a = float('inf')
>>> a/a
nan
NaN值会在所有操作中传播,而不会产生异常。
>>> c = float('nan')
>>> c + 23
nan
NaN 值的一个特别的地方时它们之间的比较操作总是返回 False。所以只能用 math.isnan() 测试是否为 NaN 有时候程序员想改变 Python 默认行为,在返回无穷大或 NaN 结果的操作中抛出异常。fpectl 模块可以用来改变这种行为,但是它在标准的Python构建中并没有被启用,它是平台相关的, 并且针对的是专家级程序员。
3.8 分数运算¶
问题:需要进行分数计算
解决方案:fractions 模块可以被用来执行包含分数的数学运算。
>>> from fractions import Fraction
>>> a = Fraction(5, 4)
>>> b = Fraction(7, 16)
>>> print(a + b)
27/16
>>> # Getting numerator/denominator
>>> c = a * b
>>> c.numerator
35
>>> c.denominator
64
>>> # Converting to a float
>>> float(c)
0.546875
>>> # Limiting the denominator of a value
>>> print(c.limit_denominator(8))
4/7
>>> # Converting a float to a fraction
>>> x = 3.75
>>> y = Fraction(*x.as_integer_ratio())
>>> y
Fraction(15, 4)
3.9 大型数组运算¶
问题:你需要在大数据集(比如数组或网格)上面执行计算。
解决方案:涉及到数组的重量级运算操作,可以使用 NumPy 库。NumPy 的一个主要特征是它会给 Python 提供一个数组对象,相比标准的 Python 列表而已更适合用来做数学运算。NumPy 中的标量运算(比如 ax * 2 或 ax + 10 )会作用在每一个元素上。另外,当两个操作数都是数组的时候执行元素对等位置计算,并最终生成一个新的数组。对整个数组中所有元素同时执行数学运算可以使得作用在整个数组上的函数运算简单而又快速。
>>> ax = np.array([1, 2, 3, 4])
>>> def f(x):
... return 3*x**2 - 2*x + 7
...
>>> f(ax)
array([ 8, 15, 28, 47])
NumPy 还为数组操作提供了大量的通用函数,这些函数可以作为 math 模块中类似函数的替代。
>>> np.sqrt(ax)
array([ 1. , 1.41421356, 1.73205081, 2. ])
>>> np.cos(ax)
array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362])
使用这些通用函数要比循环数组并使用 math 模块中的函数执行计算要快的多。尽量选择 NumPy 的数组方案。
底层实现中, NumPy 数组使用了 C 或者 Fortran 语言的机制分配内存。它们是一个非常大的连续的并由同类型数据组成的内存区域。可以构造一个比普通 Python 列表大的多的数组。
>>> grid = np.zeros(shape=(10000,10000), dtype=float)
NumPy 扩展了 Python 列表的索引功能,特别是对于多维数组。
>>> a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
>>> a[1]
array([5, 6, 7, 8])
>>> a[:,1]
array([ 2, 6, 10])
>>> a[1:3, 1:3]
array([[ 6, 7],
[10, 11]])
>>> a[1:3, 1:3] += 10
>>> a
array([[ 1, 2, 3, 4],
[ 5, 16, 17, 8],
[ 9, 20, 21, 12]])
>>> a + [100, 101, 102, 103]
array([[101, 103, 105, 107],
[105, 117, 119, 111],
[109, 121, 123, 115]])
>>> np.where(a < 10, a, 10)
array([[ 1, 2, 3, 4],
[ 5, 10, 10, 8],
[ 9, 10, 10, 10]])
讨论:NumPy 是 Python 领域中很多科学与工程库的基础,同时也是被广泛使用的最大最复杂的模块。
3.10 矩阵与线性代数运算¶
问题:需要执行矩阵和线性代数运算,比如矩阵乘法、寻找行列式、求解线性方程组等等。
解决方案:NumPy 库有一个矩阵对象可以用来解决这个问题。
>>> import numpy as np
>>> m = np.matrix([[1,-2,3],[0,4,5],[7,8,-9]])
>>> m
matrix([[ 1, -2, 3],
[ 0, 4, 5],
[ 7, 8, -9]])
m.T Return transpose
m.I Return inverse
matrix 对象的矩阵乘法直接使用 * 而 ndarray 的矩阵乘法需要使用 ndarray.dot. 可以在 numpy.linalg 子包中找到更多的操作函数
>>> import numpy.linalg
numpy.linalg.det(m) Determinant
numpy.linalg.eigvals(m) Eigenvalues
numpy.linalg.solve(m, v) Solve for x in mx = v
3.11 随机选择¶
问题:你想从一个序列中随机抽取若干元素,或者想生成几个随机数。
解决:random 模块有大量的函数用来产生随机数和随机选择元素。
random.choice() 要想从一个序列中随机的抽取一个元素
random.sample() 提取出N个不同元素
random.shuffle() 打乱序列中元素的顺序
random.randint() 生成随机整数
random.random() 生成 0 到 1 范围内均匀分布的浮点数
random.getrandbits() 获取N位随机位(二进制)的整数
random.seed() # Seed based on system time or os.urandom()
random.seed(12345) # Seed based on integer given
random.seed(b'bytedata') # Seed based on byte data
>>> values = [1, 2, 3, 4, 5, 6]
>>> random.sample(values, 2)
[6, 2]
>>> random.shuffle(values)
>>> random.getrandbits(200)
335837000776573622800628485064121869519521710558559406913275
讨论:random 模块使用 Mersenne Twister 算法来计算生成随机数。这是一个确定性算法, 但是你可以通过 random.seed() 函数修改初始化种子。random模块还包含基于均匀分布、高斯分布和其他分布的随机数生成函数。 比如, random.uniform() 计算均匀分布随机数, random.gauss() 计算正态分布随机数。
在 random 模块中的函数不应该用在和密码学相关的程序中。 如果你确实需要类似的功能,可以使用 ssl 模块中相应的函数。ssl.RAND_bytes() 可以用来生成一个安全的随机字节序列。
3.12 基本的日期与时间转换¶
问题:需要执行简单的时间转换,比如天到秒,小时到分钟等的转换
解决方案:为了执行不同时间单位的转换和计算,请使用 datetime 模块。
>>> from datetime import timedelta
>>> a = timedelta(days=2, hours=6)
>>> b = timedelta(hours=4.5)
>>> c = a + b
>>> c.days
2
>>> c.seconds
37800
>>> c.seconds / 3600
10.5
>>> c.total_seconds() / 3600
58.5
先创建一个 datetime 实例然后使用标准的数学运算来操作
>>> from datetime import datetime
>>> a = datetime(2012, 9, 23)
>>> print(a + timedelta(days=10))
2012-10-03 00:00:00
>>>
>>> b = datetime(2012, 12, 21)
>>> d = b - a
>>> d.days
89
>>> now = datetime.today()
>>> print(now)
2012-12-21 14:54:43.094063
>>> print(now + timedelta(minutes=10))
2012-12-21 15:04:43.094063
datetime 会自动处理闰年。
>>> a = datetime(2012, 3, 1)
>>> b = datetime(2012, 2, 28)
>>> a - b
datetime.timedelta(2)
>>> (a - b).days
2
>>> c = datetime(2013, 3, 1)
>>> d = datetime(2013, 2, 28)
>>> (c - d).days
1
讨论 对大多数基本的日期和时间处理问题, datetime 模块以及足够了。 如果你需要执行更加复杂的日期操作,比如处理时区,模糊时间范围,节假日计算等等,可以考虑使用 dateutil 模块。
许多类似的时间计算可以使用 dateutil.relativedelta() 函数代替。 但是,有一点需要注意的就是,它会在处理月份(还有它们的天数差距)的时候填充间隙。
>>> a = datetime(2012, 9, 23)
>>> a + timedelta(months=1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'months' is an invalid keyword argument for this function
>>>
>>> from dateutil.relativedelta import relativedelta
>>> a + relativedelta(months=+1)
datetime.datetime(2012, 10, 23, 0, 0)
>>> a + relativedelta(months=+4)
datetime.datetime(2013, 1, 23, 0, 0)
>>>
>>> # Time between two dates
>>> b = datetime(2012, 12, 21)
>>> d = b - a
>>> d
datetime.timedelta(89)
>>> d = relativedelta(b, a)
>>> d
relativedelta(months=+2, days=+28)
>>> d.months
2
>>> d.days
28
3.13 计算最后一个周五的日期¶
问题:需要查找星期中某一天最后出现的日期,比如星期五。
解决方案:Python的 datetime 模块中有工具函数和类可以帮助你执行这样的计算。
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
Topic: 最后的周五
Desc :
"""
from datetime import datetime, timedelta
weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday', 'Sunday']
def get_previous_byday(dayname, start_date=None):
if start_date is None:
start_date = datetime.today()
day_num = start_date.weekday()
day_num_target = weekdays.index(dayname)
days_ago = (7 + day_num - day_num_target) % 7
if days_ago == 0:
days_ago = 7
target_date = start_date - timedelta(days=days_ago)
return target_date
讨论:算法原理是这样的,先将开始日期和目标日期映射到星期数组的位置上(星期一索引为0), 然后通过模运算计算出目标日期要经过多少天才能到达开始日期。然后用开始日期减去那个时间差即得到结果日期。
如果你要像这样执行大量的日期计算的话,你最好安装第三方包 python-dateutil 来代替。
>>> from datetime import datetime
>>> from dateutil.relativedelta import relativedelta
>>> from dateutil.rrule import *
>>> d = datetime.now()
>>> print(d)
2012-12-23 16:31:52.718111
# Next Friday
>>> print(d + relativedelta(weekday=FR))
2012-12-28 16:31:52.718111
>>>
# Last Friday
>>> print(d + relativedelta(weekday=FR(-1)))
2012-12-21 16:31:52.718111
3.14 计算当前月份的日期范围¶
问题:你的代码需要在当前月份中循环每一天,想找到一个计算这个日期范围的高效方法。
解决方案:在这样的日期上循环并需要事先构造一个包含所有日期的列表。 你可以先计算出开始日期和结束日期, 然后在你步进的时候使用 datetime.timedelta 对象递增这个日期变量即可。
from datetime import datetime, date, timedelta
import calendar
def get_month_range(start_date=None):
if start_date is None:
start_date = date.today().replace(day=1)
_, days_in_month = calendar.monthrange(start_date.year, start_date.month)
end_date = start_date + timedelta(days=days_in_month)
return (start_date, end_date)
>>> a_day = timedelta(days=1)
>>> first_day, last_day = get_month_range()
>>> while first_day < last_day:
... print(first_day)
... first_day += a_day
...
2012-08-01
2012-08-02
2012-08-03
#... and so on...
讨论:计算出一个对应月份第一天的日期。 一个快速的方法就是使用 date 或 datetime 对象的 replace() 方法简单的将 days 属性设置成 1 即可。replace() 会创建和传入对象类型相同的对象。使用 calendar.monthrange() 函数来找出该月的总天数。monthrange() 函数会返回包含星期和该月天数的元组。 为了在日期范围上循环,要使用到标准的数学和比较操作。 比如,可以利用 timedelta 实例来递增日期,小于号 < 用来检查一个日期是否在结束日期之前。
3.15 字符串转换为日期¶
问题:想把字符串格式的日期转换为 datetime 对象
解决方案:使用Python的标准模块 datetime 可以很容易的解决这个问题。
>>> from datetime import datetime
>>> text = '2012-09-20'
>>> y = datetime.strptime(text, '%Y-%m-%d')
讨论:datetime.strptime() 方法支持很多的格式化代码, 比如 %Y 代表4位数年份,%m 代表两位数月份,这些格式化占位符也可以反过来使用。
>>> z
datetime.datetime(2012, 9, 23, 21, 37, 4, 177393)
>>> nice_z = datetime.strftime(z, '%A %B %d, %Y')
>>> nice_z
'Sunday September 23, 2012'
strptime() 的性能要比你想象中的差很多,因为它使用纯 Python 实现,并且必须处理所有的系统本地设置。如果在意性能,可以自己实现
from datetime import datetime
def parse_ymd(s):
year_s, mon_s, day_s = s.split('-')
return datetime(int(year_s), int(mon_s), int(day_s))
3.16 结合时区的日期操作¶
Not Now