Python CookBook —— Chapter 3 (个人笔记)

Chap 3 数字日期和时间

3.1 数字的四舍五入 — round(value, ndigits)

想对浮点数执行指定精度的舍入运算,可使用内置的 round(value, ndigits) 函数:

print(round(1.23, 1))     # 1.2
print(round(1.27, 1))     # 1.3
print(round(-1.27, 1))    # -1.3
print(round(1.25361, 3))  # 1.254

# 当一个值刚好在两个边界的中间时, round 函数返回离它最近的【偶数】:
print(round(1.5, 0))    # 2.0
print(round(2.5, 0))    # 2.0

# 传给 round() 函数的 ndigits 参数可以为负, 此时舍入运算会作用在十位、百位、千位等上面:
a = 1627731
print(round(a, -1))    # 1627730  舍去个位
print(round(a, -2))    # 1627700  舍去十位
print(round(a, -3))    # 1628000  舍去百位

若只是想简单地输出【一定宽度的数】,则只需在格式化时指定精度即可,无需使用 round 函数:

x = 1.23456
print(format(x, '0.2f'))    # 1.23
print(format(x, '0.3f'))    # 1.235 而非 1.234 (同 round 函数的规则)
print('value is {:0.3f}'.format(x))

不要试着用 round 函数来 “修正” 运算结果:在计算的时候会有一点点小的误差,但这些小的误差是能被理解与容忍的。如果不能允许这样的小误差,就得考虑使用 decimal 模块了。

a, b = 2.1, 4.2
c = a + b
print(c)    # 6.300000000000001, 这里无需对结果做 round 来得到 6.3

3.2 执行精确的浮点数运算 — Decimal(str) / localcontext() / math.fsum()

浮点数的一个普遍问题是它们不能精确地表示十进制数。即使是最简单的数学运算也会产生小的误差(使用 Python 的浮点数据类型时,没办法避免这样的误差):

b, a = 2.1, 4.2
c = a + b
print(c)    # 6.300000000000001
print(c == 6.3)    # False

若需要对浮点数执行精确的计算操作,并且不希望有任何小误差出现(还要能容忍一定的性能损耗),则可使用 decimal 模块。

Decimal 对象会像普通浮点数一样地工作,它支持所有的常用数学运算。当在 print & format 函数中使用它们时,看起来跟普通数字没什么两样。

from decimal import Decimal

a = Decimal('4.2')    # 注意这里参数为 str
b = Decimal('2.1')
c = a + b
print(c)    # 6.3
print(c == Decimal('6.3'))    # True
print(format(c, '>10.5f'))    # ‘   6.30000’

decimal 模块的一个主要特征是允许你控制计算的每一方面,包括数字位数和四舍五入运算。为此,你先得创建一个本地上下文并更改它的设置:

from decimal import localcontext

a, b = Decimal('1.3'), Decimal('1.7')
print(a/b)    # 默认精度为 30

with localcontext() as ctx:
    ctx.prec = 50    # 指定精度为 50
    print(a/b)

Remark在做科学领域的大多数运算时,使用普通浮点类型就已足够。① 在真实世界中很少会要求精确到普通浮点数能提供的 17 位精度。② 原生的浮点数计算要快得多,特别是在执行大量运算的时候。

# 仍要注意在 “大数和小数的加法运算中” 的误差:
import math

nums = [1.23e+18, 1, -1.23e+18]
# 1. 直接求和, 结果是 0 而不是 1, 这显然有误
print(sum(nums))

# 2. 使用 fsum 函数解决上面的问题
print(math.fsum(nums))

3.3 数字的格式化输出 — format() on floats & Decimal Obj

需要将【单个数字】格式化后输出,并控制数字的位数、对齐、千位分隔符和其他的细节时,可使用内置的 format() 函数:

x = 1234.56789

