第12章 数据类型

第 12 章:数据类型——Python 的积木们

🎉 欢迎来到 Python 数据的奇幻世界!在这一章里,我们要认识各种"数据类型"。如果说 Python 是一门艺术,那数据类型就是画家的颜料盘。没有颜料,画家也画不出蒙娜丽莎;没有数据类型,Python 程序员只能对屏幕发呆。所以,系好安全带,我们要发车了!


12.1 NoneType——“什么都没有"的哲学

12.1.1 None 的含义

在 Python 的世界里,有一个神奇的存在,它叫 None。它不是 0,不是空字符串,不是空列表,而是一个表示"什么都没有"的特殊值

你可以把 None 想象成一个空箱子——这个箱子本身是存在的,但它里面啥都没有。就像你去快递柜取件,柜子是存在的,但里面没你的包裹。

1
2
3
4
# None 就是 None,它自己
x = None
print(x)          # None
print(type(x))    # <class 'NoneType'>

📝 NoneTypeNone 的类型,全世界只有一个 None 的实例——对,就是那个 None。它不像 intstr 那样有无数个实例,None 是独一无二的孤独选手。

12.1.2 None vs 空值(""、[]、{})

这是新手最容易搞混的地方。让我用一个生动的比喻来解释:

比喻
None一个不存在的空盒子——盒子本身不存在
""一个存在的空盒子——盒子在,但里面没东西
[]一个存在的空箱子——箱子在,但里面没货物
{}一个存在的空抽屉柜——柜子在,但抽屉都是空的
1
2
3
4
5
6
7
8
9
# 让我们来验证一下
print(None == "")      # False —— 物种都不一样!
print(None == [])      # False —— 一个是虚无,一个是空list
print(None == {})      # False —— 完全不同

# 空值不是 None
print(bool(None))      # False —— None 是 falsy 的
print(bool(""))        # False —— 空字符串也是 falsy 的
print(bool([]))        # False —— 空列表还是 falsy 的

💡 虽然 None""[]{} 在布尔判断中都等于 False(都是 falsy 值),但它们本质上完全不同None 表示"这里压根没有值”,而空容器表示"这里有值,只是是空的"。

12.1.3 None 的用途

None 在 Python 中可是个多面手,用途广泛。

12.1.3.1 作为函数默认参数

当你设计一个函数,有时候需要表示"用户没给我这个参数"的状态,这时候 None 就派上用场了。

1
2
3
4
5
6
7
8
def greet(name=None):
    # 如果用户没传 name,我们就给他一个默认的
    if name is None:
        name = "神秘游客"
    print(f"你好,{name}!欢迎光临!")

greet()               # 你好,神秘游客!欢迎光临!
greet("小明")         # 你好,小明!欢迎光临!

💡 为什么不用 "" 或其他默认值?因为 None 可以明确区分"用户没传参数"和"用户传了一个空值"。如果默认参数是 "",你就分不清用户是真的传了空字符串,还是压根没传。

12.1.3.2 作为函数返回值

当函数不需要返回具体值时(比如只执行打印操作的函数),默认返回 None

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def say_hello():
    print("Hello!")

result = say_hello()
print(result)         # None —— 没有 return 语句的函数都返回 None

# 常见的场景:查找操作,没找到就返回 None
def find_user(users, user_id):
    for user in users:
        if user["id"] == user_id:
            return user
    return None  # 找不到就返回 None,而不是抛异常

users = [{"id": 1, "name": "张三"}, {"id": 2, "name": "李四"}]
print(find_user(users, 99))   # None —— 没找到
print(find_user(users, 1))    # {'id': 1, 'name': '张三'}

12.1.3.3 表示不存在或未知值

有时候,数据"暂时不存在"比"根本不应该存在"更准确。None 就是这种场景的最佳选择。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 比如一个用户的"最后一次登录时间"——新用户可能还没登录过
user = {"name": "小明", "last_login": None}
# 这里 None 表示"还没有登录记录",而不是"登录时间是0"或者"忘记记录了"

# Optional 类型提示(Python 3.5+)
from typing import Optional

def get_user_age(user: Optional[dict]) -> Optional[int]:
    """获取用户年龄,可能用户不存在(返回 None)或者用户没设置年龄(也是 None)"""
    if user is None:
        return None
    return user.get("age")

12.2 数值类型——数字的万花筒

Python 提供了丰富的数值类型,从整数到浮点数,从复数到精确小数,应有尽有。让我们一个一个来认识它们!

12.2.1 int(整数)—— 没有上限的数字

12.2.1.1 任意精度(无溢出)

在很多编程语言(比如 C、Java)中,int 是有上限的。超过上限就会溢出,数字会变成负数或者变成一个莫名其妙的结果。

但 Python 的 int任意精度的!理论上,你可以表示任意大的整数,只要你的内存够用。

1
2
3
4
5
6
7
8
# 在 Python 中,你可以计算这个
big_number = 10 ** 1000  # 1 后面跟 1000 个零!
print(big_number)  # 这在 C 语言里早就溢出了,但 Python 表示:就这?

# 再来个大一点的
really_big = 2 ** 10000
print(f"2的10000次方有 {really_big.bit_length()} 位")
# 2的10000次方有 10001 位 —— 这数字比宇宙中的原子数还多!

🌌 Python 整数使用变长表示,即:数字越大,占用的内存越多。所以别想着用一个天文数字把电脑撑爆——最多只是算得慢一点。

12.2.1.2 进制转换

Python 支持多种进制表示和转换,非常方便:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 字面量表示
print(42)           # 十进制: 42
print(0b101010)     # 二进制(binary): 42 —— 前缀 0b
print(0o52)         # 八进制(octal): 42 —— 前缀 0o
print(0x2a)         # 十六进制(hex): 42 —— 前缀 0x

# 十进制转其他进制
print(bin(42))      # '0b101010'
print(oct(42))      # '0o52'
print(hex(42))      # '0x2a'

# 字符串转整数(指定进制)
print(int("101010", 2))   # 42 —— 把二进制字符串转成十进制int
print(int("0x2a", 16))   # 42 —— 把十六进制字符串转成十进制int
print(int("52", 8))       # 42 —— 把八进制字符串转成十进制int

# 格式化输出不同进制
print(f"{42:b}")  # 101010 —— 二进制
print(f"{42:o}")  # 52 —— 八进制
print(f"{42:x}")  # 2a —— 十六进制

💡 小技巧:二进制用 0b,八进制用 0o,十六进制用 0x—— 字母 o 和 x 帮助区分八进制(octal)和十六进制(hex)。

12.2.1.3 位运算操作

位运算是对整数的二进制位进行操作,速度飞快,是高级 Python 程序员必备技能!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 与(&):两个位都是1才得1
a = 0b1100  # 12
b = 0b1010  # 10
print(bin(a & b))   # 0b1000 —— 结果是 8

# 或(|):只要有一个是1就得1
print(bin(a | b))   # 0b1110 —— 结果是 14

# 异或(^):不同得1,相同得0
print(bin(a ^ b))   # 0b0110 —— 结果是 6

# 取反(~):按位取反,等于 -(n+1)
print(~5)    # -6 —— 这是因为 Python 使用补码表示负数

# 左移(<<):二进制位左移,相当于乘以2的n次方
print(1 << 3)   # 8 —— 1 左移 3 位,等于 1 * 2^3 = 8
print(5 << 2)   # 20 —— 5 * 4 = 20

# 右移(>>):二进制位右移,相当于除以2的n次方(向下取整)
print(16 >> 2)   # 4 —— 16 / 4 = 4
print(15 >> 2)   # 3 —— 15 / 4 = 3.75,向下取整得 3

🐍 经典面试题:如何不用临时变量交换两个数字?

1
2
3
4
5
6
a = 5
b = 8
a = a ^ b
b = a ^ b  # b = (a ^ b) ^ b = a
a = a ^ b  # a = (a ^ b) ^ a = b
print(a, b)  # 8 5 —— 交换完成!

12.2.1.4 bit_length()、to_bytes()、from_bytes()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# bit_length():返回整数的二进制表示的位数
x = 15
print(x.bit_length())   # 4 —— 15 的二进制是 1111,有4位
print((2 ** 100).bit_length())  # 101 —— 2的100次方有101位

# to_bytes():将整数转为字节串(用于文件写入、网络传输等)
# to_bytes(length, byteorder='big')
num = 1024
print(num.to_bytes(2, 'big'))    # b'\x04\x00' —— 大端序
print(num.to_bytes(2, 'little')) # b'\x00\x04' —— 小端序

