文件、函数与装饰器写出可复用的 Python 代码一、背景引入在《Python 实战》入门篇里我们写下的脚本大多是「一次性」的读一个写死的文件、跑一遍就结束。但真正在生产环境里跑的代码核心诉求从来不是「能跑」而是「能复用、能维护、能交给别人改」。我在带团队做内部工具时见到过太多这样的代码配置读取没有指定编码换台机器就UnicodeDecodeError函数默认参数写成def f(x, l[])结果多次调用之间悄悄串了数据给函数加日志、加计时直接把逻辑改得面目全非。这些问题的根都指向三个 Python 里最朴素也最容易被轻视的概念——文件、函数、装饰器。这一篇我不讲语法书上的定义而是在真实云服务器上一行一行跑、抓真实回显把那些「看书觉得懂、一写就出错」的地方全部复现出来。读完后你会发现所谓「可复用的代码」无非是把这些基础打牢之后的自然结果。二、环境说明真实回显本文所有代码都在华为云 Flexus X 实例Ubuntu 24.04Python 3.12.3上真实执行。我先连上服务器、确认环境、建好实验目录/root/lab-b$uname-a;echo---;python3 --version;echo---;mkdir-p/root/lab-becholab-b-ready Linux ecs-3d3b-00046.8.0-106-generic#106-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 6 07:58:08 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux--- Python3.12.3 --- lab-b-ready可以看到内核是 6.8.0-106Python 3.12.3实验目录已就绪。下面每一节都会给出「目的 → 代码 → 真实回显 → 讲解」四段式结构代码与回显均来自服务器真实输出未做任何编造。三、分节实操3.1 文件操作open 模式、encoding 乱码、with 上下文、合并实战3.1.1 open 的常用模式对比很多人分不清r / w / a / r的差异尤其是w会清空原文件这一点踩过一次的都印象深刻。# b3_file_modes.py节选withopen(modes_demo.txt,w,encodingutf-8)asf:f.write(第一行\n第二行\n)print( 1) r 只读模式 )withopen(modes_demo.txt,r,encodingutf-8)asf:print(repr(f.read()))print( 3) w 写模式会清空原内容 )withopen(modes_demo.txt,w,encodingutf-8)asf:f.write(被 w 覆盖后的唯一一行\n)真实回显 1) r 只读模式 第一行\n第二行\n 2) a 追加模式文件末尾追加 第一行 第二行 第三行追加 3) w 写模式会清空原内容 被 w 覆盖后的唯一一行 4) r 读写模式不截断 读到的: 被 w 覆盖后的唯一一行\n 再次读: 被 w 覆盖后的唯一一行 追加在指针末尾 5) 读取不存在文件 FileNotFoundError : [Errno 2] No such file or directory: not_exist.txt 6) 当前目录文件 [b3_file_modes.py, modes_demo.txt]讲解r只读、a追加到末尾、w覆盖清空、r读写且不截断注意读完指针在末尾再write是追加而非从头。读不存在的文件会抛FileNotFoundError这正是我们做健壮 IO 时要用try/except兜住的那类错误。3.1.2 encoding 不一致导致乱码 / UnicodeDecodeError 复现这是最经典的「在我机器上好好的」问题。我故意用GBK写一个含中文的文件再用默认的UTF-8去读复现乱码异常# b3_encoding.py节选text你好世界中文编码测试withopen(gbk_file.txt,w,encodinggbk)asf:f.write(text)print(已用 GBK 写入磁盘字节预览:,open(gbk_file.txt,rb).read()[:20],...)try:withopen(gbk_file.txt,r,encodingutf-8)asf:contentf.read()print(读到了:,content)exceptUnicodeDecodeErrorase:print(捕获到异常:,type(e).__name__)print(异常信息:,e)真实回显 1) 用 GBK 编码写入中文 已用 GBK 写入磁盘字节预览: b\xc4\xe3\xba\xc3\xa3\xac\xca\xc0\xbd\xe7\xa3\xa1\xd6\xd0\xce\xc4\xb1\xe0\xc2\xeb ... 2) 用默认的 UTF-8 去读这个 GBK 文件 捕获到异常: UnicodeDecodeError 异常信息: utf-8 codec cant decode byte 0xc4 in position 0: invalid continuation byte 3) 正确做法按写入时的编码读取 正确读取: 你好世界中文编码测试 4) 用 errors 容错读取替换策略 errorsreplace 结果: ã磡ı讲解第 29 行b\xc4\xe3...是「你好世界」的 GBK 字节用 UTF-8 解码时首字节0xc4不是合法的 UTF-8 续接字节于是抛出UnicodeDecodeError并精确指出出错位置position 0。结论写文件时用什么encoding读就必须用同一个。若实在无法预知编码可加errorsreplace兜底如第 39 行但替换出来的已是丢失的信息只能算「不崩」而非「正确」。3.1.3 with 上下文 实战合并多份日志 / CSVwith的价值是「无论是否异常文件句柄都会被关闭」。我制造 3 个日志分片再合并并演示 CSV 按列头去重合并# b3_with_merge.py节选withopen(logs/merged.log,w,encodingutf-8)asout:forpathinsorted(glob.glob(logs/*.log)):withopen(path,r,encodingutf-8)assrc:out.write(f# ----{os.path.basename(path)}----\n)out.write(src.read())真实回显节选 4) 合并所有日志保持 with 安全打开 合并后行数: 1 # ---- app.1.log ---- 2 2026-07-24 10:00:01 INFO 服务启动 3 2026-07-24 10:00:02 INFO 收到请求 4 # ---- app.2.log ---- 5 2026-07-24 10:01:00 WARN 慢查询 6 2026-07-24 10:01:30 INFO 处理完成 7 # ---- app.3.log ---- 8 2026-07-24 10:02:00 ERROR 连接超时 5) 合并多个 CSV按列头去重 合并 CSV 结果: name,score Alice,90 Bob,85 Bob,85 Carol,92讲解嵌套with让每个源文件读完后立即关闭目标文件最后才关安全且清晰。CSV 合并里我只对「列头」做了去重header_seen哨兵所以数据行中的Bob,85在两份里各出现一次就都保留了——这正好提醒你去重是有业务语义的先想清楚是去重表头还是去重主键再写代码。3.2 函数参数形态与可变默认参数的坑3.2.1 位置 / 关键字 / 默认 / 可变参数# b3_func_args.py节选defgreet(name,age):returnf{name}今年{age}岁print(greet(张三,18))# 位置print(greet(age20,name李四))# 关键字deftotal(*args):print(args 类型:,type(args),内容:,args)returnsum(args)print(求和:,total(1,2,3,4,5))defshow_profile(**kwargs):print(kwargs 类型:,type(kwargs))show_profile(name王五,city北京,vipTrue)真实回显 1) 位置参数 vs 关键字参数 张三 今年 18 岁 李四 今年 20 岁 3) *args 收集位置参数为元组 args 类型: class tuple 内容: (1, 2, 3, 4, 5) 求和: 15 4) **kwargs 收集关键字参数为字典 kwargs 类型: class dict name 王五 city 北京 vip True 6) 仅限关键字参数* 之后 6 报错: f() takes 2 positional arguments but 3 were given 7) 返回多个值本质是元组 商: 3 余: 2 返回值类型: class tuple讲解*args把多余位置参数收成元组**kwargs把关键字参数收成字典。*之后的参数def f(a,b,*,c)只能用关键字传入强制调用方写清c3可读性更好。所谓「返回多个值」底层就是一个元组所以q, r divmod2(...)本质是一次元组解包。3.2.2 可变默认参数的经典坑真实复现这是我见过最高频的面试题兼生产 bug。先看错误写法# b3_mutable_default.pydefadd_item(item,bag[]):bag.append(item)returnbagprint(第一次调用 add_item(苹果):,add_item(苹果))print(第二次调用 add_item(香蕉):,add_item(香蕉))print(第三次调用 add_item(樱桃):,add_item(樱桃))print(默认列表的 id始终相同:,id(add_item.__defaults__[0]))真实回显 1) 复现坑默认列表被多次调用共享 第一次调用 add_item(苹果): [苹果] 第二次调用 add_item(香蕉): [苹果, 香蕉] 第三次调用 add_item(樱桃): [苹果, 香蕉, 樱桃] 问题每次调用都没有清空因为共用同一个默认列表对象 默认列表的 id始终相同: 127184020361792为什么默认参数bag[]在函数定义时就被创建并绑定之后每次调用如果没有传bag用的都是同一个列表对象id 始终相同。正确写法是用None作哨兵defadd_item_ok(item,bagNone):ifbagisNone:bag[]bag.append(item)returnbag真实回显正确版 2) 正确写法用 None 作哨兵 第一次: [苹果] 第二次: [香蕉] 第三次: [樱桃]3.3 高阶函数map / filter / sorted(key) / lambda / 函数对象高阶函数就是「接收或返回函数」的函数。它们把「做什么」和「怎么做」拆开是函数式复用的起点。# b3_higher_order.py节选students[{name:Alice,score:90},{name:Bob,score:75},{name:Carol,score:99}]by_scoresorted(students,keylambdas:s[score],reverseTrue)defmake_adder(n):defadder(x):returnxnreturnadder# 注意没有括号返回的是函数对象add5make_adder(5)print(add5(10) 调用结果:,add5(10))真实回显 1) map把函数作用到每个元素 平方: [1, 4, 9, 16, 25] 3) sorted key按自定义规则排序 按分数降序: Carol 99 Alice 90 Bob 75 5) 函数对象 vs 函数调用一个括号的差别 add5 的类型: class function add5(10) 调用结果: 15 add5 本身未调用: function make_adder.locals.adder at 0x7dda7d7a9300 对比误把调用当对象: make_adder(5) 返回的是函数不是数字 - 调用 make_adder(5)(10) 15讲解lambda只是匿名函数和def完全等价第 4 节两者输出一致可证。sorted(key...)把排序规则抽象成一个函数非常解耦。make_adder返回的是「函数对象」而非结果add5因此成了一个「参数被冻结」的专用函数——这是后面装饰器的核心思想函数可以像数据一样被传递和返回。3.4 装饰器从函数嵌套推导出计时器、带参装饰器、functools.wraps装饰器本质就是「接收一个函数、返回一个函数」的高阶函数。我先用函数嵌套引出再逐步加料。# b3_decorator.py节选初级版未用 wrapsdeftimer(func):defwrapper(*args,**kwargs):t0time.perf_counter()resultfunc(*args,**kwargs)print(f[计时]{func.__name__}耗时{time.perf_counter()-t0:.6f}s)returnresultreturnwrappertimerdefslow_add(a,b):time.sleep(0.2)returnab真实回显含 wraps 前后对比 2) 计时装饰器 timer初级版未用 wraps [计时] slow_add 耗时 0.200078s slow_add(3,4) 7 没有 wraps 时slow_add.__name__ wrapper (原函数名被覆盖) 3) 用 functools.wraps 保留元信息 [计时] fast_mul 耗时 0.100080s fast_mul(3,4) 12 有 wraps 时fast_mul.__name__ fast_mul fast_mul.__doc__ None 4) 带参数的装饰器三层嵌套 greet(小明) [你好, 小明!, 你好, 小明!, 你好, 小明!] 5) 装饰器叠加顺序从下往上包裹 [计时] hello 耗时 0.000002s hello() [Hi, Hi]讲解timer等价于slow_add timer(slow_add)。wrapper包裹了原函数所以原函数名__name__被覆盖成了wrapper——这会让日志、调试栈变得难读。用functools.wraps(func)装饰wrapper即可把原函数的元信息名字、文档拷贝回来第 165 行恢复到fast_mul。带参装饰器如repeat(times3)是多包一层最外层收参数中间层收函数最内层才是真正的wrapper。多个装饰器叠加时执行顺序从下往上包裹离函数最近的先包。3.5 实战电商购物车函数式 装饰器实现满减把前面所有概念串起来购物车用字典表示加购是普通函数结算用装饰器叠加「满减优惠」可任意组合。# b3_cart.py节选defcalc_subtotal():returnsum(price*qtyforprice,qtyincart.values())deffull_reduction(threshold,minus):defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):subtotalcalc_subtotal()discountminusifsubtotalthresholdelse0print(f优惠规则满{threshold}减{minus}- 本次优惠{discount})returnfunc(*args,**kwargs)-discountreturnwrapperreturndecoratorfull_reduction(threshold200,minus30)defcheckout():returncalc_subtotal()真实回显 购物车结算演示 加购: 机械键盘 x1 199 加购: 鼠标 x1 89 小计: 288 优惠规则满 200 减 30 - 本次优惠 30 最终应付满200减30: 258 换一个不满 200 的购物车 加购: 数据线 x2 25 加购: 贴膜 x1 15 小计: 65 优惠规则满 200 减 30 - 本次优惠 0 走满200减30: 65 优惠规则满 100 减 10 - 本次优惠 0 走满100减10: 65 新增 VIP 95 折装饰器叠加 加购: 显示器 x1 999 小计: 999 应用 VIP 95 折 优惠规则满 200 减 30 - 本次优惠 30 VIP 最终应付: 920.55讲解满 288 触发「满 200 减 30」→ 实付 258而不满 200 时优惠为 0分文不减。VIP 场景把vip_discount和full_reduction叠加注意从下往上full_reduction先算减 30 得 969再vip_discount乘 0.95 ≈ 920.55。优惠逻辑与结算逻辑彻底解耦——要加新活动只需再写一个装饰器无需改动checkout本体这就是可复用性的直接收益。四、踩坑清单序号坑现象正确做法1w模式覆盖文件原内容被清空且无提示确认要覆盖才用w追加用a2读写编码不一致UnicodeDecodeError: byte 0xc4 ...写读用同一encoding优先 UTF-83忘记关文件句柄泄漏、写入可能未落盘一律用with open(...) as f4可变默认参数l[]多次调用共享同一对象数据串号用None作哨兵函数内再初始化5把add5当add5()拿到函数对象而非结果区分「函数对象」与「函数调用」一个括号6装饰器没加functools.wraps原函数__name__/__doc__丢失用functools.wraps(func)装饰 wrapper7装饰器叠加顺序误解优惠计算顺序与预期相反记住「从下往上包裹」先写的后执行五、总结这一篇我们从一个朴素目标出发——写出可复用的 Python 代码然后逐层落地文件r/w/a/r的差异、encoding必须读写一致、用with兜底资源释放以及把多文件合并写成可维护的脚本函数四种参数形态、多返回值本质是元组、以及那个几乎必踩的「可变默认参数」坑高阶函数map/filter/sorted(key)与lambda把行为参数化并理解「函数也是对象」装饰器从函数嵌套推导出timer认识functools.wraps的必要性、带参装饰器的三层结构并在购物车实战中用装饰器把「优惠规则」与「结算」彻底解耦。你会发现所谓「可复用」并不是什么高深技巧而是把每一次副作用IO、状态都关进清晰的边界里把每一个变化点参数、规则都抽象成可传递的函数。下一篇我们会进入面向对象与异常处理把「状态」这件事交给类来管理。本文实验均在华为云 Flexus X 实例Ubuntu 24.04, Python 3.12.3上真实执行。
文件、函数与装饰器:写出可复用的 Python 代码
文件、函数与装饰器写出可复用的 Python 代码一、背景引入在《Python 实战》入门篇里我们写下的脚本大多是「一次性」的读一个写死的文件、跑一遍就结束。但真正在生产环境里跑的代码核心诉求从来不是「能跑」而是「能复用、能维护、能交给别人改」。我在带团队做内部工具时见到过太多这样的代码配置读取没有指定编码换台机器就UnicodeDecodeError函数默认参数写成def f(x, l[])结果多次调用之间悄悄串了数据给函数加日志、加计时直接把逻辑改得面目全非。这些问题的根都指向三个 Python 里最朴素也最容易被轻视的概念——文件、函数、装饰器。这一篇我不讲语法书上的定义而是在真实云服务器上一行一行跑、抓真实回显把那些「看书觉得懂、一写就出错」的地方全部复现出来。读完后你会发现所谓「可复用的代码」无非是把这些基础打牢之后的自然结果。二、环境说明真实回显本文所有代码都在华为云 Flexus X 实例Ubuntu 24.04Python 3.12.3上真实执行。我先连上服务器、确认环境、建好实验目录/root/lab-b$uname-a;echo---;python3 --version;echo---;mkdir-p/root/lab-becholab-b-ready Linux ecs-3d3b-00046.8.0-106-generic#106-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 6 07:58:08 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux--- Python3.12.3 --- lab-b-ready可以看到内核是 6.8.0-106Python 3.12.3实验目录已就绪。下面每一节都会给出「目的 → 代码 → 真实回显 → 讲解」四段式结构代码与回显均来自服务器真实输出未做任何编造。三、分节实操3.1 文件操作open 模式、encoding 乱码、with 上下文、合并实战3.1.1 open 的常用模式对比很多人分不清r / w / a / r的差异尤其是w会清空原文件这一点踩过一次的都印象深刻。# b3_file_modes.py节选withopen(modes_demo.txt,w,encodingutf-8)asf:f.write(第一行\n第二行\n)print( 1) r 只读模式 )withopen(modes_demo.txt,r,encodingutf-8)asf:print(repr(f.read()))print( 3) w 写模式会清空原内容 )withopen(modes_demo.txt,w,encodingutf-8)asf:f.write(被 w 覆盖后的唯一一行\n)真实回显 1) r 只读模式 第一行\n第二行\n 2) a 追加模式文件末尾追加 第一行 第二行 第三行追加 3) w 写模式会清空原内容 被 w 覆盖后的唯一一行 4) r 读写模式不截断 读到的: 被 w 覆盖后的唯一一行\n 再次读: 被 w 覆盖后的唯一一行 追加在指针末尾 5) 读取不存在文件 FileNotFoundError : [Errno 2] No such file or directory: not_exist.txt 6) 当前目录文件 [b3_file_modes.py, modes_demo.txt]讲解r只读、a追加到末尾、w覆盖清空、r读写且不截断注意读完指针在末尾再write是追加而非从头。读不存在的文件会抛FileNotFoundError这正是我们做健壮 IO 时要用try/except兜住的那类错误。3.1.2 encoding 不一致导致乱码 / UnicodeDecodeError 复现这是最经典的「在我机器上好好的」问题。我故意用GBK写一个含中文的文件再用默认的UTF-8去读复现乱码异常# b3_encoding.py节选text你好世界中文编码测试withopen(gbk_file.txt,w,encodinggbk)asf:f.write(text)print(已用 GBK 写入磁盘字节预览:,open(gbk_file.txt,rb).read()[:20],...)try:withopen(gbk_file.txt,r,encodingutf-8)asf:contentf.read()print(读到了:,content)exceptUnicodeDecodeErrorase:print(捕获到异常:,type(e).__name__)print(异常信息:,e)真实回显 1) 用 GBK 编码写入中文 已用 GBK 写入磁盘字节预览: b\xc4\xe3\xba\xc3\xa3\xac\xca\xc0\xbd\xe7\xa3\xa1\xd6\xd0\xce\xc4\xb1\xe0\xc2\xeb ... 2) 用默认的 UTF-8 去读这个 GBK 文件 捕获到异常: UnicodeDecodeError 异常信息: utf-8 codec cant decode byte 0xc4 in position 0: invalid continuation byte 3) 正确做法按写入时的编码读取 正确读取: 你好世界中文编码测试 4) 用 errors 容错读取替换策略 errorsreplace 结果: ã磡ı讲解第 29 行b\xc4\xe3...是「你好世界」的 GBK 字节用 UTF-8 解码时首字节0xc4不是合法的 UTF-8 续接字节于是抛出UnicodeDecodeError并精确指出出错位置position 0。结论写文件时用什么encoding读就必须用同一个。若实在无法预知编码可加errorsreplace兜底如第 39 行但替换出来的已是丢失的信息只能算「不崩」而非「正确」。3.1.3 with 上下文 实战合并多份日志 / CSVwith的价值是「无论是否异常文件句柄都会被关闭」。我制造 3 个日志分片再合并并演示 CSV 按列头去重合并# b3_with_merge.py节选withopen(logs/merged.log,w,encodingutf-8)asout:forpathinsorted(glob.glob(logs/*.log)):withopen(path,r,encodingutf-8)assrc:out.write(f# ----{os.path.basename(path)}----\n)out.write(src.read())真实回显节选 4) 合并所有日志保持 with 安全打开 合并后行数: 1 # ---- app.1.log ---- 2 2026-07-24 10:00:01 INFO 服务启动 3 2026-07-24 10:00:02 INFO 收到请求 4 # ---- app.2.log ---- 5 2026-07-24 10:01:00 WARN 慢查询 6 2026-07-24 10:01:30 INFO 处理完成 7 # ---- app.3.log ---- 8 2026-07-24 10:02:00 ERROR 连接超时 5) 合并多个 CSV按列头去重 合并 CSV 结果: name,score Alice,90 Bob,85 Bob,85 Carol,92讲解嵌套with让每个源文件读完后立即关闭目标文件最后才关安全且清晰。CSV 合并里我只对「列头」做了去重header_seen哨兵所以数据行中的Bob,85在两份里各出现一次就都保留了——这正好提醒你去重是有业务语义的先想清楚是去重表头还是去重主键再写代码。3.2 函数参数形态与可变默认参数的坑3.2.1 位置 / 关键字 / 默认 / 可变参数# b3_func_args.py节选defgreet(name,age):returnf{name}今年{age}岁print(greet(张三,18))# 位置print(greet(age20,name李四))# 关键字deftotal(*args):print(args 类型:,type(args),内容:,args)returnsum(args)print(求和:,total(1,2,3,4,5))defshow_profile(**kwargs):print(kwargs 类型:,type(kwargs))show_profile(name王五,city北京,vipTrue)真实回显 1) 位置参数 vs 关键字参数 张三 今年 18 岁 李四 今年 20 岁 3) *args 收集位置参数为元组 args 类型: class tuple 内容: (1, 2, 3, 4, 5) 求和: 15 4) **kwargs 收集关键字参数为字典 kwargs 类型: class dict name 王五 city 北京 vip True 6) 仅限关键字参数* 之后 6 报错: f() takes 2 positional arguments but 3 were given 7) 返回多个值本质是元组 商: 3 余: 2 返回值类型: class tuple讲解*args把多余位置参数收成元组**kwargs把关键字参数收成字典。*之后的参数def f(a,b,*,c)只能用关键字传入强制调用方写清c3可读性更好。所谓「返回多个值」底层就是一个元组所以q, r divmod2(...)本质是一次元组解包。3.2.2 可变默认参数的经典坑真实复现这是我见过最高频的面试题兼生产 bug。先看错误写法# b3_mutable_default.pydefadd_item(item,bag[]):bag.append(item)returnbagprint(第一次调用 add_item(苹果):,add_item(苹果))print(第二次调用 add_item(香蕉):,add_item(香蕉))print(第三次调用 add_item(樱桃):,add_item(樱桃))print(默认列表的 id始终相同:,id(add_item.__defaults__[0]))真实回显 1) 复现坑默认列表被多次调用共享 第一次调用 add_item(苹果): [苹果] 第二次调用 add_item(香蕉): [苹果, 香蕉] 第三次调用 add_item(樱桃): [苹果, 香蕉, 樱桃] 问题每次调用都没有清空因为共用同一个默认列表对象 默认列表的 id始终相同: 127184020361792为什么默认参数bag[]在函数定义时就被创建并绑定之后每次调用如果没有传bag用的都是同一个列表对象id 始终相同。正确写法是用None作哨兵defadd_item_ok(item,bagNone):ifbagisNone:bag[]bag.append(item)returnbag真实回显正确版 2) 正确写法用 None 作哨兵 第一次: [苹果] 第二次: [香蕉] 第三次: [樱桃]3.3 高阶函数map / filter / sorted(key) / lambda / 函数对象高阶函数就是「接收或返回函数」的函数。它们把「做什么」和「怎么做」拆开是函数式复用的起点。# b3_higher_order.py节选students[{name:Alice,score:90},{name:Bob,score:75},{name:Carol,score:99}]by_scoresorted(students,keylambdas:s[score],reverseTrue)defmake_adder(n):defadder(x):returnxnreturnadder# 注意没有括号返回的是函数对象add5make_adder(5)print(add5(10) 调用结果:,add5(10))真实回显 1) map把函数作用到每个元素 平方: [1, 4, 9, 16, 25] 3) sorted key按自定义规则排序 按分数降序: Carol 99 Alice 90 Bob 75 5) 函数对象 vs 函数调用一个括号的差别 add5 的类型: class function add5(10) 调用结果: 15 add5 本身未调用: function make_adder.locals.adder at 0x7dda7d7a9300 对比误把调用当对象: make_adder(5) 返回的是函数不是数字 - 调用 make_adder(5)(10) 15讲解lambda只是匿名函数和def完全等价第 4 节两者输出一致可证。sorted(key...)把排序规则抽象成一个函数非常解耦。make_adder返回的是「函数对象」而非结果add5因此成了一个「参数被冻结」的专用函数——这是后面装饰器的核心思想函数可以像数据一样被传递和返回。3.4 装饰器从函数嵌套推导出计时器、带参装饰器、functools.wraps装饰器本质就是「接收一个函数、返回一个函数」的高阶函数。我先用函数嵌套引出再逐步加料。# b3_decorator.py节选初级版未用 wrapsdeftimer(func):defwrapper(*args,**kwargs):t0time.perf_counter()resultfunc(*args,**kwargs)print(f[计时]{func.__name__}耗时{time.perf_counter()-t0:.6f}s)returnresultreturnwrappertimerdefslow_add(a,b):time.sleep(0.2)returnab真实回显含 wraps 前后对比 2) 计时装饰器 timer初级版未用 wraps [计时] slow_add 耗时 0.200078s slow_add(3,4) 7 没有 wraps 时slow_add.__name__ wrapper (原函数名被覆盖) 3) 用 functools.wraps 保留元信息 [计时] fast_mul 耗时 0.100080s fast_mul(3,4) 12 有 wraps 时fast_mul.__name__ fast_mul fast_mul.__doc__ None 4) 带参数的装饰器三层嵌套 greet(小明) [你好, 小明!, 你好, 小明!, 你好, 小明!] 5) 装饰器叠加顺序从下往上包裹 [计时] hello 耗时 0.000002s hello() [Hi, Hi]讲解timer等价于slow_add timer(slow_add)。wrapper包裹了原函数所以原函数名__name__被覆盖成了wrapper——这会让日志、调试栈变得难读。用functools.wraps(func)装饰wrapper即可把原函数的元信息名字、文档拷贝回来第 165 行恢复到fast_mul。带参装饰器如repeat(times3)是多包一层最外层收参数中间层收函数最内层才是真正的wrapper。多个装饰器叠加时执行顺序从下往上包裹离函数最近的先包。3.5 实战电商购物车函数式 装饰器实现满减把前面所有概念串起来购物车用字典表示加购是普通函数结算用装饰器叠加「满减优惠」可任意组合。# b3_cart.py节选defcalc_subtotal():returnsum(price*qtyforprice,qtyincart.values())deffull_reduction(threshold,minus):defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):subtotalcalc_subtotal()discountminusifsubtotalthresholdelse0print(f优惠规则满{threshold}减{minus}- 本次优惠{discount})returnfunc(*args,**kwargs)-discountreturnwrapperreturndecoratorfull_reduction(threshold200,minus30)defcheckout():returncalc_subtotal()真实回显 购物车结算演示 加购: 机械键盘 x1 199 加购: 鼠标 x1 89 小计: 288 优惠规则满 200 减 30 - 本次优惠 30 最终应付满200减30: 258 换一个不满 200 的购物车 加购: 数据线 x2 25 加购: 贴膜 x1 15 小计: 65 优惠规则满 200 减 30 - 本次优惠 0 走满200减30: 65 优惠规则满 100 减 10 - 本次优惠 0 走满100减10: 65 新增 VIP 95 折装饰器叠加 加购: 显示器 x1 999 小计: 999 应用 VIP 95 折 优惠规则满 200 减 30 - 本次优惠 30 VIP 最终应付: 920.55讲解满 288 触发「满 200 减 30」→ 实付 258而不满 200 时优惠为 0分文不减。VIP 场景把vip_discount和full_reduction叠加注意从下往上full_reduction先算减 30 得 969再vip_discount乘 0.95 ≈ 920.55。优惠逻辑与结算逻辑彻底解耦——要加新活动只需再写一个装饰器无需改动checkout本体这就是可复用性的直接收益。四、踩坑清单序号坑现象正确做法1w模式覆盖文件原内容被清空且无提示确认要覆盖才用w追加用a2读写编码不一致UnicodeDecodeError: byte 0xc4 ...写读用同一encoding优先 UTF-83忘记关文件句柄泄漏、写入可能未落盘一律用with open(...) as f4可变默认参数l[]多次调用共享同一对象数据串号用None作哨兵函数内再初始化5把add5当add5()拿到函数对象而非结果区分「函数对象」与「函数调用」一个括号6装饰器没加functools.wraps原函数__name__/__doc__丢失用functools.wraps(func)装饰 wrapper7装饰器叠加顺序误解优惠计算顺序与预期相反记住「从下往上包裹」先写的后执行五、总结这一篇我们从一个朴素目标出发——写出可复用的 Python 代码然后逐层落地文件r/w/a/r的差异、encoding必须读写一致、用with兜底资源释放以及把多文件合并写成可维护的脚本函数四种参数形态、多返回值本质是元组、以及那个几乎必踩的「可变默认参数」坑高阶函数map/filter/sorted(key)与lambda把行为参数化并理解「函数也是对象」装饰器从函数嵌套推导出timer认识functools.wraps的必要性、带参装饰器的三层结构并在购物车实战中用装饰器把「优惠规则」与「结算」彻底解耦。你会发现所谓「可复用」并不是什么高深技巧而是把每一次副作用IO、状态都关进清晰的边界里把每一个变化点参数、规则都抽象成可传递的函数。下一篇我们会进入面向对象与异常处理把「状态」这件事交给类来管理。本文实验均在华为云 Flexus X 实例Ubuntu 24.04, Python 3.12.3上真实执行。