我决定从安装完Python、打开编辑器敲下第一行代码那天说起。当时我给自己定的计划很简单每天记一点笔记把数据存储和运算这两个最基础的底座吃透。结果一学才发现这两块内容看着不难水却深得很——a 1这行代码背后发生了什么0.1 0.2为什么不是0.3列表切片为什么能倒着取位运算到底有什么用这些问题每一个拎出来都能写一篇笔记。折腾了半个多月踩了无数坑终于把这块知识体系理清了。这篇就把我的自学笔记整理出来重点围绕数据存储、变量机制、数值精度、运算符优先级和位运算这些核心内容展开适合刚入门想系统梳理基础的人也适合学了一阵子但总觉得哪里囫囵吞枣的朋友。保证都是实操验证过的东西不整虚的。1. 先搞清楚变量到底是怎么“存数据”的1.1 变量是标签不是盒子绝大多数Python教程开篇就会教你写a 1然后告诉你“把整数1赋值给变量a”。问题就出在这个“赋值”上——很多人会把变量理解成一个盒子把1装进去。这个理解在Java、C这种语言里勉强说得通但在Python里是错的。Python里的变量本质上是“贴在对象上的标签”。当你写a 1时实际发生的事情是先在内存里创建一个整数对象1然后把标签a贴到这个对象上。如果再写b a并不是把对象复制一份给b而是把同一个对象的另一个标签b也贴上去。所以此时a和b指向的是同一个内存对象。我用一个直观的方式验证过a 1 b a print(id(a), id(b)) # 两个id完全一致 a 2 print(b) # 依然是1这里很多人会困惑既然a和b指向同一个对象为什么改了ab不变关键就在于a 2这行代码——它不是“修改了原来那个1”而是“创建了一个新的整数对象2并把标签a从1上撕下来贴到2上”。原来的1还在内存里只是没人贴它了标签b还挂在它上面。这个理解为什么重要因为后续学变量作用域、参数传递、可变对象修改时如果还用“盒子模型”思考百分之百要栽跟头。我在学函数传参时就深有体会后面我还会详细讲这个坑。1.2 小整数缓存与“引用”导致的连环坑Python有个机制叫“小整数缓存”-5到256之间的整数在解释器启动时会预先创建好所有代码引用这些数字时都指向同一个对象。所以你会发现a 100 b 100 print(id(a) id(b)) # True指向同一个缓存对象 c 300 d 300 print(id(c) id(d)) # False超过缓存范围就各自创建对象这个细节看起来冷门但直接在“变量值相同但id不同”这类问题里会碰见。还有更实际的坑——可变对象的引用共享。刚入门时我用列表做过一个矩阵初始化matrix [[0] * 3] * 3 print(matrix) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] matrix[0][0] 5 print(matrix) # [[5, 0, 0], [5, 0, 0], [5, 0, 0]]当时我直接懵了改一个位置全列都变。原因就是[0] * 3先创建了一个长度为3的列表对象外层* 3复制的是这个列表对象的引用而不是内容。内存里只有一份[0, 0, 0]外面三个位置全是指向它的指针。正确做法是matrix [[0] * 3 for _ in range(3)]这个错误在力扣刷题、构建邻接矩阵时特别常见我的建议是涉及嵌套列表构造一律用列表推导式不要用乘法。2. 数据存储的底层细节整数、浮点数、字符串到底是什么2.1 整数是对象浮点数有精度陷阱Python的整数是任意精度对象理论上可以无限大——只要内存够。这和C语言的int是完全不同的概念所以Python里做超大数运算不用担心溢出print(2 ** 100)一张图解释就是Python的整数在内存里是根据实际位数分配空间的存多少位就占多少字节。但这个特点也意味着整数运算比C语言慢做大规模数值计算时比如量化交易回测、矩阵乘法纯Python循环会非常吃力这就是后面我要引入numpy的核心理由。浮点数就更坑了。0.1 0.2的结果是0.30000000000000004我相信每个自学者都会遇到。原因在于计算机用二进制表示浮点数而0.1和0.2的二进制是无限循环小数必须截断存储。这不是Python的问题——Python遵循的是IEEE 754浮点标准Java、C全都一样谁也不能精确表示0.1。我在实际操作中试过几种方案# 方案一round圆整注意第二个参数是保留几位小数 print(round(0.1 0.2, 2)) # 0.3 # 方案二Decimal精确十进制 from decimal import Decimal print(Decimal(0.1) Decimal(0.2)) # 0.3 # 方案三比较时用误差范围 a 0.1 0.2 print(abs(a - 0.3) 1e-9) # True金融计算、涉及钱的场景优先用Decimal但注意要传字符串直接传0.1这种浮点字面量进去Decimal接收的其实已经是失真后的二进制值。2.2 字符串和字节串文本在内存里的模样字符串在Python里是Unicode序列每个字符对应一个码点。存储上Python为了兼顾性能和内存做了内部优化——像ASCII可表示的字符串在内部会用紧凑的单字节表示只有出现中文等宽字符时才会切换到多字节表示。这也是为什么有人会觉得“中文占几个字节”的问题答案不确定。除了str还有bytes类型。写入文件、网络传输、处理二进制数据时都是bytes两者不能混淆。我写爬虫解析网页时就经常需要text 你好 b text.encode(utf-8) print(type(b), len(b)) # class bytes 6每个汉字utf-8占3字节 back b.decode(utf-8) print(back) # 你好这块的坑在于编码格式不匹配导致的UnicodeDecodeError尤其是从网上拉的旧数据用GBK编码而系统默认UTF-8时。处理外部数据永远要先确认编码乱码不是玄学就是编码和解码规则不一致导致的。2.3 结构化数据容器怎么选列表、元组、字典、集合这四个内置容器我一开始觉得就是“装东西的”用了几个月才琢磨出各自的适用场景列表有序可变的序列适合保持插入顺序、相同类型的数据。元组有序不可变适合做字典的键、函数返回多值时打包、常量数据等。字典键值映射适合按名字取数据查找速度是O(1)。集合无序不重复适合去重、交集并集运算。一个重要特点是元组的不可变是“引用层面的不可变”。元组里存一个列表列表内容还是能改t (1, [2, 3]) t[1].append(4) print(t) # (1, [2, 3, 4])有人说这是个bug其实不是这是Python语义的体现。选择容器类型时要基于“是否允许修改内容”和“查找方式”来做判断而不是看哪个顺手用哪个。3. 运算规则从加减乘除到位运算把这些优先级吃透3.1 表达式运算的优先级和结合性初学者在写判断语句时丢括号是家常便饭。Python里运算符优先级从高到低大致是**指数运算*///%-然后是位移运算符接着是^|最后是比较、赋值等。最容易被忽略的是**是右结合的2 ** 3 ** 2等于2 ** 9而不是(2 ** 3) ** 2print(2 ** 3 ** 2) # 512 print((2 ** 3) ** 2) # 64这类反直觉的地方我的经验是绝不依赖记忆优先级长表达式全部加括号。这不是保守而是过一两个月回头读代码加括号的版本能省大量时间。还有连比比较运算在Python里的特殊语义x 5 print(1 x 10) # True等价于 1 x and x 10这种写法很优雅但新手容易读歪。另外注意and和or的短路0 and 5结果是0因为左侧为假就直接返回左侧0 or 5结果是5。利用短路可以写默认值name user_input or 默认名3.2 位运算从布尔代数到按位或赋值位运算这块内容自学的人经常跳过因为日常业务开发很少直接用到。但一旦接触低层概念——网络掩码、权限系统、状态标记、图形像素操作、CTF题目——位运算就是绕不开的工具。先记住四张表。与只有同为1才为1或|只要有1就为1异或^不同为1取反~0变1、1变0。移位和把二进制位整体左移或右移左移相当于乘以2的n次方右移相当于整除2的n次方print(5 2) # 205*2^220 print(5 1) # 25整除22在权限系统设计里可以用位来管理多个开关。这是我写过的一个小例子READ 1 # 0b001 WRITE 2 # 0b010 EXEC 4 # 0b100 # 给用户授予读和写的权限 permission READ | WRITE # 0b011 # 判断是否有执行权限 if permission EXEC: print(有执行权限) else: print(无执行权限)按位或赋值|就更有用了。它和的语义类似就是把permission permission | WRITE简写为permission | WRITE。在状态积累场景里是神器多个条件判断中每满足一个就用flag | 某值把对应位置1最后统一检查。经典的用法是LeetCode第78题求子集用1 i枚举所有组合或者在做相邻矩阵、图的可达性计算时用位压缩状态能成倍节省内存。我当时在信息论作业里用位运算处理过8个布尔特征的所有组合状态比用布尔列表快得多也省得多。3.3 浮点数运算的几个性能注意点Python的浮点数运算是基于double类型的精度有限速度比纯整数运算略慢但远不如循环慢。真正要关心的不是单个运算是多快而是“在循环里不要做多余运算”。我做列表求和时对比过两个写法import time data [float(i) for i in range(100000)] # 写法一库函数 start time.time() s sum(data) print(time.time() - start) # 约0.003秒 # 写法二手写循环 s 0 start time.time() for x in data: s x print(time.time() - start) # 约0.012秒内置函数sum是用C实现的性能上秒杀手写循环。这个笔记给我的启发是Python的性能优化核心不是怎么写循环而是把循环交给内置函数或库去做。4. 实操笔记切片、邻接矩阵、小工具实战4.1 列表和数组切片的各种姿势切片是Python的招牌语法[start:stop:step]三个参数左闭右开。很多人背了“含头不含尾”规则但换到负数就乱套。我整理了一张速查表表达式结果说明lst[1:4]第1到第3个元素不包含索引4lst[:3]前3个元素start省略为0lst[3:]第3个到末尾stop省略为末尾lst[::-1]反转列表step为-1从右往左lst[::2]每隔一个取一个步长为2lst[-3:]最后3个元素负数从末尾数切片不修改原列表它会创建新列表。对大型列表频繁切片会额外分配内存这是性能隐患。如果只需要读数据用itertools.islice更省内存如果确实要改原列表直接在原位置赋值lst [1, 2, 3, 4, 5] lst[1:4] [20, 30] print(lst) # [1, 20, 30, 5]注意切片赋值的长度不需要和原切片一致它可以扩张或压缩列表。字符串和元组也支持切片语法它们返回新的字符串和元组。4.2 用Python构建邻接矩阵邻接矩阵是图论的经典存储结构数据结构课程里常讲。我自学时用最基础的方法手写了一个顺便把数据类型和运算都串起来了。用一个二维列表表示图结构假设我们有5个节点、若干条边n 5 edges [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)] # 初始化全零矩阵 adj [[0] * n for _ in range(n)] # 填充边 for u, v in edges: adj[u][v] 1 adj[v][u] 1 # 无向图 for row in adj: print(row)输出结果[0, 1, 0, 0, 1] [1, 0, 1, 0, 0] [0, 1, 0, 1, 0] [0, 0, 1, 0, 1] [1, 0, 0, 1, 0]这里有两个要点一是初始化用推导式避开共享引用问题1.2节讲过二是无向图的矩阵是对称的双向都置1。如果是带权图把1改成权重即可用0或无穷大表示不可达。如果数据量上来比如几千个节点纯Python嵌套列表的读写效率就很拉胯了。这时候上numpy才是正路import numpy as np adj np.zeros((n, n), dtypeint) for u, v in edges: adj[u][v] 1 adj[v][u] 1 # 计算每个节点的度数 degree adj.sum(axis1) print(degree)numpy的数组存储是连续内存向量化运算比Python列表快一个量级。学数据结构和算法时用numpy做邻接矩阵的矩阵乘法、计算可达性、求最短路径都非常顺手。这也是我在热词里看到“numpy构建邻接矩阵”“python矩阵”就想展开说说的原因。4.3 用位运算写一个状态开关的小工具学完位运算后我写了第一个有实际用途的小脚本——一个简化版权限检查工具。需求场景一个命令行小系统不同的用户操作对应不同的标记位管理员可以组合授予权限。# 权限定义 PERM_READ 1 # 0b001 PERM_WRITE 2 # 0b010 PERM_DELETE 4 # 0b100 PERM_EXECUTE 8 # 0b1000 class PermissionSystem: def __init__(self, user_perms0): self.user_perms user_perms def grant(self, perm): self.user_perms | perm # 按位或赋值 def revoke(self, perm): self.user_perms ~perm # 取反再与 def has(self, perm): return (self.user_perms perm) perm def show(self): print(f二进制: {self.user_perms:08b}, 十进制: {self.user_perms}) user PermissionSystem() user.grant(PERM_READ) user.grant(PERM_WRITE) user.show() # 二进制: 00000011 print(user.has(PERM_WRITE)) # True user.revoke(PERM_WRITE) user.show() # 二进制: 00000001这段代码把位运算的几种操作全用到了|加权限、检查权限、 ~撤销权限。比起用多个布尔变量这种方式通过一个整数就能存所有权限状态且检查逻辑极其简洁。数据库里存权限也是用类似的方式存成整型字段。5. 自学踩坑实录与排查思路5.1 新手真的会犯的经典错误我在学习过程中踩过不少坑这里挑几个有代表性的分享都是真实发生过的。第一个操作符优先级翻车。我写过if a b 0这样的逻辑原意是判断“a和b都是0”结果Python实际执行的是a (b 0)因为比较操作符优先级高于位运算。这种bug极难排查因为代码看着没问题。解决方案只有一个位运算和比较运算混合时全部用括号。第二个浮点数直接比较。我在做成绩及格判断时写过if score 60.0刚好成绩是60.1减某个小量算出来的结果明明应该相等却不相等。后来改成if abs(score - 60.0) 0.0001就一切正常了。第三个循环里改正在遍历的列表。我想从列表里删除所有偶数nums [1, 2, 3, 4, 5, 6] for num in nums: if num % 2 0: nums.remove(num) print(nums) # [1, 3, 5]——居然对再试别的数据换成[1, 2, 4, 5, 6]输出变成[1, 4, 5]删第二个数4时跳过了一个元素。因为删除元素后列表索引会前移循环的迭代游标还在原位。标准做法是用新列表暂存或者倒序遍历。第四个可变默认参数。我写过def add_item(item, cache[]): cache.append(item); return cache第二次调用时发现cache里残留上一次的数据。因为默认参数只在函数定义时创建一次。改成def add_item(item, cacheNone): if cache is None: cache []这个问题就解决了。5.2 排查思路先验证类型再验证值我总结出来的一个排查套路报错时先看类型再讲逻辑。Python是动态类型语言很多运行时错误是类型不匹配引起的。比如热词里提到的python连接cmd、winusb、cv2这类问题安装库时经常遇到“要安装缺失的节点请先在你的python环境中运行pip install -u --pre comfyui-m”这种提示本质就是环境依赖不完整。我当时装OpenCV的时候踩过同样的坑。排查步骤一般是用pip list查看已安装包版本。用python -c import 模块名测试单个库是否能正常导入。检查是否盯着多个Python环境在装——系统Python、Anaconda、虚拟环境三套环境各自装各自的包管不过来。最好统一用虚拟环境venv或conda管理新项目新建环境不污染全局。在调试变量值的时候我推荐两个思路一种是print()流式输出定位但现在更推荐用pdb设置断点或者用IDE的调试面板一步步看变量变化。第一次用断点看那个列表删除的bug时立刻就知道问题是游标和索引错位比瞎试快太多。5.3 数据验证的启动习惯不管写什么数据处理脚本现在的我都会在开头加一段“输入数据自检”。比如写一个处理学生成绩的脚本先写scores [85, 92, 78, 60, 55] # 自检1没有空值 assert all(isinstance(s, (int, float)) for s in scores), 存在非数值数据 # 自检2范围合理 assert all(0 s 100 for s in scores), 成绩范围异常 # 自检3长度合理 assert len(scores) len(set(scores)) or True, 提示信息这种自检代码看似增加了几行实际能节省大量后期排查时间。数据科学项目里读入的CSV文件经常有空值、脏数据不检查就往下算算出来的结果全是错的还以为是算法问题。数据清洗永远优先于数据处理这是自学半年后最大的体会。6. 工具链与后续扩展方向6.1 IDE和环境配置建议我在热词里看到大量“python安装教程”“vscode python环境配置”“python环境变量配置”相关词说明环境问题是新手的一座大山。这里我给个简洁明了的行动方案Python官网下载安装包安装时务必勾选Add Python to PATH这个勾选能避免后续环境变量配置的一堆麻烦。编辑器用VS Code安装Python扩展后按CtrlShiftP调出命令面板选Python: Select Interpreter选择你装好的那个Python解释器。包管理工具使用pip。pip换源以后下载速度快很多比如用清华或阿里云的镜像源文件里写上index-url即可。每个项目尽量用虚拟环境隔离依赖用python -m venv venv创建然后在VS Code里选中它作为解释器。这套流程我已经用烂了也帮同事配过基本二十分钟能搞定。6.2 numpy和数据结构下一个必学模块如果把这篇笔记总结成一句话那就是数据存储解决“数据长什么样、存在哪”运算解决“数据能怎么变”。搞清楚这两块的底层机制后后续学numpy、pandas、机器学习都会顺畅很多。numpy数组和Python列表最核心的区别就是存储方式列表存的是对象的引用内存不连续numpy数组存的是连续的同类型数值配合向量化计算速度能提升数十倍。我在热词里看到“python构建邻接矩阵”“python矩阵0”相关词如果要做网状数据、矩阵运算强烈建议直接用numpy。我近期准备专门为numpy的存储模型写一篇笔记——为什么ndarray的切片是视图而不是复制、广播机制到底怎么工作、np.array和np.asarray的区别等等。目前这些内容还在整理实验代码阶段。7. 几个自学者最容易忽略的细节7.1 内存的释放与引用计数Python有垃圾回收机制但这不代表你可以完全不管内存。我在循环中处理大量数据时会发现内存占用只增不减——因为每轮循环创建的对象都有引用链条指向它们直到函数结束才被回收。手动释放的办法del large_list # 删除引用更主动的是用gc.collect()强制回收但这个调用有开销不推荐频繁执行。另一个常见问题是循环中追加数据到列表不加以控制例如爬虫写数据时结果列表越攒越大几百MB内存就这么没了。此时应该分块处理或者边抓边写入磁盘。7.2is和不要混用比较的是值是否相等is比较的是两者是否指向同一个内存对象。初学者很容易犯这个错a 100 b 100 a is b # True小整数缓存 a 1000 b 1000 a is b # False超范围的新对象判断是否为空、是否为None官方推荐用is None。但判断两个列表内容是否一致必须用。这个坑我在做单测比对结果时踩了一次从那次起就养成了“比较内容用、比较身份用is”的习惯。7.3 内置函数和标准库是你的武器库真正动手写代码后我发现很多需求标准库已经提供了好用的工具不需要自己造轮子collections.defaultdict字典默认值避免KeyError。collections.Counter一行代码统计频率。itertools.groupby分组处理数据。functools.lru_cache递归记忆化缓存刷题利器。random、math、os、sys常用的底层设施。我写数据处理脚本时的习惯是先过一遍标准库有哪些相关模块再看看第三方库最后才自己实现逻辑。有时候你觉得麻烦往往是因为还没找到合适的工具。写在最后的一点个人体会学Python最大的好处是反馈极快——写几行代码就能看到结果这种即时正反馈特别适合自学者保持动力。但基础的数据存储和运算部分恰恰是这种“快反馈”掩盖了理解深度的地方。你确实能把a 1写出来但只有理解了标签机制、不可变语义、浮点精度陷阱、位运算的用途后面学函数、学类、学算法时才能举一反三不靠死记硬背。我现在回看自己最初写的笔记大部分内容现在都已内化成直觉。这篇算是系列的第一篇后续还会写关于函数传参、文件读写和numpy存储模型的笔记。如果非要给各位正在自学的人一个建议每学一个新概念就拿debugger断点看一眼内存里的对象行为每写一个脚本就在开头加数据自检。这两个习惯比多写一百行代码都管用。