# 1. 控制位数 & 对齐
print('[' + format(x, '0.2f') + ']')      # [1234.57]
print('[' + format(x, '>10.1f') + ']')    # [    1234.6]
print('[' + format(x, '<10.1f') + ']')    # [1234.6    ]
print('[' + format(x, '^10.1f') + ']')    # [  1234.6  ]

# 2. 附上千位分隔符
print(format(x, ','))    
print(format(x, '0,.1f'))

# 3. 想使用科学计数法, 只需将 f 改成 e / E (取决于指数输出的大小写形式)
print(format(x, 'e'))    # 1.234568e+03
print(format(x, '0.2E'))    # 1.23E+03

# 4. 下面的例子同时指定了宽度 & 精度 (居中对齐, 长度20, 附上千位分隔符, 保留两位小数)
print('The value is [{:^20,.2f}]'.format(x))

上面演示的技术也适用于 Decimal 数字对象。另外,不推荐使用 (%) 来格式化数字,format() 函数是更好的选择。

3.4 2 / 8 / 16 进制整数 — bin() / oct() / hex() & format() / int()

下面介绍转换 or 输出使用二进制,八进制 or 十六进制表示的整数。

# 1. 为将整数转换为 2/8/16 进制的【字符串】, 可分别使用 bin, oct, hex 函数:
x = 1234
print(bin(x))    # 二进制
print(oct(x))    # 八进制
print(hex(x))    # 十六进制

# 2. 若不想输出 0b, 0o, 0x の前缀时可使用 format() 函数:
print(format(x, 'b'))
print(format(x, 'o'))
print(format(x, 'x'))

# 3. 负整数的情况:
x = -1234
print(format(x, 'b'))
print(format(x, 'o'))
print(format(x, 'x'))

# 4. 将 2, 8, 16 进制的【数字字符串】转换为十进制整数:
print(int('0b10011010010', 2))
print(int('0o2322', 8))
print(int('0x4d2', 16))    # int 的第二参数指定“进制”

3.5 字节到大整数的打包与解包 — ???

看不太懂0.0

3.6 复数的数学运算 — complex() / 1 + 1j / .real / .imag / .conjugate() / cmath

接下来介绍 Python 中关于复数的基础知识:

# 1. 使用 Complex Numbers 的两种方案:
a = complex(2, 4)
b = 3 - 5j
print(a, b)

# 2. 获取 Complex Numbers 的实部, 虚部 & 共轭复数:
print(a.real, a.imag)    # 实部 & 虚部
print(a.conjugate())     # 共轭复数

# 3. 复数的数学运算:
print(a + b)
print(a * b)
print(a / b)
print(abs(a))    # 复数の模

# 4. 使用复变函数 (正/余弦, 指数函数等):
import cmath

print(cmath.sin(a))
print(cmath.cos(a))
print(cmath.exp(a))

# 5. 使用 numpy 构造复数数组并在其上执行各种操作:
import numpy as np

a = np.array([2 + 3j, 4 + 5j, 6 - 7j, 8 + 9j])
print(a)
print(a + 2)
print(np.sin(a))

# 6. Python 的标准数学函数不能产生复数值:
import math
print(math.sqrt(-1))    # ValueError: math domain error

# 7. 上述问题改进如下:
print(cmath.sqrt(-1))    # 1j

3.7 无穷大与 NaN — float() / math.isinf() / math.isnan()

下面介绍正无穷、负无穷 & NaN(非数字) 的浮点数:

# 1. 使用 float() 来创建正无穷, 负无穷 & NaN(非数字):
a = float('inf')
b = float('-inf')
c = float('nan')
print(a, b, c)

# 2. 使用 math.isinf() & math.isnan() 函数测试这些值的存在性 :
import math

print(math.isinf(a))
print(math.isinf(b))
print(math.isnan(c))

# 3. 无穷大在数学计算中会传播:
print(45 + float('-inf'))
print(10 * float('-inf'))
print(233 / float('-inf'))    # -0.0 (注意这个负号)

# 4. 有些操作是未定义的并且会返回一个 NaN (和数学中的未定型一致):
print(float('inf') / float('-inf'))
print(float('inf') + float('-inf'))

