核心主线只有一句话:Python 里"变量不是盒子,而是贴在对象上的标签"。

全文从对象模型切入,先讲清变量、共享引用、垃圾回收这些"底层认知",再铺开数值、None、字符串三大基础类型,接着是列表/元组/集合/字典四大内置数据结构与序列协议,最后落到文件 IO 和四个综合案例。把这套吃透,你写代码时脑子里就有了一张"内存里到底发生了什么"的地图。


一、先建立核心认知:一切皆对象,变量只是标签

Python 脚本里一切都是对象。对象在内存中,是"数值"和"操作"的集合。一个 Python 程序从大到小是:模块 > 语句 > 表达式 > 对象。

这条认知不建立,后面所有"为什么值是这样变的"都会想不通。

1. 对象的内部结构

每个对象除了存值,还有两个标准头部信息:

  • 类型标志符(type designator):标识这个对象是什么类型。
  • 引用计数器(reference counter):记录当前有多少变量名指向它,决定何时回收。

关键推论:类型属于对象,不属于变量。变量本身没有类型。

2. 变量是怎么"创建"的

a = 3

Python 执行了三件事:

  1. 创建一个整数对象 3
  2. 若变量 a 尚未存在,创建一个名为 a 的变量。
  3. 把变量 a 和新对象 3 连接起来(贴标签)。

变量出现在表达式里时,会被它引用的对象替换。所以变量使用前必须赋值,否则报 NameError

3. 共享引用:两个变量指向同一个对象

a = 3
b = a

此时 ab 指向同一个整数对象 3。这叫共享引用(shared reference)

修改时的坑——看下面这个经典例子:

a = 3
b = a
a = a + 2

a + 2 的结果是存进新对象 5a 被重新贴到 5 上,b 仍指向 3。这说明:数字是不可变的,你永远改不了对象 3 的值,所谓的"修改"都是创建新对象再重新贴标签。

但可变对象就地修改时,共享引用会让你"莫名其妙中招":

L1 = [2, 3, 4]
L2 = L1
print(L2)       # [2, 3, 4]
L1[0] = 24
print(L2)       # [24, 3, 4]  ← L2 也变了!

采用**不变模式(immutable)**设计的对象,所有"修改"都靠创建新对象完成,包括:数字、布尔值、None、字符串、元组、冻结集合、range。可变对象(列表、字典、集合)才支持就地修改。

4. 相等:== 还是 is

  • == 比较是否相等。
  • is 比较身份(内存中是不是同一个对象)。
list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 == list2)   # True(值相等)
print(list1 is list2)   # False(不是同一个对象)

两个关于"同一性"的进阶细节:

  • 小整数缓存[-5, 256] 范围内的整数会被缓存,多次赋相同值会引用同一个对象。
    a = 3
    b = 3
    print(a is b)   # True
    
  • 常量折叠:同一代码块内相同的大整数常量,编译成字节码时可能被合并成同一个对象(交互环境每次输入是独立代码块,不会折叠)。

5. 垃圾回收:引用计数归零即回收

a = 3
a = 'Hello'

a 被赋予新对象 Hello,旧对象 3 若没有其它变量引用,其引用计数器归零,内存被自动回收。这就是垃圾回收(GC)

小结:变量是标签不是盒子;可变/不可变决定了"改一个会不会牵连另一个";== 看值、is 看身份;引用计数归零自动回收。


二、数值类型:不止整数和浮点

Python 里数字不只是一种类型,而是一组相似类型的集合:整数、浮点数、复数、固定精度小数(Decimal)、分数(Fraction)、布尔值。

1. 创建数字

字面量最常用,部分特殊类型用内置函数:

n = 9999999999999999999   # 整数,无大小上限
f = 3.14e-10              # 浮点数,双精度
c = 1 + 2j                # 复数,一对浮点数实现
b = 0b1010                # 二进制 10;0o123 八进制;0x123 十六进制
from fractions import Fraction
frac = Fraction(1, 3)     # 分数 1/3

2. bool 的本质:它就是数值

boolint 的子类,True 对应 1False 对应 0,只是重写了显示逻辑:

print(True + 5)    # 6
print(False + 3)   # 3

假值清单(可直接当 False 用,也能被 bool() 转成 False):0""[]{}None

3. 两个特殊浮点值:NaN 与 inf

这两个 IEEE 754 特殊值,做数据分析/算法边界时经常碰到。

NaN(Not a Number)——表示缺失或未定义的浮点值:

x = float('nan')
print(x == x)        # False!NaN 不等于任何值,包括自己
import math
print(math.isnan(x)) # True,判断 NaN 要用这个

inf(infinity)——表示无穷:

import math
inf = math.inf
print(inf + 100)     # inf
print(inf - inf)     # nan(无穷减无穷无意义)
print(1 / inf)       # 0.0
print(math.isinf(inf))  # True,判断要用 isinf 而非 ==