# from_bytes():从字节串恢复整数
b = b'\x04\x00'
print(int.from_bytes(b, 'big'))     # 1024
print(int.from_bytes(b, 'little'))  # 4

12.2.2 float(浮点数)—— 小数点的艺术

12.2.2.1 IEEE 754 双精度标准

Python 的 float(又称双精度浮点数,double)遵循 IEEE 754 标准。这个标准规定:

  • 使用 64 个二进制位(bit)存储一个浮点数
  • 1 位用于符号(正/负)
  • 11 位用于指数(决定数值范围)
  • 52 位用于尾数(决定数值精度)

这意味着浮点数的精度是有限的,大约有 15-17 位有效十进制数字。

1
2
3
4
import sys
print(sys.float_info)
# sys.float_info(max=1.7976931348623157e+308, max_exp=1024, ...)
# 这告诉我们:float 能表示的最大值约为 1.8 × 10^308

12.2.2.2 精度问题(0.1 + 0.2 != 0.3)

这是 Python 浮点数最著名的问题——也是所有编程语言浮点数的通病!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 经典问题
print(0.1 + 0.2)   # 0.30000000000000004 —— 为什么会这样?!

# 比较浮点数不能直接用 ==
print(0.1 + 0.2 == 0.3)   # False —— 令人崩溃的 False!

# 正确的方法:使用 math.isclose() 或 round()
import math
print(math.isclose(0.1 + 0.2, 0.3))   # True —— 这才对嘛

# 或者用 decimal 模块(后面会详细介绍)
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2') == Decimal('0.3'))  # True —— 精确计算

🔬 为什么会这样?因为 0.1 在二进制中是一个无限循环小数!就像 1/3 在十进制中是 0.333… 一样,0.1 在二进制中是无限循环的。计算时只能截断,所以有了误差。

12.2.2.3 inf / -inf / nan 的处理

浮点数有三个特殊值:正无穷、负无穷、和不是一个数(NaN)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 无穷大
print(float('inf'))           # inf
print(float('-inf'))          # -inf
print(10 ** 1000)             # inf —— 超过 float 能表示的最大值

# 无穷大的运算
print(float('inf') + 1)       # inf
print(float('inf') + float('-inf'))  # nan —— 无穷加负无穷 = 不知道是啥
print(float('inf') - float('inf'))   # nan

# NaN(Not a Number)
print(float('nan'))           # nan
print(0.0 / 0.0)               # nan —— 0除以0
print(float('inf') - float('inf'))   # nan

# 判断是否是 nan —— 不能用 ==,要用 math.isnan()
import math
print(math.isnan(float('nan')))   # True
print(math.isnan(0.0 / 0.0))      # True

# 判断是否是有限或无穷
print(math.isfinite(1.0))         # True
print(math.isfinite(float('inf')))  # False
print(math.isfinite(float('nan'))) # False

⚠️ NaN 有一个诡异的特点:NaN 不等于任何东西,包括它自己!

1
2
3
4
import math
nan = float('nan')
print(nan == nan)          # False —— NaN 和谁都不等,包括它自己!
print(math.isnan(nan))     # True —— 所以判断 NaN 要用 isnan()

12.2.3 complex(复数)—— 进入数学的平行世界

Python 原生支持复数(complex number),妈妈再也不用担心我算不出方程的根了!

12.2.3.1 复数创建:3 + 4j

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 复数的创建:使用 j(不是 i!)
z = 3 + 4j
print(z)            # (3+4j)
print(type(z))      # <class 'complex'>

# 也可以用 complex() 函数
z2 = complex(3, 4)  # 等价于 3 + 4j
print(z2)           # (3+4j)

# 纯虚数
pure_img = 5j
print(pure_img)     # 5j

12.2.3.2 .real、.imag、conjugate()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
z = 3 + 4j

# 实部和虚部
print(z.real)      # 3.0 —— 实部
print(z.imag)      # 4.0 —— 虚部

# 共轭复数:实部不变,虚部取反
print(z.conjugate())   # (3-4j)

# 复数的模(绝对值)
print(abs(z))          # 5.0 —— sqrt(3^2 + 4^2) = 5

# 复数运算
z1 = 1 + 2j
z2 = 3 + 4j
print(z1 + z2)     # (4+6j)
print(z1 * z2)     # (-5+10j)
print(z1 / z2)     # (0.44+0.08j)

📐 复数在科学计算、信号处理、电气工程中非常有用。比如交流电路分析、傅里叶变换等,都离不开复数。

12.2.4 decimal(精确十进制)—— 财务人员的最爱

12.2.4.1 Decimal 使用场景(金融计算)

浮点数虽然快,但精度不够。对于金融计算,必须用 Decimal

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from decimal import Decimal

# 金融计算:发工资!
salary = Decimal('10000.00')
tax = Decimal('0.15')
bonus = Decimal('5000.00')

total = salary * (1 - tax) + bonus
print(total)   # 13000.00 —— 精确!

# 如果用 float 呢?
salary_f = 10000.00
tax_f = 0.15
bonus_f = 5000.00
total_f = salary_f * (1 - tax_f) + bonus_f
print(total_f)  # 12999.999999999998 —— 精度问题!

# 计算商品总价(复用上面的 Decimal)
price1 = Decimal('0.10')
price2 = Decimal('0.20')
print(price1 + price2)   # 0.30 —— 精确!
print(float(price1) + float(price2))  # 0.30000000000000004

💰 重要原则:涉及到钱的地方,永远用 Decimal!虽然 float 计算快,但如果算错账被老板叫去喝茶,那可就不好玩了。

12.2.4.2 精度控制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from decimal import Decimal, getcontext

# 设置精度(默认28位)
getcontext().prec = 10

a = Decimal('1') / Decimal('3')
print(a)   # 0.3333333333 —— 只有10位精度