# 5. NaN 会在【所有数学计算】中传播而不会产生异常:
print(23 + float('nan'))        # nan
print(float('nan') / 2)         # nan
print(16 * float('nan'))        # nan
print(math.sqrt(float('nan')))  # nan

# 6. NaN 有一个特点, 它们之间的比较操作总是返回 False:
print(float('nan') == float('nan'))    # False
print(float('nan') is float('nan'))    # False
""" 由于这个原因, 测试一个 NaN 唯一安全的方法就是使用 math.isnan() """

3.8 分数运算 — Fraction(p, q) / fraction.limit_denominator(n) / some_float.as_integer_ratio()

fractions 模块可被用来执行包含分数的数学运算:

from fractions import Fraction

# 1. 创建 Fraction 实例
a, b = Fraction(5, 4), Fraction(7, 16)
print(f'a = {a}; b = {b}')
print(f'a + b = {a + b}')
print(f'a * b = {a * b}')

# 2. Getting numerator/denominator
c = a * b
print(c)
print(c.numerator, c.denominator)    # 分子 & 分母

# 3. Fraction to Float
print(float(c))

# 4. Limiting the denominator of a value
d = c.limit_denominator(8)    # see below
print(f'c = {c}; d = {d}; difference = {float(c - d)}')

# 5. Float to Fraction
x = 3.75
print(x.as_integer_ratio())    # (15, 4)    将 float 数值转化为相应分子分母所成的 tuple
y = Fraction(*x.as_integer_ratio())
print(y)    # 15/4

limit_denominator(max_denominator=1000000) 返回一个 Fraction 使其值最接近 self (即调用此方法的 Fraction 实例) 且分母不大于 max_denominator。此方法适用于找出给定浮点数的有理近似值

pi = Fraction('3.1415926535897932')    # 另一种实例化 Fraction 对象的方法
print(pi)    # 7853981633974483/2500000000000000
pi_q = pi.limit_denominator(1000)    # 限制分母不超过 1000
print(pi_q, float(pi_q))    # 355/113    3.1415929203539825

3.9 大型数组运算 — numpy.ndarray 各种特性及通用函数, np.where()

涉及到数组的重量级运算时可使用 NumPy 库,其一个主要特征是它会给 Python 提供一个 (较标准 Python 列表更适合做数学运算的) 数组对象。

# 1. Python lists
x = [1, 2, 3, 4]
y = [5, 6, 7, 8]

print(x * 2)    # [1, 2, 3, 4, 1, 2, 3, 4]
print(x + y)    # [1, 2, 3, 4, 5, 6, 7, 8]
# print(x + 10)    # TypeError: can only concatenate list (not "int") to list

# 2. Numpy arrays
import numpy as np

ax = np.array([1, 2, 3, 4])
ay = np.array([5, 6, 7, 8])

print(ax * 2)    # [2 4 6 8]
print(ax + ay)    # [ 6  8 10 12]
print(ax + 10)    # [11 12 13 14]    注意, 这种运算对 list 是不允许的
print(ax * ay)    # [ 5 12 21 32]
print(ax ** ay)    # 对应元素间的幂运算

NumPy 中的标量运算 (如 ax * 2 or ax + 10 ) 会作用在每个元素上;当两个操作数都是数组时执行对应位置上的元素间的运算,并最终生成一个新数组。

# 1. 计算多项式值的例子: 
def f(x):
    """ 定义一个多项式 """
    return 3*(x**2) - 2*x + 7

print(f(ax))    # [ 8 15 28 47]    对数组中每个元素同时计算了 f(x) 所定义的 polynomial 的值

# 2. Numpy 提供的通用函数, 可用于代替 math 模块中的对应函数:
""" 使用这些通用函数要比循环数组并使用 math 模块中的函数执行计算要快的多。 """
print(np.sqrt(ax))    # 对每个元素开方
print(np.cos(ax))    # 求每个元素的余弦值