实战用途:求最值前用 max_val = -float('inf')min_val = float('inf') 初始化;图算法(如 Dijkstra)用 inf 表示两点不可达。注意 inf 不能转整数,会抛 OverflowError

4. 四类运算

类别 要点
算术 //向下取整5 // -2 == -3,不是向零);** 幂运算
比较 支持链式 a < b <= c5 == 5.0 为真(自动类型转换)
逻辑 and/or 短路求值and 返回第一个假值或最后值,or 返回第一个真值或最后值
位运算 仅整数;~x 等价于 -(x+1);左移乘 2ⁿ、右移整除 2ⁿ

逻辑运算的短路很有用:

port = config.get('port') or 8080      # 取默认值
value = obj and obj.child and obj.child.value  # 安全嵌套访问

5. 类型转换与格式化

运算只能在相同类型间发生:类型不同则自动向上转型(简单类型转复杂类型,不丢精度,如 int + float → float)。手动转换用 int()/float()/complex()/bool()/str()/bin()/oct()/hex()

格式化首选 f-stringPython 3.6+,编译期优化最快):

print(f"{1234567.89:,.2f}")   # 1,234,567.89
print(f"{0.8512:.1%}")         # 85.1%
print(f"{255:#012x}")          # 0x00000000ff

注意 round() 用的是银行家舍入(四舍六入五成双):round(2.5) == 2round(3.5) == 4。涉及浮点比较用 math.isclose(a, b)

小结:数字是类型集合;bool 即数值;NaN/inf 用于边界与缺失值;// 向下取整、逻辑短路、round 银行家舍入是三大易错点。


三、None:被低估的"空值"

NoneNoneType 类型的唯一实例,表示"没有值/空值/缺失值",不是 0、不是空串、不是 False

常见用法

result = None          # 1. 先声明后赋值
def greet():
    print("Hi")
print(greet())         # 2. 无 return 的函数默认返回 None

def log(msg, when=None):
    if when is None:    # 3. 可选参数的哨兵值
        print("No time")

三个误区

print(None == False)   # False,None 不是布尔值
print(None == "")      # False,也不是空串/空列表
print(x is None)       # ✅ 推荐:用 is 判断
print(x == None)       # ❌ 不推荐

因为 None 是单例(全局只有一个),判断身份用 is 而非 ==


四、字符串:不可变的字符序列

Python 3 里字符串默认 Unicode。三类文本相关类型:str(文本)、bytes(二进制)、bytearray(可修改的字节数组)。

1. 字面量与转义

s1 = 'abc'             # 单引号
s2 = "abc"             # 双引号,可嵌单引号
s3 = '''多行
字符串'''               # 三引号,保留换行
path = r'C:\new\path'  # 原始字符串 r"",反斜杠按字面处理,常用于路径/正则

常用转义:\n 换行、\t 制表、\\ 反斜杠、\u4e0b / \U00004e0b Unicode 码点、\N{SNOWMAN} 按名称取字符。

2. 序列操作与编解码

字符串是字符序列,支持索引、切片、重复,但不可变(不能改单个字符):

s = 'abcdefg'
print(s[::-1])         # gfedcba,倒序
b = '你好'.encode('utf-8')   # str -> bytes
print(b.decode('utf-8'))     # 你好,bytes -> str

print(ord('四'))       # 22235,字符 -> 编码
print(chr(22235))      # 四,编码 -> 字符
print(int('1001', 2))  # 9,二进制串转整数

3. 常用方法

s = 'spaam'
print(s.find('aa'))          # 2,查找
print(s.replace('aa', 'xx')) # spxxm,替换(返回新串)
print('-'.join(list('spxm')))# s-p-x-m,合并
print('a b c'.split())       # ['a', 'b', 'c'],劈开

4. 格式化

str.format() 与现代 f-string 都支持字段名、索引、字典/列表索引、对齐与进制:

temp = '学 {tech},好工作;就读 {school}'
print(temp.format(tech='IT', school='课工场'))
print(f"{'spam':>10} = {123.4567:<10}")   # 右对齐/左对齐
print(f"{255:b}, {255:o}, {255:X}")       # 二进制/八/十六

字符串不可变,所有"修改"方法(replace/split 等)都返回新对象;encode/decode 是文本与二进制的桥梁。


五、四大内置数据结构

这是 part2-variable 最重的一块。先记住一张总表,再逐个拆。

结构 可变? 有序? 典型用途
列表 list 有序可变集合,最常用的容器
元组 tuple 不可变记录、函数多返回值、dict 键
集合 set 去重、成员检测、集合运算
字典 dict ✅(键不可变) ✅(3.7+ 保序) 键值映射,查找 O(1)

1. 列表 list:可变序列

增删改查齐全,重点说拷贝推导

