Python 3.12 核心语法实战从数据类型到流程控制《Python 实战》基础篇第二篇。承接上篇搭好的虚拟环境这一篇我把 Python 3.12 的核心语法一个个敲一遍所有输出都是服务器真实回显。语法书能讲清楚规则但坑只有真跑过才知道长在哪儿。环境说明本篇实验全部在上一篇建好的虚拟环境之外用系统python33.12.3运行因为要演示的语法特性 3.9/3.10/3.12 都已支持。先确认版本$ python3-VPython3.12.3下面每个示例都是一个独立.py文件我贴出源码再贴服务器跑出来的真实结果。一、变量、字符串与 f-stringPython 里变量不用声明类型赋值即用。字符串的常用操作很多f-string 是最舒服的格式化方式namePythonversion3.12pi3.14159# f-string 基本用法print(f语言:{name}, 版本:{version})# 3.8 调试写法 f{x}x42print(f{x},{x*2})# 字符串常用操作s Hello, Python print(s.strip())print(s.strip().upper())print(s.strip().replace(Python,World))print(.join(reversed(abc)))# 反转print(pyinpython)# 子串判断print(len(s))真实回显语言: Python, 版本: 3.12 x42, x*284 Hello, Python HELLO, PYTHON Hello, World cba True 17重点说两个常被忽视的点。f{x}是 Python 3.8 引入的调试写法它会同时打印变量名和值调试时比手写print(x, x)省事太多我几乎在每段调试代码里都用它。另一个是print(.join(reversed(abc)))得到cba——字符串反转不用自己写循环reversed配合join一行搞定。二、数字类型与浮点精度坑整数、浮点、布尔、复数都是数字类型但浮点运算有一处经典陷阱print(0.1 0.2 ,0.10.2)# 浮点误差print(0.1 0.2 0.3 ?,0.10.20.3)importmathprint(math.isclose:,math.isclose(0.10.2,0.3))fromdecimalimportDecimalprint(Decimal:,Decimal(0.1)Decimal(0.2))# 精确print(整除与取余:,7//2,7%2,2**10)# 地板除 vs 真除print(5/2 ,5/2, | -5//2 ,-5//2)真实回显0.1 0.2 0.30000000000000004 0.1 0.2 0.3 ? False math.isclose: True Decimal: 0.3 整除与取余: 3 1 1024 5/2 2.5 | -5//2 -30.1 0.2不等于0.3而是0.30000000000000004这是二进制浮点的固有问题不是 Python 的 bug。所以涉及金额、精度比较时绝不能直接用要么用math.isclose允许误差要么用decimal.Decimal注意要用字符串构造0.1否则一进来就带误差。7 // 2是整除得37 % 2是取余得12 ** 10是幂运算得1024。最后那个-5 // 2 -3也值得记Python 的地板除是向下取整-2.5向下就是-3和很多语言不一样。真实踩坑脚本名千万别叫 numbers.py这一段我自己的操作翻过车原样记下来。我最初把上面这个文件命名为numbers.py一运行就炸了而且报错信息非常迷惑0.1 0.2 0.30000000000000004 0.1 0.2 0.3 ? False math.isclose: True 0.1 0.2 0.30000000000000004 0.1 0.2 0.3 ? False math.isclose: True 0.1 0.2 0.30000000000000004 0.1 0.2 0.3 ? False math.isclose: True Traceback (most recent call last): File /usr/lib/python3.12/decimal.py, line 3, in module from _decimal import * File /root/lab-a/numbers.py, line 5, in module from decimal import Decimal ImportError: cannot import name Decimal from partially initialized module decimal (most likely due to a circular import) (/usr/lib/python3.12/decimal.py) During handling of the above exception, another exception occurred: Traceback (most recent call last): File /root/lab-a/numbers.py, line 5, in module from decimal import Decimal File /usr/lib/python3.12/decimal.py, line 8, in module from _pydecimal import * File /usr/lib/python3.12/_pydecimal.py, line 157, in module import numbers as _numbers File /root/lab-a/numbers.py, line 5, in module from decimal import Decimal ImportError: cannot import name Decimal from partially initialized module decimal (most likely due to a circular import) (/usr/lib/python3.12/decimal.py)根因是我的脚本叫numbers.py恰好和标准库里的numbers模块同名。当decimal内部执行import numbers时Python 因为当前目录里有numbers.py就把标准库的numbers给遮蔽了进而触发循环导入。上面那三遍重复的0.10.2打印就是numbers.py被当成标准库反复导入自己导致的。把文件改名为nums.py后一切正常。经验法则永远不要用标准库已有的名字如numbers.py、json.py、email.py、random.py给自己的脚本命名这是新手最高频也最隐蔽的坑之一。三、列表、元组、集合、字典全套四种内置容器各有所长下面一次性过完# 列表lst[3,1,2]lst.append(4)lst.sort()print(list:,lst)# 元组t(1,2,3)print(tuple:,t,不可变)# 集合a{1,2,3};b{3,4,5}print(set 交集:,ab,并集:,a|b)# 字典d1{a:1,b:2}d2{b:3,c:4}d1.update(d2)print(dict update:,d1)# 3.9 合并运算符 |d3{a:1,b:2}|{b:99,c:3}print(dict |:,d3)# 字典推导sq{k:k*kforkinrange(1,5)}print(dict comp:,sq)真实回显list: [1, 2, 3, 4] tuple: (1, 2, 3) 不可变 set 交集: {3} 并集: {1, 2, 3, 4, 5} dict update: {a: 1, b: 3, c: 4} dict |: {a: 1, b: 99, c: 3} dict comp: {1: 1, 2: 4, 3: 9, 4: 16}要点列表可变、有序元组不可变适合当常量或函数返回多值集合自动去重、支持交集并集|字典里要特别注意|合并运算符Python 3.9 引入它返回新字典且后者覆盖前者同键上面d3里b从2变成了99。推导式列表、字典、集合都支持是 Python 很爽的语法糖{k: k*k for k in range(1, 5)}一行就生成了平方表。四、流程控制if / match / while / for条件、循环、模式匹配逐个演示。先if和 Python 3.10 的matchscore85ifscore90:gAelifscore60:gBelse:gCprint(grade:,g)defhandle(cmd):matchcmd.split():case[quit]:return退出case[get,name]:returnf获取{name}case[set,name,val]:returnf设置{name}{val}case_:return未知命令print(handle(quit))print(handle(get user))print(handle(set user 100))print(handle(foo bar baz))i0whilei3:forjinrange(2):print(fwhile i{i}for j{j})i1defkind(v):matchv:caseint()ifv0:return正整型caseint():return非正整型casestr():return字符串caselist()asxsiflen(xs)2:return长列表case_:return其他print(kind(5),kind(-1),kind(hi),kind([1,2,3,4]))真实回显grade: B 退出 获取 user 设置 user100 未知命令 while i0 for j0 while i0 for j1 while i1 for j0 while i1 for j1 while i2 for j0 while i2 for j1 正整型 非正整型 字符串 长列表match是 Python 3.10 才有的结构模式匹配比一长串if/elif清爽太多。它能解构序列case [get, name]表示列表第一个元素是字符串get剩下的绑定到namecase _是通配兜底。kind函数里还演示了带守卫的模式case int() if v 0先匹配整型再用if加条件以及case list() as xs把匹配到的列表绑定到变量xs继续判断长度。结构化匹配在处理命令解析、协议字段时特别好用。五、实战一用 match 写四则运算计算器把上面的match用起来做个能识别3 4这种表达式的小计算器defcalc(expr):importre mre.match(r\s*([0-9.])\s*([\-*/])\s*([0-9.])\s*,expr)ifnotm:return格式错误a,op,bfloat(m.group(1)),m.group(2),float(m.group(3))matchop:case:returnabcase-:returna-bcase*:returna*bcase/:returna/bifb!0else除数不能为0case_:return未知运算符forein[3 4,10 - 2,6 * 7,8 / 2,5 / 0,bad]:print(f{e!r:10}{calc(e)})真实回显3 4 7.0 10 - 2 8.0 6 * 7 42.0 8 / 2 4.0 5 / 0 除数不能为0 bad 格式错误这里用正则抽出数字和运算符再用match分发。注意f{e!r:10}!r把字符串按repr形式带引号输出10右对齐占 10 列排版更整齐。5 / 0没有让程序崩溃因为我提前用if b ! 0拦住了除零这是写计算器必须考虑的边界。六、实战二手机通讯录dict json 持久化再做一个带增删查和文件持久化的通讯录顺便演示int()类型转换和json落盘importjson,os FILE/root/lab-a/contacts.jsondefload():ifos.path.exists(FILE):withopen(FILE,r,encodingutf-8)asf:returnjson.load(f)return{}defsave(book):withopen(FILE,w,encodingutf-8)asf:json.dump(book,f,ensure_asciiFalse,indent2)bookload()print(初始通讯录:,book)# 增book[张三]{phone:13800000001,age:int(28)}book[李四]{phone:13900000002,age:int(32)}save(book)print(新增后:,book)# 查print(查张三:,book.get(张三))print(查王五(不存在):,book.get(王五,无此联系人))# 改book[张三][phone]13811111111save(book)# 删removedbook.pop(李四,None)print(删除李四:,removed)save(book)print(最终文件内容:)print(open(FILE,encodingutf-8).read())真实回显初始通讯录: {} 新增后: {张三: {phone: 13800000001, age: 28}, 李四: {phone: 13900000002, age: 32}} 查张三: {phone: 13800000001, age: 28} 查王五(不存在): 无此联系人 删除李四: {phone: 13900000002, age: 32} 最终文件内容: { 张三: { phone: 13811111111, age: 28 } }book.get(key, 默认值)是安全取值的写法——键不存在时返回你给的默认值而不是抛KeyError查王五时返回了无此联系人就是它发挥作用。json.dump(..., ensure_asciiFalse, indent2)让中文原样保存、带缩进更可读。文件里age: 28是数字而非字符串因为我在存的时候用int(28)做了转换——从外部读进来的数据往往都是字符串记得按需转成正确的类型。真实踩坑路径里的 %s 忘了格式化这个通讯录脚本我第一版也写错了原样贴出当时的报错初始通讯录: {} Traceback (most recent call last): File /root/lab-a/contacts.py, line 21, in module save(book) File /root/lab-a/contacts.py, line 12, in save with open(FILE, w, encodingutf-8) as f: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FileNotFoundError: [Errno 2] No such file or directory: %s/contacts.json原因很直白我起初写的是FILE %s/contacts.json却忘了在后面用% BASE把%s替换成真实目录于是 Python 真的去打开一个名字叫%s/contacts.json的文件自然不存在。这种格式化占位符忘了填的笔误在拼接路径、日志、SQL 时极常见。修法是直接用拼接好的绝对路径或者老老实实FILE %s/contacts.json % BASE。报错信息把出错的行和具体的文件名都打印出来了照着看一眼就能定位。七、避坑可变默认参数函数默认参数如果是可变对象列表、字典等会在多次调用间共享酿成诡异 bugdefadd_item_bad(item,lst[]):lst.append(item)returnlstprint(第一次:,add_item_bad(1))print(第二次:,add_item_bad(2))# 期望 [2]实际累积了 [1,2]defadd_item_good(item,lstNone):iflstisNone:lst[]lst.append(item)returnlstprint(good1:,add_item_good(1))print(good2:,add_item_good(2))真实回显第一次: [1] 第二次: [1, 2] good1: [1] good2: [2]第二次调用只传了2却返回了[1, 2]——因为默认列表[]在函数定义时只创建一次之后每次调用都在往同一个列表里追加。正确写法是把默认值设为None在函数体内再新建列表。这条坑我见过不止一个人栽尤其是写递归或累积逻辑时。八、避坑浅拷贝copy()和切片得到的是浅拷贝嵌套的可变对象仍指向同一份importcopy a{scores:[80,90]}ba.copy()# 浅拷贝b[scores].append(100)# 影响到了 aprint(a:,a)print(b:,b)print(是同一列表对象?,a[scores]isb[scores])ccopy.deepcopy(a)# 深拷贝c[scores].append(999)print(深拷贝后 a:,a)print(深拷贝后 c:,c)真实回显a: {scores: [80, 90, 100]} b: {scores: [80, 90, 100]} 是同一列表对象? True 深拷贝后 a: {scores: [80, 90, 100]} 深拷贝后 c: {scores: [80, 90, 100, 999]}a.copy()之后改b[scores]竟然连a也变了因为b[scores]和a[scores]是同一个列表对象输出True已证实。只要涉及嵌套可变结构要用copy.deepcopy做深拷贝才能彻底隔离。九、附带几个高频小技巧最后补几个我写业务代码天天在用的小语法顺便巩固上面的容器和循环。names[张三,李四,王五]scores[90,85,88]fori,ninenumerate(names):print(i,n)print(---- zip ----)forn,sinzip(names,scores):print(f{n}:{s})# 元组解包a,b,*rest[1,2,3,4,5]print(a,b,rest ,a,b,rest)# 交换变量x,y10,20x,yy,xprint(swap:,x,y)# is vs m[1,2];n[1,2]print(:,mn,is:,misn)真实回显0 张三 1 李四 2 王五 ---- zip ---- 张三: 90 李四: 85 王五: 88 a,b,rest 1 2 [3, 4, 5] swap: 20 10 : True is: Falseenumerate在遍历时顺带给出下标比自己维护一个计数器清爽zip把多个可迭代对象按位置拉链式配对处理名字分数这类平行序列时离不开它。a, b, *rest [1,2,3,4,5]这个写法我很喜欢——*rest把剩下的元素一股脑收进列表得到rest [3,4,5]拆包时特别灵活。变量交换x, y y, x在 Python 里一行就成不需要中间变量这是元组解包的语法糖。最后那个与is的区别值得单独划重点m n是True因为两个列表的内容相同m is n是False因为is比的是对象身份内存地址这是两个不同的列表对象。is只对小整数缓存、单例None这类场景可靠比较两个值永远用不要用is这是新手写条件判断时很容易写错的一处。踩坑清单现象根因正确做法0.1 0.2 0.3是False二进制浮点固有误差用math.isclose或Decimal文件命名为numbers.py导致循环导入同名遮蔽标准库模块脚本名避开标准库保留名FileNotFoundError: %s/...路径字符串%s忘了格式化直接拼绝对路径或补% BASE可变默认参数跨调用串数据默认对象只创建一次默认设None体内新建浅拷贝后改一个影响另一个嵌套对象仍共享引用用copy.deepcopymatch匹配不到缺少case _兜底总加一个通配兜底分支总结这一篇把 Python 3.12 的骨架过了一遍变量与 f-string 让表达简洁Decimal和math.isclose兜住浮点精度四种容器加|合并运算符覆盖日常数据结构match模式匹配比if/elif更优雅也更强两个小实战把语法落到了能跑的东西上。而那几个坑——标准库同名遮蔽、路径占位符漏填、可变默认参数、浅拷贝共享引用——都是我亲手敲错、看着报错改过来的比任何记忆都牢。语法只是工具踩坑才是学费。写这篇时我刻意在每个示例后面都贴了真实回显而不是只给结论——因为语法背得再熟不亲手敲一遍、不亲眼看看报错长什么样遇到真问题还是两眼一抹黑。下一篇可以聊函数、类与模块化把这套基础真正用起来。本文实验均在华为云 Flexus X 实例Ubuntu 24.04, Python 3.12.3上真实执行。