# 3. Numpy 数组较 list 的优势之一, 创建巨大的二维数组:
grid = np.zeros(shape=(10000, 10000), dtype=float)    # 10,000 by 10,000 2d-array
print(grid)
print(grid + 10)    # 每个元素执行 + 10 的运算
print(np.sin(grid + 10))    # 对每个元素 + 10 后求 sin 的值

Numpy 多维数组的索引功能值得注意:

# >>>>>> Indexing
# -----------------------------------------------------------------------------------------
a = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8],
              [9, 10, 11, 12]])    # 2d-array with size=(3, 4)
print(a)
# 1. Select row 1
print(a[1])
# 2. Select column 1
print(a[:, 1])
# 3. Select a subregion and change it (注意 3 是取不到的)
a[1:3, 1:3] += 10
print(a)

# >>>>>> Broadcast a row vector across an operation on all rows
# -----------------------------------------------------------------------------------------
print(a + [100, 101, 102, 103])
# 将一个 (1d) 行向量加到 2d 数组上, 效果是该行向量被加到 2d 数组中每个行向量上了, 这类似 scalar + 1d array 的情况, 那么对所有低维对象 + 高维对象, 这种规律是否都被满足?

# 注意这里 a 本身没有变化, 因为执行计算会生成一个新的 array
print(a)

# >>>>>> Conditional assignment on an array
# -----------------------------------------------------------------------------------------
print(np.where(a < 10, a, 10))    
# 在数组 a 中小于 10 的元素输出其自身, 否则输出 10 (注意这里的参数必须和数组变量名一致)

3.10 矩阵与线性代数运算 — np.matrix(), .T, .I, .det(), .eigvals(), .solve(m, v)

NumPy 通过矩阵对象来执行【矩阵 & 线性代数运算】,如矩阵乘法,求行列式值,解线性方程组等

import numpy as np

# 1. 实例化一个 3X3 矩阵
m = np.matrix([[1, -2, 3],
               [0, 4, 5],
               [7, 8, -9]])
print(m)

# 2. Transpose
print(m.T)

# 3. Inverse
print(m.I)

# 4. 实例化一个 3X1 矩阵 (列向量)
v = np.matrix([[2],
               [3],
               [4]])

# 5. Matrix Multiplication
print(m * v)

import numpy.linalg as la    # Linear Algebra 方面的计算通过 linalg 来实现

# 6. Determinant
print(la.det(m))

# 7. Eigenvalues
print(la.eigvals(m))

# 8. Solve for x in (mx = v)
x = la.solve(m, v)
print(x)

# 9. check solution
print(m * x == v)

3.11 随机选择 — random.choice(), .sample(), .shuffle(), .randint(), .random(), .seed()

从一个序列中随机抽取若干元素 or 生成几个随机数可通过 random 模块实现:

import random

# 1.想从一个序列中随机抽取一个元素, 可使用 random.choice():
values = [x for x in range(1, 6)]
print(random.choice(values))

# 2. 为提取 N 个“不同元素”的样本用于进一步操作, 可使用 random.sample():
print(random.sample(values, 3))

# 3. 想打乱序列中元素的顺序, 可使用 random.shuffle():
random.shuffle(values)
print(values)    # 注意到序列本身被改变了

# 4. 生成随机“整数”可使用 random.randint():
print(random.randint(0, 10))
print(random.randint(10, 100))

# 5. 为生成 0 到 1 范围内“均匀分布”的浮点数, 可使用 random.random():
print(random.random())

# 6. 可通过 random.seed() 函数修改初始化种子:
random.seed()    # Seed based on system time or os.urandom()
random.seed(12345)    # Seed based on integer given
""" 因为生成的是伪随机数, 有时需要复现随机数, 有时则需要不同的随机, 这时 random.seed() 就有用了 """

另外,random.uniform() 可生成均匀分布的随机数;random.gauss() 可生成正态分布的随机数。

# 为展示上面两个函数的效果, 需要结合可视化, 这里先挖个坑咯~

