Python高级——变量本质、对象模型与核心数据结构
核心主线只有一句话:Python 里"变量不是盒子,而是贴在对象上的标签"。
全文从对象模型切入,先讲清变量、共享引用、垃圾回收这些"底层认知",再铺开数值、None、字符串三大基础类型,接着是列表/元组/集合/字典四大内置数据结构与序列协议,最后落到文件 IO 和四个综合案例。把这套吃透,你写代码时脑子里就有了一张"内存里到底发生了什么"的地图。
一、先建立核心认知:一切皆对象,变量只是标签
Python 脚本里一切都是对象。对象在内存中,是"数值"和"操作"的集合。一个 Python 程序从大到小是:模块 > 语句 > 表达式 > 对象。
这条认知不建立,后面所有"为什么值是这样变的"都会想不通。
1. 对象的内部结构
每个对象除了存值,还有两个标准头部信息:
- 类型标志符(type designator):标识这个对象是什么类型。
- 引用计数器(reference counter):记录当前有多少变量名指向它,决定何时回收。
关键推论:类型属于对象,不属于变量。变量本身没有类型。
2. 变量是怎么"创建"的
a = 3
Python 执行了三件事:
- 创建一个整数对象
3。 - 若变量
a尚未存在,创建一个名为a的变量。 - 把变量
a和新对象3连接起来(贴标签)。
变量出现在表达式里时,会被它引用的对象替换。所以变量使用前必须赋值,否则报 NameError。
3. 共享引用:两个变量指向同一个对象
a = 3
b = a
此时 a 和 b 指向同一个整数对象 3。这叫共享引用(shared reference)。
修改时的坑——看下面这个经典例子:
a = 3
b = a
a = a + 2
a + 2 的结果是存进新对象 5,a 被重新贴到 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 的本质:它就是数值
bool 是 int 的子类,True 对应 1,False 对应 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 <= c;5 == 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-string(Python 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) == 2、round(3.5) == 4。涉及浮点比较用math.isclose(a, b)。
小结:数字是类型集合;bool 即数值;NaN/inf 用于边界与缺失值;
//向下取整、逻辑短路、round银行家舍入是三大易错点。
三、None:被低估的"空值"
None 是 NoneType 类型的唯一实例,表示"没有值/空值/缺失值",不是 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 风险。
总结
- 变量是标签不是盒子——类型属于对象,共享引用 + 可变/不可变决定了值的传播行为。
- 数值/None/字符串三大基础类型,记住
//向下取整、逻辑短路、None is None、str不可变。 - 列表/元组/集合/字典四大结构,按"可变?有序?"选型;深浅拷贝是共享引用的最大坑。
- 序列协议让你对一切有序容器举一反三。
- 文件 + pickle 解决持久化,但 pickle 有安全风险。
- 四个综合案例把以上全部串成真实系统。
一句话收尾:对象模型是根,数据结构是骨,案例是肉——根骨清楚了,写什么都稳。
更多推荐
所有评论(0)