L = [1, 2, 3]
L.append(4)          # 追加
L.extend([5, 6])     # 追加集合
L.insert(0, 0)       # 插入

# 拷贝:引用赋值不是克隆!
A = L
B = L[:]             # 浅拷贝(切片)
import copy
C = copy.deepcopy(L) # 深拷贝(嵌套对象完全独立)

L.sort(reverse=True) # 原地排序
L.reverse()          # 原地反转

列表推导式是 Python 的标志性写法:

squares = [x*x for x in range(10)]        # 0~9 的平方
evens = [x*x for x in range(10) if x % 2 == 0]  # 过滤偶数后的平方

浅拷贝只复制外层,嵌套的可变对象仍共享;有嵌套结构务必用 deepcopy

2. 元组 tuple:不可变记录

t = (1, 'a', None)
single = (42,)    # ⚠️ 单元素元组必须加逗号,否则 (42) 只是数字 42
empty = ()

元组不可变,但若包含可变对象(如列表),其内容可被修改。元组因不可变可作字典键:

d = {(1, 2): 'point'}

3. 集合 set:去重与集合运算

s = {1, 2, 3, 2}   # {1, 2, 3},自动去重
s.add(4)
s.remove(9)        # 不存在会 KeyError
s.discard(9)       # 不存在不报错

# 数学运算
a | b    # 并集
a & b    # 交集
a - b    # 差集
a ^ b    # 对称差集
a.issubset(b)      # 是否子集
a.isdisjoint(b)    # 是否无交集

frozenset 是不可变集合,可作为字典键:

fs = frozenset({1, 2, 3})
# fs.add(4)  # AttributeError

空集合只能用 set(),写 {} 得到的是空字典。

4. 字典 dict:键值映射

d = {'name': 'Alice', 'age': 30}
print(d.get('sex', 'unknown'))  # 带默认值查询,避免 KeyError
d.setdefault('age', 18)         # 键不存在才设
d.update({'age': 31})           # 合并,后者覆盖前者

# 字典推导式 + zip
keys = ['a', 'b']
vals = [1, 2]
d2 = {k: v for k, v in zip(keys, vals)}   # {'a': 1, 'b': 2}

遍历三种姿势:

for k in d: ...        # 遍历键
for v in d.values(): ...
for k, v in d.items(): ...   # 遍历键值对

5. 序列协议:通用操作的底层逻辑

列表、元组、字符串、range 都遵循**序列(sequence)**协议,共享一套通用操作:索引、切片、长度 len()、成员检测 in、连接(仅同类型)、重复 *

理解"序列协议"就能举一反三:为什么字符串和列表都能切片、in、下标访问——因为它们都是序列。

小结:可变与否决定能否就地改;深浅拷贝是共享引用的最大坑;列表推导/字典推导是 Pythonic 核心;集合做去重与关系运算最快。


六、文件与对象持久化

1. 文本 vs 二进制

  • 文本模式:内容表示为 str,自动做 Unicode 编解码。
  • 二进制模式:以原始 bytes 处理,不做任何转换。

2. 读写与上下文管理器

with 是文件操作的最佳实践,自动关闭句柄,避免资源泄漏:

# 读
with open('test.txt', 'r', encoding='utf-8') as f:
    content = f.read()        # 一次读入
    # line = f.readline()    # 逐行,返回空串表示结束

# 写 / 追加
with open('out.txt', 'w', encoding='utf-8') as f:
    f.write('hello')

# 复制文件
with open('test.txt', 'r', encoding='utf-8') as src, \
     open('copy.txt', 'w', encoding='utf-8') as dst:
    dst.write(src.read())

常用模式:r 读、w 写(覆盖)、a 追加、b 二进制、+ 读写。

3. pickle:对象序列化

把复杂对象直接存盘,再原样读回:

import pickle
data = {'users': [1, 2, 3], 'config': None}
with open('data.pkl', 'wb') as f:
    pickle.dump(data, f)

with open('data.pkl', 'rb') as f:
    restored = pickle.load(f)   # 原样恢复

⚠️ 安全警告:绝不对来自网络或不可信来源的 .pkl 调用 pickle.load(),反序列化会执行其中的任意代码,是典型 RCE 风险。


总结

  1. 变量是标签不是盒子——类型属于对象,共享引用 + 可变/不可变决定了值的传播行为。
  2. 数值/None/字符串三大基础类型,记住 // 向下取整、逻辑短路、None is Nonestr 不可变。
  3. 列表/元组/集合/字典四大结构,按"可变?有序?"选型;深浅拷贝是共享引用的最大坑。
  4. 序列协议让你对一切有序容器举一反三。
  5. 文件 + pickle 解决持久化,但 pickle 有安全风险。
  6. 四个综合案例把以上全部串成真实系统。

一句话收尾:对象模型是根,数据结构是骨,案例是肉——根骨清楚了,写什么都稳。


更多推荐