# 量化(quantize)到指定小数位
from decimal import ROUND_HALF_UP
price = Decimal('19.99') * Decimal('3')
print(price.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 59.97 —— 四舍五入到分

12.2.5 fractions(分数)—— 数学老师的梦想

12.2.5.1 Fraction 类使用

如果你讨厌小数,用分数吧!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from fractions import Fraction

# 创建分数
f1 = Fraction(3, 4)     # 3/4
f2 = Fraction('5/7')   # 也可以用字符串
f3 = Fraction(0.5)     # 从浮点数创建(注意精度损失)
f4 = Fraction("0.5")   # 从字符串创建(精确)

print(f1)      # 3/4
print(f2)      # 5/7
print(f3)      # 1/2
print(f4)      # 1/2

# 分数运算 —— 结果还是分数!
result = Fraction(1, 3) + Fraction(1, 6)
print(result)  # 1/2 —— 自动约分!

# 解决数学题:1/3 + 1/6 = ?
from fractions import Fraction
print(Fraction(1, 3) + Fraction(1, 6))  # 1/2

# 浮点数转分数
import math
print(Fraction(math.pi).limit_denominator(1000))
# 355/113 —— 著名的圆周率近似分数,误差极小!

12.2.6 math 模块详解——数学瑞士军刀

math 模块是 Python 内置的数学工具箱,函数多得让人眼花缭乱!

12.2.6.1 数值函数(ceil、floor、sqrt、pow、exp、log 等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import math

# ceil(x):向上取整 —— 天花板
print(math.ceil(3.2))    # 4 —— 往大的取
print(math.ceil(-3.2))   # -3 —— 往大的取(-3 > -3.2)

# floor(x):向下取整 —— 地板
print(math.floor(3.9))   # 3 —— 往小的取
print(math.floor(-3.9))  # -4 —— 往小的取(-4 < -3.9)

# sqrt(x):平方根
print(math.sqrt(16))    # 4.0
print(math.sqrt(2))     # 1.4142135623730951

# pow(x, y):x的y次方(等价于 x ** y)
print(math.pow(2, 3))   # 8.0 —— 注意返回的是 float
print(2 ** 3)           # 8 —— 这个是 int

# exp(x):e的x次方
print(math.exp(1))      # 2.718281828459045 —— e ≈ 2.718

# log(x, base):对数,默认底数为 e(自然对数)
print(math.log(math.e))      # 1.0 —— ln(e) = 1
print(math.log(100, 10))     # 2.0 —— log10(100) = 2
print(math.log2(1024))       # 10.0 —— log2(1024) = 10
print(math.log10(1000))      # 3.0 —— log10(1000) = 3

# factorial(n):阶乘
print(math.factorial(5))   # 120 —— 5! = 5×4×3×2×1 = 120

# gcd(a, b):最大公约数
print(math.gcd(48, 18))    # 6 —— 48和18的最大公约数是6

# fabs(x):绝对值(返回 float)
print(math.fabs(-5))    # 5.0

# trunc(x):截断取整(向0取整)
print(math.trunc(3.9))    # 3 —— 和 floor 不同!
print(math.trunc(-3.9))   # -3 —— 向0取整

12.2.6.2 常量(pi、e、tau、inf、nan)

1
2
3
4
5
6
7
import math

print(math.pi)    # 3.141592653589793 —— 圆周率 π
print(math.e)     # 2.718281828459045 —— 自然常数 e
print(math.tau)   # 6.283185307179586 —— 2π,完整圆的弧度
print(math.inf)   # inf —— 无穷大
print(math.nan)   # nan —— 不是一个数

12.2.6.3 三角函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import math

# 角度转弧度
angle_deg = 45
angle_rad = math.radians(angle_deg)
print(f"{angle_deg}° = {angle_rad} rad")  # 45° = 0.7853981633974483 rad

# 弧度转角度
rad = math.pi / 4
deg = math.degrees(rad)
print(f"{rad} rad = {deg}°")  # 0.785... rad = 45.0°

# 三角函数
print(math.sin(angle_rad))    # 0.7071067811865476 —— sin(45°) ≈ √2/2
print(math.cos(angle_rad))    # 0.7071067811865476 —— cos(45°) ≈ √2/2
print(math.tan(angle_rad))    # 0.9999999999999999 —— tan(45°) ≈ 1

# 反三角函数
print(math.asin(0.5))    # 0.5235987755982989 —— arcsin(0.5) = π/6
print(math.acos(0.5))    # 1.0471975511965979 —— arccos(0.5) = π/3
print(math.atan(1))      # 0.7853981633974483 —— arctan(1) = π/4

# 双曲函数
print(math.sinh(1))    # 1.1752011936438014
print(math.cosh(1))    # 1.5430806348152437
print(math.tanh(1))    # 0.7615941559557649

12.2.7 random 模块详解——让程序学会掷骰子

“随机"是程序员的魔法——也是游戏开发、数据分析、机器学习的基石!

12.2.7.1 基本随机数(random、randint、uniform 等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import random

# random():返回 [0.0, 1.0) 区间内的随机浮点数
print(random.random())       # 0.721... 每次运行都不一样!

# uniform(a, b):返回 [a, b] 区间内的随机浮点数
print(random.uniform(1, 10)) # 5.374... 在1到10之间

# randint(a, b):返回 [a, b] 区间内的随机整数(两端都包含)
print(random.randint(1, 6))  # 4 —— 掷骰子!

# randrange(start, stop, step):返回随机选择的元素
print(random.randrange(0, 101, 2))  # 随机偶数,0到100之间

12.2.7.2 随机选择与打乱(choice、sample、shuffle 等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import random

# choice(seq):从序列中随机选一个元素
fruits = ["苹果", "香蕉", "橘子", "葡萄"]
print(random.choice(fruits))  # 随机选一个水果

# sample(population, k):从总体中随机选k个不重复的元素
deck = list(range(1, 53))  # 52张牌
hand = random.sample(deck, 5)  # 发5张
print(hand)  # [23, 45, 12, 7, 38] —— 5张不重复的牌

# shuffle(x):原地打乱序列(会修改原列表)
cards = ["红桃A", "黑桃K", "方块Q", "梅花J"]
random.shuffle(cards)  # 洗牌!
print(cards)  # 顺序被打乱了

# choices(population, weights=None, k=1):可重复选择
# 模拟有偏骰子:6出现的概率是其他数字的2倍
weights = [1, 1, 1, 1, 1, 2]  # 1-5各1份,6有2份
print(random.choices(range(1, 7), weights=weights, k=10))
# 可能输出类似 [3, 6, 1, 6, 4, 6, 2, 6, 5, 6]

12.2.7.3 随机分布(gauss、expovariate 等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import random

# gauss(mu, sigma):正态分布(高斯分布)
# mu = 均值,sigma = 标准差
height = random.gauss(170, 10)  # 平均身高170,标准差10
print(f"随机身高:{height:.1f} cm")

# expovariate(lambd):指数分布
# lambd = 1 / 平均值
arrival_time = random.expovariate(1/5)  # 平均5分钟来一辆车
print(f"随机等待时间:{arrival_time:.2f} 分钟")

# 其他分布
print(random.betavariate(2, 5))   # Beta分布
print(random.gammavariate(2, 2))  # Gamma分布
print(random.lognormvariate(0, 1)) # 对数正态分布
print(random.triangular(0, 1))    # 三角分布

12.2.7.4 随机种子

随机种子让随机数"可重现”——相同的种子产生相同的随机序列!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import random

# 设置种子
random.seed(42)
print(random.random())   # 0.6394267984578837
print(random.random())   # 0.02501075520032851
print(random.randint(1, 100))  # 81

# 重新设置种子,序列会重复
random.seed(42)
print(random.random())   # 0.6394267984578837 —— 和上面第一个一样!
print(random.random())   # 0.02501075520032851 —— 和上面第二个一样!
print(random.randint(1, 100))  # 81 —— 和上面第三个一样!

🎯 实战技巧:调试需要随机数但不想要随机结果时,设定种子!这样每次运行程序的行为都一致,方便排查问题。上线前记得把种子代码删掉或者改成基于时间!

12.2.8 statistics 模块——数据的统计大师

statistics 模块提供了一整套统计分析工具,是数据科学的好帮手!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import statistics

data = [2, 4, 4, 4, 5, 5, 7, 9]

# 均值(mean):所有数加起来除以个数
print(statistics.mean(data))       # 5.0 —— (2+4+4+4+5+5+7+9)/8 = 40/8 = 5

# 中位数(median):排序后位于中间的值
print(statistics.median(data))     # 4.5 —— 排序后 [2,4,4,4,5,5,7,9],中间是4和5,平均4.5
print(statistics.median([1, 2, 3]))  # 2 —— 奇数个直接取中间

# 众数(mode):出现次数最多的值
print(statistics.mode(data))       # 4 —— 4出现了3次,最多

# 方差(variance):衡量数据离散程度
print(statistics.variance(data))   # 4.0 —— 各数据与均值的差的平方的平均

# 标准差(stdev):方差的平方根,更直观
print(statistics.stdev(data))      # 2.0 —— √4 = 2

# 其他统计量
print(statistics.harmonic_mean(data))  # 调和平均数
print(statistics.geometric_mean(data)) # 几何平均数

12.3 序列类型——排队是有讲究的

12.3.1 list(列表)—— 最常用的可变序列

列表是 Python 中最最常用的数据结构!它就像一个可以随时增删改的购物清单。

12.3.1.1 列表创建([]、list()、列表推导式)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 方法1:直接用方括号
empty = []
numbers = [1, 2, 3, 4, 5]
mixed = ["hello", 42, 3.14, True]  # 列表可以装任意类型

# 方法2:用 list() 函数
another_list = list()           # 空列表
from_string = list("hello")     # ['h', 'e', 'l', 'l', 'o']
from_tuple = list((1, 2, 3))    # [1, 2, 3] —— 从元组创建
from_range = list(range(5))     # [0, 1, 2, 3, 4]

# 方法3:列表推导式(List Comprehension)—— Python的独门绝技!
squares = [x ** 2 for x in range(5)]    # [0, 1, 4, 9, 16]
evens = [x for x in range(10) if x % 2 == 0]  # [0, 2, 4, 6, 8]

12.3.1.2 索引访问与切片

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fruits = ["苹果", "香蕉", "橘子", "葡萄", "西瓜"]

# 正向索引:从0开始
print(fruits[0])   # 苹果 —— 第一个元素
print(fruits[1])   # 香蕉 —— 第二个元素

# 反向索引:从-1开始
print(fruits[-1])  # 西瓜 —— 最后一个
print(fruits[-2])  # 葡萄 —— 倒数第二个

# 切片:[start:stop:step]
print(fruits[1:4])     # ['香蕉', '橘子', '葡萄'] —— 索引1到3(不包括4)
print(fruits[:3])      # ['苹果', '香蕉', '橘子'] —— 从开头到索引2
print(fruits[2:])      # ['橘子', '葡萄', '西瓜'] —— 从索引2到结尾
print(fruits[::2])     # ['苹果', '橘子', '西瓜'] —— 每隔一个取一个
print(fruits[::-1])    # ['西瓜', '葡萄', '橘子', '香蕉', '苹果'] —— 反转!

# 切片是左闭右开区间 [start, stop)
print(fruits[1:2])  # ['香蕉'] —— 只取索引1,不包括索引2

💡 记住切片公式:list[start:stop:step],其中 start 包含,stop 不包含!

12.3.1.3 增删改操作(append、insert、pop、remove、del)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
nums = [1, 2, 3]

# 增
nums.append(4)      # 末尾添加:[1, 2, 3, 4]
print(nums)

nums.insert(1, 99)  # 在索引1位置插入99:[1, 99, 2, 3, 4]
print(nums)

nums.extend([5, 6])  # 末尾追加多个:[1, 99, 2, 3, 4, 5, 6]
print(nums)

# 改
nums[0] = 100       # 修改索引0的元素:[100, 99, 2, 3, 4, 5, 6]
print(nums)

# 删
nums.pop()          # 弹出最后一个:[100, 99, 2, 3, 4, 5],返回6
print(nums)

nums.pop(1)         # 弹出索引1的元素:[100, 2, 3, 4, 5],返回99
print(nums)

nums.remove(100)    # 删除第一个匹配的元素:[2, 3, 4, 5]
print(nums)

del nums[0]         # 删除索引0的元素:[3, 4, 5]
print(nums)

del nums[0:2]       # 删除切片:[5]
print(nums)

12.3.1.4 排序(sort、sorted、reverse)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
numbers = [3, 1, 4, 1, 5, 9, 2, 6]

# sort():原地排序(修改原列表)
numbers.sort()
print(numbers)  # [1, 1, 2, 3, 4, 5, 6, 9]

# sort(reverse=True):降序排列
numbers.sort(reverse=True)
print(numbers)  # [9, 6, 5, 4, 3, 2, 1, 1]

# sort(key=):自定义排序规则
words = ["banana", "apple", "Cherry", "date"]
words.sort(key=str.lower)  # 不区分大小写排序
print(words)  # ['apple', 'banana', 'Cherry', 'date']

# sorted():返回排序后的新列表,不修改原列表
numbers = [3, 1, 4, 1, 5]
sorted_nums = sorted(numbers)
print(numbers)      # [3, 1, 4, 1, 5] —— 原列表不变
print(sorted_nums)  # [1, 1, 3, 4, 5] —— 新列表

# reverse():原地反转
numbers.reverse()
print(numbers)  # [5, 4, 3, 1, 1]

12.3.1.5 拷贝(copy、[:]、deepcopy)

这是新手容易踩坑的地方!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 浅拷贝的问题
a = [[1, 2], [3, 4]]
b = a          # 这不是拷贝!b和a是同一个对象
b[0][0] = 99
print(a)  # [[99, 2], [3, 4]] —— a也被改了!
print(b)  # [[99, 2], [3, 4]]

# 正确的拷贝方式
a = [[1, 2], [3, 4]]
c = a.copy()    # 浅拷贝:外层列表是新的,但内层列表还是共享的!
d = a[:]        # 同上
c[0][0] = 99    # 修改内层元素——这会影响原列表 a!
print(a)  # [[99, 2], [3, 4]] —— a 也被影响了,因为浅拷贝共享内层对象!
print(c)  # [[99, 2], [3, 4]]

# 真正的深拷贝
import copy
a = [[1, 2], [3, 4]]
e = copy.deepcopy(a)  # 深拷贝:完全独立
e[0][0] = 99
print(a)  # [[1, 2], [3, 4]] —— 原列表不受影响!
print(e)  # [[99, 2], [3, 4]]

🎭 记忆口诀= 是"起别名"(共享对象),.copy() 是"浅拷贝"(表亲),deepcopy() 是"深拷贝"(完全独立)!

12.3.1.6 列表推导式详解

列表推导式是 Python 的语法糖,让代码更简洁、更 Pythonic!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 基本格式:[表达式 for 项目 in 可迭代对象]

# 传统写法
squares = []
for x in range(10):
    squares.append(x ** 2)
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 列表推导式写法
squares = [x ** 2 for x in range(10)]
print(squares)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# 带条件过滤
evens = [x for x in range(20) if x % 2 == 0]
print(evens)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# 多重循环
pairs = [(x, y) for x in [1, 2] for y in [3, 4]]
print(pairs)  # [(1, 3), (1, 4), (2, 3), (2, 4)]

# 带条件的多重循环
pairs_50 = [(x, y) for x in [1, 2, 3] for y in [3, 4, 5] if x + y > 5]
print(pairs_50)  # [(1, 5), (2, 4), (2, 5), (3, 4), (3, 5)]

# 字典推导式(类似的语法)
word = "hello"
char_count = {c: word.count(c) for c in set(word)}
print(char_count)  # {'h': 1, 'e': 1, 'l': 2, 'o': 1}

12.3.1.7 性能分析:append O(1)、insert O(n) 等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 时间复杂度分析
# O(1) - 常数时间:不管数据多大,操作时间不变
lst = []
lst.append(1)      # O(1) —— 追加很快

# O(n) - 线性时间:操作时间和数据量成正比
lst.insert(0, 1)   # O(n) —— 头部插入,需要移动所有元素

lst.pop()           # O(1) —— 尾部删除
lst.pop(0)          # O(n) —— 头部删除,需要移动所有元素

# O(1) 平均:访问、修改
lst[0] = 99         # O(1) —— 直接定位
val = lst[0]        # O(1) —— 直接访问

# 查找
print(1 in lst)     # O(n) —— 线性查找,需要遍历

# 排序
lst.sort()          # O(n log n) —— 最好的比较排序算法

性能建议:尾部操作多用 append()/pop(),避免频繁的 insert(0, x)pop(0)。如果需要频繁在头部操作,考虑用 collections.deque

12.3.2 tuple(元组)—— 不可变的序列

12.3.2.1 元组创建与索引

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 创建元组
empty = ()
single = (42,)  # 注意!单个元素的元组必须加逗号
multi = (1, 2, 3, "hello", True)

# 也可以不加括号(tuple unpacking 常见写法)
t = 1, 2, 3
print(t)  # (1, 2, 3)

# 用 tuple() 从其他序列创建
t_from_list = tuple([1, 2, 3])
t_from_str = tuple("hello")
print(t_from_str)  # ('h', 'e', 'l', 'l', 'o')

# 索引和切片(和列表一样)
t = (1, 2, 3, 4, 5)
print(t[0])      # 1 —— 正向索引
print(t[-1])     # 5 —— 反向索引
print(t[1:4])    # (2, 3, 4) —— 切片

12.3.2.2 元组不可变性的意义

元组一旦创建,内容不能修改

1
2
3
4
5
6
7
t = (1, 2, 3)
# t[0] = 99  # TypeError: 'tuple' object does not support item assignment

# 但如果元组里包含可变对象,那些可变对象可以修改!
t = (1, [2, 3], 4)
t[1].append(99)  # 合法!因为 list 是可变的
print(t)  # (1, [2, 3, 99], 4)

🛡️ 元组的不可变性有什么好处?

  1. 安全:作为字典的键或集合的元素时,不会被意外修改
  2. 性能:比列表更轻量,创建和存储更快
  3. 语义明确:向其他程序员声明"这些数据不应该被改变"

12.3.2.3 命名元组(namedtuple)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections import namedtuple

# 定义一个命名元组类型
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)

# 可以用索引访问
print(p[0])    # 10
print(p[1])    # 20

# 也可以用名字访问
print(p.x)    # 10 —— 更直观!
print(p.y)    # 20

# 可以像普通元组一样解包
x, y = p
print(x, y)   # 10 20

# 转换为字典
print(p._asdict())  # {'x': 10, 'y': 20}

# 替换某些字段(创建新实例)
p2 = p._replace(x=100)
print(p2)   # Point(x=100, y=20)

📍 命名元组的应用场景:当你要表示一个固定结构的数据(如坐标、RGB颜色、数据库记录)时,命名元组比普通元组更清晰,比类更轻量!

12.3.2.4 元组解包

元组解包是 Python 最优雅的特性之一!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 基本解包
coordinates = (10, 20, 30)
x, y, z = coordinates
print(x, y, z)  # 10 20 30

# 交换变量(不需要临时变量!)
a, b = 1, 2
a, b = b, a  # Pythonic 的交换方式
print(a, b)  # 2 1

# 用 * 收集多余的元素
first, *rest = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4, 5]

# 函数返回元组
def get_min_max(numbers):
    return min(numbers), max(numbers)

low, high = get_min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(f"最小值: {low}, 最大值: {high}")  # 最小值: 1, 最大值: 9

# 解包可以用于任何可迭代对象
*a, b = "hello"
print(a)  # ['h', 'e', 'l', 'l']
print(b)  # 'o'

12.3.2.5 tuple vs list 的选择

特性tuplelist
可变性不可变可变
性能更快、更省内存稍慢
用途固定数据、函数返回值动态数据
能否做字典键✅ 可以❌ 不行
能否做集合元素✅ 可以❌ 不行

🎯 选择原则:如果数据不应该被修改,用 tuple;否则用 list。就像"常量"用 tuple,“变量"用 list 一样!

12.3.3 range —— 惰性整数序列

12.3.3.1 range(start, stop, step)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# range(start, stop, step)
# 注意:是左闭右开区间 [start, stop)

r = range(0, 10)      # 0 到 9
r2 = range(5, 15)     # 5 到 14
r3 = range(0, 10, 2)  # 0, 2, 4, 6, 8 —— 偶数
r4 = range(10, 0, -1) # 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 —— 倒序

# 访问元素
print(r3[0])   # 0
print(r3[2])   # 4

# 判断是否在序列中
print(4 in r3)    # True
print(5 in r3)    # False

# 切片(返回新的 range)
print(r3[1:3])  # range(2, 6)

12.3.3.2 range 是惰性的(不占内存)

range 是一个惰性序列——它不会一次性生成所有数字,而是按需计算!

1
2
3
4
# range(0, 10**18) 在 C 层面实现,不占用大量内存
big_range = range(0, 10**18)
print(len(big_range))  # 1000000000000000000 —— 居然能算长度!
print(big_range[100])  # 100 —— 但只占用极少的内存

💾 为什么 range 这么省内存? 因为 range 存储的只是 startstopstep 三个参数,每次访问元素时动态计算。想象一下,如果 range(0, 10**18) 要生成所有数字,那得占用 800 PB 内存!但用惰性序列,就只需要几个字节。

12.3.3.3 range 与 list 互转

1
2
3
4
5
6
7
8
9
# range 转 list
r = range(5)
lst = list(r)
print(lst)  # [0, 1, 2, 3, 4]

# list 转 range —— 没那么直接,但可以用
lst = [0, 1, 2, 3, 4]
r = range(len(lst))
print(r)  # range(0, 5)

12.3.3.4 range 的性能优势

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import time

# 用 range 遍历
start = time.time()
for i in range(10000000):
    pass
end = time.time()
print(f"range 遍历: {end - start:.4f} 秒")

# 用 list 遍历
start = time.time()
for i in list(range(10000000)):
    pass
end = time.time()
print(f"list 遍历: {end - start:.4f} 秒")
# range 快得多,因为不需要预先创建大列表!

12.4 映射类型——键值对的艺术

12.4.1 dict(字典)—— Python 最核心的数据结构

字典是 Python 中最重要的数据结构!它用键值对存储数据,查找速度极快。

graph LR
    A["字典 d = {'a': 1, 'b': 2}"] --> B["键 'a'"]
    A --> C["键 'b'"]
    B --> D["值 1"]
    C --> E["值 2"]

12.4.1.1 字典创建({}、dict()、dict comprehension)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 方法1:花括号
d1 = {}
d2 = {"name": "张三", "age": 25}

# 方法2:dict() 函数
d3 = dict()  # 空字典
d4 = dict([("a", 1), ("b", 2)])  # 从键值对列表创建
d5 = dict(name="李四", age=30)   # 关键字参数

# 方法3:字典推导式
squares = {x: x**2 for x in range(5)}
print(squares)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

12.4.1.2 键值访问(d[“key”]、d.get(“key”))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
person = {"name": "小明", "age": 18, "city": "北京"}

# 方式1:中括号访问(键不存在会报错)
print(person["name"])   # 小明
# print(person["gender"])  # KeyError: 'gender'

# 方式2:get 方法(键不存在返回 None 或默认值)
print(person.get("name"))       # 小明
print(person.get("gender"))     # None —— 不报错!
print(person.get("gender", "未知"))  # 未知 —— 提供默认值

12.4.1.3 增删改操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
d = {"name": "张三"}

# 增
d["age"] = 25       # 直接添加新键值对
d.update({"city": "北京", "job": "工程师"})  # 批量添加
print(d)  # {'name': '张三', 'age': 25, 'city': '北京', 'job': '工程师'}

# 改
d["age"] = 26       # 修改现有键的值
print(d)  # {'name': '张三', 'age': 26, 'city': '北京', 'job': '工程师'}

# 删
del d["job"]        # 删除指定键值对
print(d)  # {'name': '张三', 'age': 26, 'city': '北京'}

popped = d.pop("city")  # 弹出并返回被删除的值
print(popped)   # 北京
print(d)  # {'name': '张三', 'age': 26}

d.clear()        # 清空字典
print(d)  # {}

12.4.1.4 视图对象(keys、values、items)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
d = {"a": 1, "b": 2, "c": 3}

# keys():所有键
print(d.keys())    # dict_keys(['a', 'b', 'c'])

# values():所有值
print(d.values())  # dict_values([1, 2, 3])

# items():所有键值对
print(d.items())   # dict_items([('a', 1), ('b', 2), ('c', 3)])

# 视图对象是动态的——字典变,它们也跟着变
d["d"] = 4
print(d.keys())    # dict_keys(['a', 'b', 'c', 'd']) —— 自动更新!

# 可以遍历
for key in d.keys():
    print(f"{key}: {d[key]}")

for key, value in d.items():
    print(f"{key}: {value}")

12.4.1.5 字典的哈希性要求

字典的必须是可哈希的(hashable)——即不可变类型!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 可哈希的类型(可以作为字典的键)
valid_keys = {
    42: "整数键",
    "hello": "字符串键",
    (1, 2): "元组键",  # 元组不可变,所以可以
    3.14: "浮点数键",
    True: "布尔键",
}

# 不可哈希的类型(不能作为字典的键)
# 列表是可变的,所以不能作为键
# d = {[1, 2]: "列表键"}  # TypeError: unhashable type: 'list'

# 但是,如果列表在元组里,就可以!
d = {([1, 2],): "嵌套列表的元组键"}
print(d)  # {([1, 2],): '嵌套列表的元组键'}

🏷️ **哈希(Hash)**是什么?哈希是一种将任意长度的数据转换为固定长度值的函数。Python 用哈希快速定位键的位置,所以查找速度才能达到 O(1)!

12.4.1.6 字典推导式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 数字平方的字典
squares = {x: x**2 for x in range(5)}
print(squares)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 字符串长度字典
words = ["apple", "banana", "cherry"]
lengths = {w: len(w) for w in words}
print(lengths)  # {'apple': 5, 'banana': 6, 'cherry': 6}

# 条件过滤
nums = [1, 2, 3, 4, 5, 6]
evens_sq = {x: x**2 for x in nums if x % 2 == 0}
print(evens_sq)  # {2: 4, 4: 16, 6: 36}

12.4.1.7 合并字典(| 和 |=,Python 3.9+)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Python 3.9+ 的字典合并运算符
d1 = {"a": 1, "b": 2}
d2 = {"b": 3, "c": 4}

# | 运算符:合并成新字典
merged = d1 | d2
print(merged)  # {'a': 1, 'b': 3, 'c': 4} —— 注意:d2 的值覆盖了 d1

# |= 运算符:就地扩展
d1 |= d2
print(d1)  # {'a': 1, 'b': 3, 'c': 4}

12.4.1.8 性能分析:访问/设置平均 O(1)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 字典的操作复杂度
d = {}

# O(1) 操作
d["key"] = "value"    # 设置:常数时间
val = d["key"]        # 访问:常数时间
d.get("key")          # 获取:常数时间
d.pop("key")          # 删除:常数时间
"key" in d            # 成员检查:常数时间

# 遍历是 O(n)
for key in d:         # O(n)
    pass

为什么字典这么快? 因为 Python 字典底层是哈希表。当你访问键时,Python 先计算键的哈希值,然后用哈希值直接找到存储位置,不需要遍历整个字典!

12.4.1.9 字典遍历顺序(Python 3.7+ 保证按插入顺序)

1
2
3
4
5
6
7
8
9
# Python 3.7+,字典保持插入顺序
d = {}
d["z"] = 1
d["a"] = 2
d["m"] = 3

for key in d:
    print(key, end=" ")
print()  # 输出: z a m —— 插入顺序!

12.4.2 defaultdict(默认值字典)

12.4.2.1 default_factory 参数

当访问不存在的键时,defaultdict 会自动创建一个默认值!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from collections import defaultdict

# 创建一个默认值是 int(0)的字典
dd_int = defaultdict(int)
dd_int["a"] += 1  # 不需要先检查键是否存在
print(dd_int["a"])  # 1
print(dd_int["b"])  # 0 —— 自动创建,默认值是 int 的默认值(0)

# 创建一个默认值是 list 的字典
dd_list = defaultdict(list)
dd_list["fruits"].append("apple")
dd_list["fruits"].append("banana")
dd_list["colors"].append("red")
print(dd_list)  # defaultdict(<class 'list'>, {'fruits': ['apple', 'banana'], 'colors': ['red']})

# 创建一个默认值是 dict 的字典(二级字典)
dd_dict = defaultdict(dict)
dd_dict["user1"]["name"] = "张三"
print(dd_dict)  # defaultdict(<class 'dict'>, {'user1': {'name': '张三'}})

12.4.2.2 与 dict.setdefault() 的对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 传统 dict 的写法
d = {}
if "key" not in d:
    d["key"] = []
d["key"].append(1)

# 使用 setdefault
d = {}
d.setdefault("key", []).append(1)

# defaultdict 的写法(最简洁)
from collections import defaultdict
d = defaultdict(list)
d["key"].append(1)  # 自动创建空列表

🏆 推荐defaultdictsetdefault() 更直观、更易读!

12.4.3 OrderedDict(有序字典)

12.4.3.1 Python 3.7+ 字典已保证顺序

在 Python 3.7+ 中,普通字典已经保证了插入顺序,所以 OrderedDict 的主要优势已经不那么明显了!

1
2
3
4
5
6
# Python 3.7+ 的普通字典
d = {}
d["z"] = 1
d["a"] = 2
d["b"] = 3
print(list(d.keys()))  # ['z', 'a', 'b'] —— 有序!

12.4.3.2 move_to_end() 的用处

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from collections import OrderedDict

od = OrderedDict({"a": 1, "b": 2, "c": 3})

# 移动键到末尾
od.move_to_end("a")
print(list(od.keys()))  # ['b', 'c', 'a']

# 移动键到开头
od.move_to_end("c", last=False)
print(list(od.keys()))  # ['c', 'b', 'a']

# 适用场景:需要频繁调整顺序时

12.4.4 Counter(计数器)

12.4.4.1 most_common(n)、elements()、运算操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from collections import Counter

# 统计单词出现次数
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counter = Counter(words)
print(counter)  # Counter({'apple': 3, 'banana': 2, 'cherry': 1})

# most_common(n):返回出现次数最多的 n 个元素
print(counter.most_common(2))  # [('apple', 3), ('banana', 2)]

# elements():返回所有元素(重复出现次数次)
print(list(counter.elements()))
# ['apple', 'apple', 'apple', 'banana', 'banana', 'cherry']

# 计数器运算
c1 = Counter(a=3, b=1)
c2 = Counter(a=1, b=2)
print(c1 + c2)   # Counter({'a': 4, 'b': 3}) —— 加法
print(c1 - c2)   # Counter({'a': 2}) —— 减法(只保留正数)
print(c1 & c2)   # Counter({'a': 1, 'b': 1}) —— 交集(取较小值)
print(c1 | c2)   # Counter({'a': 3, 'b': 2}) —— 并集(取较大值)

12.4.5 ChainMap(字典链)

12.4.5.1 多个字典链式访问

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections import ChainMap

# 把多个字典"链"在一起
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 100, "c": 3}
dict3 = {"d": 4}

# ChainMap 按顺序查找
chain = ChainMap(dict1, dict2, dict3)
print(chain["a"])   # 1 —— 只在 dict1 中找到
print(chain["b"])   # 2 —— dict1 中的 b,优先!
print(chain["c"])   # 3 —— dict2 中的 c
print(chain["d"])   # 4 —— dict3 中的 d

# 修改只影响第一个字典
chain["a"] = 999
print(dict1)  # {'a': 999, 'b': 2}

# 适用场景:合并多个配置字典(如命令行参数 + 默认配置)
defaults = {"theme": "light", "language": "zh"}
user_config = {"theme": "dark"}
config = ChainMap(user_config, defaults)
print(config["theme"])   # dark —— 用户配置优先
print(config["language"])  # zh —— 回退到默认配置

12.5 集合类型——去重和找交集的神器

12.5.1 set(可变集合)

集合是无序的、元素不重复的数据结构。就像去参加派对,每个人都是独一无二的!

12.5.1.1 集合创建({}、set())

1
2
3
4
5
6
7
8
9
# 注意:创建空集合不能用 {},那会创建字典!
empty_set = set()          # 空集合
numbers = {1, 2, 3, 4, 5}  # 非空集合

# 从其他序列创建
from_list = set([1, 2, 2, 3, 3, 3])  # {1, 2, 3} —— 自动去重!
from_string = set("hello")  # {'h', 'e', 'l', 'o'} —— l只出现一次
print(from_list)   # {1, 2, 3}
print(from_string) # {'h', 'e', 'l', 'o'}

12.5.1.2 增删操作(add、remove、discard、pop)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
s = {1, 2, 3}

# 增
s.add(4)           # 添加单个元素
print(s)  # {1, 2, 3, 4}

s.update([5, 6, 7])  # 添加多个元素
print(s)  # {1, 2, 3, 4, 5, 6, 7}

# 删
s.remove(3)        # 删除指定元素,元素不存在会报错 KeyError
print(s)  # {1, 2, 4, 5, 6, 7}

s.discard(100)    # 删除指定元素,元素不存在也不会报错
s.pop()           # 随机删除并返回一个元素
print(s)

12.5.1.3 集合运算(&、|、-、^)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

# 交集(&):两个集合都有的元素
print(a & b)   # {3, 4}

# 并集(|):所有元素
print(a | b)   # {1, 2, 3, 4, 5, 6}

# 差集(-):a有但b没有的元素
print(a - b)   # {1, 2}
print(b - a)   # {5, 6}

# 对称差集(^):只在其中一个集合中出现的元素
print(a ^ b)   # {1, 2, 5, 6}

12.5.1.4 集合关系判断(issubset、issuperset、isdisjoint)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
a = {1, 2, 3}
b = {1, 2, 3, 4, 5}
c = {6, 7, 8}

# issubset():a 是否是 b 的子集
print(a.issubset(b))    # True —— a 的所有元素都在 b 中
print(b.issubset(a))    # False

# issuperset():a 是否是 b 的超集
print(b.issuperset(a))  # True —— b 包含 a 的所有元素
print(a.issuperset(b))  # False

# isdisjoint():a 和 b 是否没有交集
print(a.isdisjoint(c))  # True —— 完全没有共同元素
print(a.isdisjoint(b))  # False —— 有交集

12.5.1.5 集合推导式

1
2
3
4
5
6
7
# 基本语法:{表达式 for 项目 in 可迭代对象}
squares = {x ** 2 for x in range(10)}
print(squares)  # {0, 1, 64, 4, 36, 9, 16, 49, 81, 25}

# 带条件
evens = {x for x in range(20) if x % 2 == 0}
print(evens)  # {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}

12.5.1.6 性能分析:成员检查 O(1)

1
2
3
4
5
6
7
# 集合的成员检查是 O(1) —— 极快!
s = set(range(1000000))
print(999999 in s)   # True —— 瞬间完成!

# 列表的成员检查是 O(n) —— 慢!
lst = list(range(1000000))
print(999999 in lst)  # True —— 需要遍历整个列表!

经验法则:如果要频繁检查"某个元素在不在集合里”,用 set 而不是 list!性能差距是指数级的!

12.5.2 frozenset(不可变集合)

12.5.2.1 可作为字典的键

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# frozenset 是不可变的集合,可以作为字典的键!
fs1 = frozenset([1, 2, 3])
fs2 = frozenset([2, 3, 4])

# 作为字典的键
d = {fs1: "集合1", fs2: "集合2"}
print(d)  # {frozenset({1, 2, 3}): '集合1', frozenset({2, 3, 4}): '集合2'}

# 作为另一个集合的元素
set_of_frozensets = {frozenset([1, 2]), frozenset([3, 4])}
print(set_of_frozensets)

12.5.2.2 与 set 的区别

操作setfrozenset
可变✅ 可变❌ 不可变
可哈希❌ 不可哈希✅ 可哈希
可作为字典键❌ 不行✅ 可以
可作为集合元素❌ 不行✅ 可以
添加/删除元素✅ 可以❌ 不行

12.6 其他内置类型

12.6.1 bytes 与 bytearray

bytesbytearray 都是字节序列,用于处理二进制数据。

12.6.1.1 bytes:不可变字节序列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# bytes 是不可变的字节序列
b = b"hello"
print(b)            # b'hello'
print(type(b))      # <class 'bytes'>
print(len(b))       # 5
print(b[0])          # 104 —— 字节值(不是字符'h')

# 从其他类型创建
b1 = bytes([104, 101, 108, 108, 111])  # 从整数列表
b2 = b"hello".replace(b"l", b"L")      # 使用 bytes 方法
b3 = "中文".encode("utf-8")            # 字符串编码

# bytes 不能修改
# b[0] = 72  # TypeError: 'bytes' object does not support item assignment

12.6.1.2 bytearray:可变字节序列

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# bytearray 是可变的字节序列
ba = bytearray(b"hello")
print(ba)            # bytearray(b'hello')
ba[0] = 72           # 可以修改!h -> H
print(ba)            # bytearray(b'Hello')

ba.extend(b" world")  # 可以追加
print(ba)            # bytearray(b'Hello world')

ba.append(33)        # 可以追加单个字节
print(ba)            # bytearray(b'Hello world!')

12.6.1.3 bytes 与 str 的转换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# str -> bytes:编码(encode)
s = "hello 你好"
b = s.encode("utf-8")
print(b)  # b'hello \xe4\xbd\xa0\xe5\xa5\xbd' —— UTF-8编码的字节

# bytes -> str:解码(decode)
b = b'hello \xe4\xbd\xa0\xe5\xa5\xbd'
s = b.decode("utf-8")
print(s)  # hello 你好

# 十六进制表示
b = bytes.fromhex("e4 bd a0 e5 a5 bd")
print(b)        # b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(b.hex())  # e4bda0e5a5bd

📡 什么时候用 bytes? 网络编程、文件读写(图片、音频等二进制文件)、加密操作等场景,都需要用 bytes 处理原始二进制数据!

12.6.2 type 与 isinstance

12.6.2.1 type() 查看类型

1
2
3
4
5
6
7
8
print(type(42))          # <class 'int'>
print(type(3.14))        # <class 'float'>
print(type("hello"))     # <class 'str'>
print(type([1, 2]))      # <class 'list'>
print(type((1, 2)))      # <class 'tuple'>
print(type({"a": 1}))    # <class 'dict'>
print(type({1, 2}))      # <class 'set'>
print(type(None))        # <class 'NoneType'>

12.6.2.2 isinstance() 类型检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# isinstance() 是最推荐的类型检查方式
print(isinstance(42, int))          # True
print(isinstance(3.14, float))       # True
print(isinstance("hello", str))      # True
print(isinstance([1, 2], list))      # True
print(isinstance((1, 2), tuple))     # True

# isinstance 支持元组——检查是否是多种类型之一
print(isinstance(42, (int, float)))  # True
print(isinstance("hello", (int, str)))  # True

12.6.2.3 type() vs isinstance() 的区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# type() 严格匹配类型
print(type(True) == bool)      # True
print(type(True) == int)       # False —— bool 不是 int 的子类?

# isinstance() 考虑继承关系
print(isinstance(True, bool))  # True
print(isinstance(True, int))    # True —— bool 是 int 的子类!
print(issubclass(bool, int))   # True —— 在 Python 中,bool 是 int 的子类

# 在 type() 中,使用 == 比较类型
# isinstance() 中使用 isinstance() 函数判断

🎯 推荐用 isinstance() 而不是 type() ==:因为 isinstance() 会考虑继承关系,更符合多态的思想。比如一个 bool 值既是 bool 类型,也是 int 类型(因为 boolint 的子类)!

12.6.3 切片对象 slice

12.6.3.1 s = slice(start, stop, step)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 创建一个切片对象
s = slice(1, 5, 2)  # 从索引1到5,步长2

# 用于序列切片
lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(lst[s])  # [1, 3] —— 取 lst[1] 和 lst[3]

# 多维切片
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
s_rows = slice(0, 2)    # 前两行
s_cols = slice(1, 3)    # 后两列
print(matrix[s_rows][s_cols])  # [[2, 3], [5, 6]]

# 用在字典上
d = {"a": 1, "b": 2, "c": 3, "d": 4}
keys = ["a", "b", "c"]
s = slice(2)
print({k: d[k] for k in keys[s]})  # {'a': 1, 'b': 2}

12.7 文件操作——让数据永久保存

文件操作是 Python 编程中最重要的技能之一!数据存在内存里,关机就没了;存到文件里,才能永久保存。

12.7.1 文件打开与关闭

12.7.1.1 open() 函数:open(file, mode=‘r’, encoding=‘utf-8’)

1
2
3
4
5
6
7
8
9
# 基本语法
f = open("test.txt", mode="r", encoding="utf-8")
# file: 文件路径
# mode: 打开模式
# encoding: 字符编码(文本模式时使用)

# 读取文件内容
content = f.read()
f.close()  # 记得关闭!

12.7.1.2 模式:r / w / a / x / b / t / +

模式含义文件不存在指针位置
r只读报错开头
w只写创建开头(清空文件)
a追加创建末尾
x创建并写报错(如果存在)开头
b二进制模式--
t文本模式(默认)--
+读写模式--
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 文本模式
f = open("text.txt", "r")   # 只读
f = open("text.txt", "w")   # 只写(覆盖)
f = open("text.txt", "a")   # 追加

# 二进制模式(用于图片、音频等)
f = open("image.png", "rb")   # 读取二进制
f = open("output.png", "wb")  # 写入二进制

# 组合模式
f = open("file.txt", "r+")   # 读写,指针在开头
f = open("file.txt", "w+")   # 读写(先清空)
f = open("file.txt", "a+")   # 读写,指针在末尾

12.7.1.3 编码:utf-8 / gbk / latin-1

1
2
3
4
5
6
7
8
# 推荐使用 UTF-8(万国码)
f = open("file.txt", "r", encoding="utf-8")

# 中文 Windows 可能默认用 gbk
f = open("file.txt", "r", encoding="gbk")

# latin-1 用于 Latin-1 字符(西欧语言)
f = open("file.txt", "r", encoding="latin-1")

💡 编码小知识:UTF-8 是最通用的编码,能表示世界上所有语言的字符。写代码时,永远用 encoding='utf-8',别问为什么,问就是血的教训!

12.7.1.4 with 语句(自动关闭)

最佳实践:永远用 with 语句打开文件,它会自动关闭文件!

1
2
3
4
5
6
7
8
9
# 手动方式(容易忘记关闭)
f = open("test.txt", "r")
content = f.read()
f.close()  # 忘记关就麻烦了!

# with 语句(推荐!)
with open("test.txt", "r", encoding="utf-8") as f:
    content = f.read()
# 文件在这里自动关闭,即使发生异常也会关闭!

🛡️ with 语句就像一个贴心的管家——你用完文件后,它会自动帮你收拾残局。不用手动 close(),不用担心异常导致文件没关闭!

12.7.2 文件读取

1
2
with open("sample.txt", "w", encoding="utf-8") as f:
    f.write("第一行\n第二行\n第三行\n第四行\n")

12.7.2.1 read():读取全部

1
2
3
4
5
6
7
8
with open("sample.txt", "r", encoding="utf-8") as f:
    content = f.read()  # 读取整个文件
    print(content)

# 指定读取字符数
with open("sample.txt", "r", encoding="utf-8") as f:
    chunk = f.read(5)  # 只读5个字符
    print(chunk)  # 第一行

12.7.2.2 readline():读取一行

1
2
3
4
5
6
with open("sample.txt", "r", encoding="utf-8") as f:
    line1 = f.readline()
    print(line1)  # 第一行(包含换行符)

    line2 = f.readline()
    print(line2)  # 第二行

12.7.2.3 readlines():读取所有行

1
2
3
4
5
6
7
8
9
with open("sample.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()
    print(lines)
    # ['第一行\n', '第二行\n', '第三行\n', '第四行\n']

# 去掉换行符
with open("sample.txt", "r", encoding="utf-8") as f:
    lines = [line.strip() for line in f.readlines()]
    print(lines)  # ['第一行', '第二行', '第三行', '第四行']

12.7.2.4 迭代读取:for line in f

1
2
3
4
5
6
7
8
# 最推荐的读取方式——逐行迭代
with open("sample.txt", "r", encoding="utf-8") as f:
    for line in f:
        print(line.strip())
# 第一行
# 第二行
# 第三行
# 第四行

🎯 推荐方式:大多数情况下,直接用 for line in f 迭代读取就行!它一行一行读,不会一次性把整个文件加载到内存,适合大文件!

12.7.3 文件写入

12.7.3.1 write():写入字符串

1
2
3
4
with open("output.txt", "w", encoding="utf-8") as f:
    f.write("Hello, world!\n")  # 写入字符串(注意手动加换行)
    f.write("第二行\n")
    f.write("第三行\n")

12.7.3.2 writelines():写入多行

1
2
3
4
lines = ["第一行\n", "第二行\n", "第三行\n"]

with open("output.txt", "w", encoding="utf-8") as f:
    f.writelines(lines)  # 批量写入

12.7.3.3 print() 输出到文件

1
2
3
with open("output.txt", "w", encoding="utf-8") as f:
    print("Hello", file=f)  # print 默认输出到屏幕,加上 file=f 就输出到文件
    print("World", file=f)

12.7.4 文件指针

文件指针就像读书时的手指头,告诉你当前读到哪里了

12.7.4.1 tell():当前位置

1
2
3
4
with open("sample.txt", "r", encoding="utf-8") as f:
    print(f.read(5))   # 读5个字符
    pos = f.tell()      # 获取当前位置
    print(f"当前指针位置: {pos}")  # 5(UTF-8下一个中文占3字节)

12.7.4.2 seek():移动指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
with open("sample.txt", "r", encoding="utf-8") as f:
    # seek(offset, whence)
    # whence: 0=文件开头, 1=当前位置, 2=文件末尾

    f.read(10)  # 先读一些

    f.seek(0)  # 回到文件开头
    print(f.read(5))

    f.seek(0, 2)  # 跳到文件末尾
    print(f.read())  # 读不到任何东西(末尾之后)

    f.seek(0)  # 回到开头

12.7.5 pathlib 面向对象路径

pathlib 是 Python 3.4+ 引入的面向对象的路径操作库,比传统的 os.path 更直观!

12.7.5.1 Path() 创建路径对象

1
2
3
4
5
6
7
8
9
from pathlib import Path

# Windows 路径
p = Path("C:/Users/longx/Documents")
print(p)  # C:\Users\longx\Documents

# 相对路径
p = Path(".") / "docs" / "chapter12.md"
print(p)  # docs\chapter12.md

12.7.5.2 路径拼接:Path() / / operator

1
2
3
4
5
6
7
8
9
from pathlib import Path

# 用 / 运算符拼接路径(跨平台!)
base = Path("C:/Users")
docs = base / "Documents" / "Python" / "chapter12.md"
print(docs)  # C:\Users\Documents\Python\chapter12.md

# 也可以用 .joinpath()
docs = base.joinpath("Documents", "Python", "chapter12.md")

12.7.5.3 读取写入:read_text() / write_text()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pathlib import Path

# 写入文件
p = Path("test.txt")
p.write_text("Hello, pathlib!\n第二行", encoding="utf-8")

# 读取文件
content = p.read_text(encoding="utf-8")
print(content)

# 二进制读写用 read_bytes() / write_bytes()
p.write_bytes(b"binary data")
data = p.read_bytes()

12.7.5.4 路径检查:exists() / is_file() / is_dir()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pathlib import Path

p = Path("test.txt")
p.write_text("hello", encoding="utf-8")

print(p.exists())    # True —— 文件存在
print(p.is_file())   # True —— 是文件
print(p.is_dir())    # False —— 不是目录

d = Path(".")
print(d.exists())    # True
print(d.is_dir())    # True
print(d.is_file())   # False

12.7.5.5 目录操作:mkdir() / rmdir() / iterdir()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pathlib import Path

# 创建目录
d = Path("new_folder")
d.mkdir(exist_ok=True)  # exist_ok=True 表示已存在也不报错

# 列出目录内容
for item in d.iterdir():
    print(item.name)

# 递归创建(父目录不存在也一起创建)
d = Path("a/b/c/d")
d.mkdir(parents=True, exist_ok=True)

# 删除目录(必须是空目录)
d.rmdir()

# 删除文件
p = Path("test.txt")
p.unlink()  # 删除文件
# p.rmdir()  # 只能删除空目录

12.7.6 shutil 高级文件操作

shutil 是 Python 的高级文件操作库,提供复制、移动、删除等功能。

12.7.6.1 copy() / copy2() / copyfile()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import shutil
from pathlib import Path

# 确保目录存在
Path("test_folder").mkdir(exist_ok=True)

# copy():复制文件(保留权限,不保留元数据)
shutil.copy("source.txt", "test_folder/dest.txt")

# copy2():复制文件并尽可能保留元数据(修改时间等)
shutil.copy2("source.txt", "test_folder/dest2.txt")

# copyfile():只复制文件内容,不复制权限和元数据
shutil.copyfile("source.txt", "test_folder/dest3.txt")

📋 copy vs copy2:如果你在乎文件的"修改时间"等元数据,用 copy2();如果只在乎内容,用 copy()

12.7.6.2 move() / rename()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import shutil
from pathlib import Path

# move():移动文件或目录(相当于剪切+粘贴)
shutil.move("old_location.txt", "new_location.txt")

# rename():重命名(在同一目录)
p = Path("old_name.txt")
p.rename("new_name.txt")

# 如果目标在不同目录,效果等同于移动
p = Path("file.txt")
shutil.move("file.txt", "subfolder/file.txt")

12.7.6.3 remove() / rmtree()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import shutil
from pathlib import Path

# remove():删除文件
p = Path("to_delete.txt")
p.unlink()  # 或者用 os.remove()
# p.remove()  # pathlib 没有 remove 方法,只有 unlink

# rmtree():递归删除目录(删除整个目录树)
shutil.rmtree("danger_folder")  # 危险!整个文件夹都没了!

⚠️ rmtree 危险警告:这个操作不可逆!删除前请三思!建议先检查目录是否存在,或者先备份!


本章小结

本章我们全面探索了 Python 的数据类型宇宙,现在让我们来总结一下核心要点:

📌 NoneType

  • None 表示"空"或"不存在",不等于 0""[]{}
  • 常用于函数默认参数、返回值、以及表示"未知"状态

📌 数值类型

  • int:任意精度整数,不会溢出,是 Python 的"数学超能力"
  • float:IEEE 754 双精度浮点数,有精度问题(0.1 + 0.2 != 0.3
  • complex:复数类型,3 + 4j,有 .real.imagconjugate()
  • Decimal:精确十进制,用于金融计算
  • Fraction:分数类型,精确表示分数

📌 math / random / statistics

  • math 模块提供数学函数(ceilfloorsqrtlog 等)和常量(pie
  • random 模块用于生成随机数,设置种子可以重现结果
  • statistics 模块提供统计函数(meanmedianmodestdev

📌 序列类型

  • list:可变序列,最常用的数据结构,append() O(1),insert() O(n)
  • tuple:不可变序列,可作为字典键,支持解包和命名元组
  • range:惰性序列,不占内存,适合遍历

📌 映射类型

  • dict:键值对,O(1) 访问/查找,Python 3.7+ 保证插入顺序
  • defaultdict:访问不存在的键时自动创建默认值
  • Counter:计数器,most_common() 找最高频元素
  • ChainMap:多个字典链式访问

📌 集合类型

  • set:无序不重复,&|-^ 集合运算,O(1) 成员检查
  • frozenset:不可变集合,可作为字典键

📌 其他内置类型

  • bytes:不可变字节序列,bytearray:可变字节序列
  • type() vs isinstance():后者考虑继承关系,更推荐

📌 文件操作

  • with open() 自动管理文件关闭
  • pathlib.Path 是现代推荐的面向对象路径操作
  • shutil 提供高级文件操作(复制、移动、删除)

🎉 恭喜你完成了数据类型的学习! 现在你已经是 Python 数据的"老司机"了!当然,Python 的世界还有更多精彩等待你去探索(比如类、装饰器、异步编程等),但数据类型是基础中的基础,必须扎扎实实地掌握!继续加油!

最后修改 April 9, 2026: 新增 Philosophy 教程 (0a13837)