3.12 基本的日期与时间转换 — timedelta, datetime, 其间的加减法, today, dateutil.relativedelta

为执行不同时间单位的转换和计算,可使用 datetime 模块:

# 1. 时间段:
from datetime import timedelta

a = timedelta(days=2, hours=0.5)    # 创建时间段对象 timedelta
b = timedelta(hours=1.5)
c = a + b    # timedelta 对象可做加减法

print(c.days, c.seconds)    # 天数, 秒数
print(c.seconds / 3600)    # 秒数 to 小时数
print(c.total_seconds(), c.total_seconds() / 3600)    # 总秒数, 总小时数

# 2. 表示指定的日期 & 时间:
from datetime import datetime

a = datetime(2020, 9, 3)    # 创建日期时间对象 datetime
b = datetime(2020, 10, 1)
print(a, b)

c = a + timedelta(days=10)    # 日期时间对象 +/- 时间段对象
d = b - a    # datetime 对象相减得到一个 timedelta 对象 (不支持相加!)
print(c)
print(d)
print(d.days)

now = datetime.today()    # 当前日期时间
print(now)
print(now + timedelta(minutes=10))    # 当前日期时间对象 +/- timedelta

# 3. 注意 datetime 会自动处理闰年:
a = datetime(2012, 3, 1)
b = datetime(2012, 2, 28)
print(a - b)    # 2 days, 0:00:00    (可见2012为闰年)

c = datetime(2013, 3, 1)
d = datetime(2013, 2, 28)
print(c - d)    # 1 day, 0:00:00    (可见2013非闰年)

# 4. 更复杂的日期操作 ———— dateutil 模块中的 relativedelta() 方法:
from dateutil.relativedelta import relativedelta

a = datetime(2020, 7, 23)
# print(a + timedelta(months=1))
# 结果为 TypeError: 'months' is an invalid keyword argument for __new__()
""" timedelta 对象无法实现“增加一个月的时间段”, 这样的功能可通过 relativedelta 对象实现 """
print(a + relativedelta(months=+4))    # 这个 (+) 似乎不写也行

# 5. 同样时间间隔的不同表示: timedelta VS relativedelta
a = datetime(2020, 7, 23)
b = datetime(2020, 12, 24)
d = b - a
print(d)    # 154 days, 0:00:00

d = relativedelta(b, a)
print(d)    # relativedelta(months=+5, days=+1)
print(d.months, d.days)

3.13 计算最后一个周五的日期 — datetime 模块的应用, dateutil 模块的应用

如何获取上一个周五出现的日期?

# 返回上一个星期 X 的具体日期 (如星期五)
from datetime import datetime, timedelta, date

weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']


def get_previous_byday(dayname, start_date=None):
    # 1.若不指定起始日期, 则以当前日期为起始日期
    if start_date is None:
        start_date = date.today()
    # 2.获取起始日期的星期数, 0 ~ 6, 如 Sunday 对应 6
    day_num = start_date.weekday()
    # 3.确定目标星期数在 weekdays 列表中的索引
    day_num_target = weekdays.index(dayname)
    # 4.⭐⭐⭐⭐⭐⭐确定目标星期数还要经过几天才能到达起始星期数⭐⭐⭐⭐⭐⭐
    days_ago = (7 + day_num - day_num_target) % 7
    # 5.若今天的星期数同目标星期数恰好相同, 则上个目标星期数必为7天前
    if days_ago == 0:
        days_ago = 7
    # 6.从起始日期减去相应的天数, 获得目标星期数的具体日期
    target_date = start_date - timedelta(days=days_ago)
    return target_date


# 测试上面的函数
print(date.today())    # 以当前日期为参照
x = get_previous_byday('Monday')    # 上个周一的具体日期
y = get_previous_byday('Tuesday')    # 上个周二的具体日期
z = get_previous_byday('Friday')    # 上个周五的具体日期
print(x, y, z, sep='\n')

w = get_previous_byday('Sunday', date(2020, 8, 1))    # 2020-08-01 前的一个周日的日期
print(w)

dateutil 模块更适合执行大量日期计算:

from datetime import date
from dateutil.relativedelta import relativedelta
from dateutil.rrule import *

d = date.today()    # 以当前日期为参照
print(d)

# 2.1 Next Friday
print(d + relativedelta(weekday=FR))    # 这里传递的参数是什么含义?
# 2.2 Last Friday
print(d + relativedelta(weekday=FR(-1)))    # 这里传递的参数是什么含义?

3.14 计算当前月份的日期范围 — datetime 的应用, calendar 简介, 处理日期时间的 Generator

想要遍历当前月份中的每一天,需要一个计算这个日期范围的高效方法。

这样的遍历需要先构造一个包含所有日期的列表。为此可先计算出开始日期 & 结束日期,然后在步进时使用 datetime.timedelta 对象递增日期变量:

from datetime import datetime, date, timedelta
import calendar


def get_month_range(start_date=None):
    if start_date is None:
        # 获取当前日期(不含时间)后, 将 day 替换为 1 (即变成本月 1 号)
        start_date = date.today().replace(day=1)    # replace 也可以指定 year & month
    # calendar.monthrange(year, month) 返回指定年月的第一天是周几(Monday 以 0 表示) & 指定年月共有几天
    _, 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


# 1. 定义“步长”为 1 天
delta_day = timedelta(days=1)

# 2. 调用函数, 获取起始日期 & 终止日期
first_day, last_day = get_month_range()

# 3. 通过 while 开始遍历
while first_day < last_day:    # 这里使用 (<) 而不是 (<=), 否则将出现下月 1 号
    print(first_day)
    first_day += delta_day

可以为日期的迭代创建一个 Generator 如下:

def date_range(start, stop, step):
    while start < stop:
        yield start
        start += step

        
# 下面使用此 Generator:
for d in date_range(datetime(2020, 9, 1), datetime(2020, 10, 1), timedelta(hours=12)):
    # 注意 step 不一定得是以天为单位
    print(d)

上面结构简单的 Generator 能够这么有效,还得归功于 Python 中日期与时间能够使用标准数学 & 比较操作符来进行运算

calendar 模块有个很实用的功能:

print(calendar.calendar(2020))    # 2020 年的日历
print(calendar.month(2020, 8))    # 2020 年 8 月的日历

3.15 字符串转换为日期 — datetime.strptime(), datetime.strftime()

字符串形式的日期时间 与 datetime 对象相互转换:

# 1. 将【字符串形式的日期时间】转化为【相应的 datetime 对象】:
from datetime import datetime

text = '2020-09-1'
y = datetime.strptime(text, '%Y-%m-%d')    # 通过 strptime 实现 str 到 datetime
z = datetime.today()
diff = z - y    # 现在可以进行 datetime 对象的运算了
print(type(y), y)
print(diff)
""" datetime.strptime() 方法支持很多的格式化代码, 如 %Y 代表 4 位数年份, %m 代表两位数月份。"""

# 2. 将【datetime 对象】转化为 【好康的 str 对象】:
z = datetime(2020, 7, 28, 12, 21)
print(type(z), z)    # 2020-07-28 12:21:00

pretty_z = datetime.strftime(z, '%A %B %d, %Y')    # 通过 strftime 实现 datetime 到 str
print(type(pretty_z), pretty_z)    # Tuesday July 28, 2020

''' 
	需要注意的是,strptime() 的性能很差,特定情况下,下面的解析函数性能上会好得多 
'''

def parse_ymd(s):
    """
    :param s: 日期字符串
    :return: 日期时间对象
    """
    year_s, month_s, day_s = s.split('-')
    return datetime(int(year_s), int(month_s), int(day_s))

假如要处理大量的日期格式为 YYYY-MM-DD 的字符串,采用上面的函数效率远比 datetime.strptime 好得多。

3.16 结合时区的日期操作 — ???

看不懂0.0以后再啃

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值