
写Python这几年我几乎每次给新人答疑最后都会落到同一个话题上函数的定义与调用。很多语法书把“def和return讲得清清楚楚可到了自己动手写还是会出现“函数没生效”“参数传错了”“返回了个None”这类基础问题。这篇文章就把Python函数的定义与调用这件事彻底掰开从最简单的def语法讲到参数传递、返回值、作用域最后再用一个完整案例串起来新手能照着重现老手也能当一手排查手册用。我尽量用写代码时的真实语境来讲不整虚的。你准备好跟着敲一遍就够了。1. 为什么你绕不开函数1.1 没有函数的日子先看一段刚学Python时很多人写过的代码# 计算三个矩形的面积 r1_w, r1_h 3, 4 area1 r1_w * r1_h print(第一个矩形面积, area1) r2_w, r2_h 5, 6 area2 r2_w * r2_h print(第二个矩形面积, area2) r3_w, r3_h 7, 8 area3 r3_w * r3_h print(第三个矩形面积, area3)这段代码能跑但问题很直接同样的“宽乘高”逻辑重复了三遍如果哪天需求变了要求面积保留两位小数你就得改三个地方。更麻烦的是如果中间某个数算错了你要在几段几乎一样的代码里找差异眼睛都会看花。这就是没有函数的痛苦逻辑散落一地改一处容易漏三处。如果你写过超过50行代码一定遇到过这种“复制粘贴式写代码”。复制一次还能忍复制第二次第三次就开始难受等复制到第五次哪怕是最简单的逻辑也变得难以维护。函数就是用来终结这个状态的。1.2 函数到底解决了什么问题函数帮我们解决的核心问题有三类复用、抽象、模块化。先说复用。把一段逻辑封装进函数以后想用就调一次不用再写一遍。比如上面的面积计算封装成rectangle_area(w, h)之后任何地方需要算矩形面积一行调用就搞定。再说抽象。调用方只需要关心“我要什么结果”不需要关心内部是怎么计算的。你把函数写得足够好别人看到rectangle_area(3, 4)就能理解这是在求矩形面积不需要看到w * h这个算式。这在团队协作里特别重要别人读你代码时不用顺着每一行逻辑去猜意图。最后是模块化。大任务拆成小任务每个函数只负责一件具体的事。比如一个电商下单流程可以拆成check_stock()、calculate_price()、create_order()、send_notification()每个函数各管一段出了问题能快速定位到对应函数而不是在一大坨代码里翻来找去。1.3 什么时候该拆函数这是我被问过最多的问题之一“我这段代码到底要不要封装成函数”我的建议很简单同一段逻辑出现三次以上就必须封装出现两次可以开始考虑就算只出现一次如果它跟你当前主流程的语义不在一个层次上也值得封装。比如你正在写订单处理逻辑中间突然插了一段“把JSON字符串里的时间字段转成datetime”的代码这段可能只出现一次但它跟订单主流程是两码事拆成parse_datetime_field(json_str, field_name)反而让主流程更清晰。另外函数也是一种“命名”。给一段逻辑起一个好名字本身就等于给代码写注释。看到一个叫cal_total_with_discount()的函数比看到10行加减乘除更容易理解业务。2. 从def开始定义一个函数需要注意的4个细节2.1 def 关键词、冒号和缩进缺一不可定义一个函数的基础语法大家都见过但新手最容易在三个地方翻车忘了冒号、缩进不对、函数体不缩进。def greet(name): 向传入的name打个招呼 message Hello, name return message第一行是函数头def是定义函数的关键词greet是函数名括号里的name是形式参数行尾必须有一个英文冒号。第二行是文档字符串docstring不是必须的但强烈建议写。第三第四行是函数体缩进一个层级Python社区习惯用4个空格这些代码才属于greet这个函数。如果函数体不缩进Python就会认为函数定义已经结束直接报IndentationError。我见过一个很搞笑的错误函数体是缩进了但后面调用函数的代码也跟着缩进了结果被当成函数的一部分函数就一直没被“外层代码”调用。缩进在Python里不是美观问题是语法问题。2.2 函数名怎么取函数名本质上就是一个变量名它遵守Python标识符的规则只能以字母、下划线开头后面可以跟字母、数字、下划线不能用数字开头不能用关键字。下面这些都是合法函数名def get_user_info(): pass def calculate_avg_score(): pass def _private_helper(): pass下面这些全是非法函数名def 123abc(): # 数字开头报错 pass def class(): # class是关键字报错 pass def my-name(): # 横杠不是合法标识符字符 pass命名建议上我个人的习惯是函数名要反映“它做了什么”最好用“动词名词”的结构比如read_file、save_order、compute_total。千万别取func1、test2这种名字临时测试还好代码一多你自己都分不清谁是谁。2.3 用docstring给函数写说明书很多人忽略docstring觉得写不写都能跑。但等你过两个星期再回来看自己的代码或者同事接手你的代码docstring的价值就出来了。def rectangle_area(width, height): 计算矩形面积。 :param width: 矩形宽度 :param height: 矩形高度 :return: 面积 return width * height在函数内部第一行写一个字符串字面量它就会被保存成函数的__doc__属性可以通过help(rectangle_area)查看很多IDE悬浮提示也会显示。这不是注释是函数“说明书”的一部分能实实在在带进开发流程里。2.4 定义函数只是在“做菜谱”不会真的执行这个点太重要了我甚至觉得很多“函数怎么不生效”的问题都出在这里。看这段代码def say_hello(): print(hello)你运行这段代码屏幕上什么都不会输出。因为def只是把函数对象创建出来绑定到say_hello这个变量上并不会去执行函数体。你可以把定义函数理解为“写菜谱”把调用理解为“照菜谱做菜”。“写完菜谱”和“做菜”是两件事写完不调用菜谱就永远躺在那里。新手经常在交互式环境里敲了一个函数定义然后大喊“怎么没反应”其实很正常。真正执行需要写一行调用say_hello()括号这一下才触发函数体执行。3. 调用函数时Python在背后做的三件事3.1 调用时Python会查名字、传参数、执行、返回调用函数看着就是函数名(参数)但Python背后其实做了几步操作。第一步根据函数名在作用域里找到对应的函数对象。这跟查变量是同一个机制找不到就会抛NameError。第二步把你写在括号里的实参跟函数定义里的形参做绑定一一对应。第三步在函数自己的作用域里执行函数体代码。第四步把return表达式的值作为结果返回给调用方如果没有return则返回None。def add(a, b): return a b result add(3, 5) print(result) # 8在这个例子里add调用时Python先把3绑定给a把5绑定给b然后执行a b最后把8返回赋值给result。3.2 为什么每次调用互不干扰函数栈每次调用函数Python都会为这次调用创建一个“运行帧”你可以把它想象成一张独立的草稿纸。函数里的局部变量都写在这张草稿纸上函数调用结束这张纸就会被处理掉。所以同一个函数调用两次函数内部的变量不会互相串门def count_up(): total 0 total 1 return total print(count_up()) # 1 print(count_up()) # 1而不是2很多新手不理解为什么第二次调用没有从1变成2就是因为每次调用都是独立的执行环境total在每次函数进入时都被初始化为0。这种“独立的执行帧”也就是函数栈的基本形态。函数被递归调用时会一层套一层地创建执行帧直到递归终止条件触发才开始逐层返回。3.3 参数传递的真相不是单纯的值传递Python官方文档的说法是“对象引用传递”通俗理解是不可变对象传“值”可变对象传“引用”。看这个直观例子def modify(x): x x 1 return x def append_one(items): items.append(1) num 10 modify(num) print(num) # 10外部不受影响 lst [1, 2, 3] append_one(lst) print(lst) # [1, 2, 3, 1]外部被改了num是整数不可变函数内部的x x 1产生了新对象外面的num没变。lst是列表可变函数内部往列表里追加元素操作的是同一个列表对象所以外部能感知到变化。这就是Python参数传递最容易被误解的地方。写函数前先想清楚我的参数是可变的还是不可变的如果函数内部会修改可变对象要不要在函数外先做一份拷贝比如def clean_data(items): items [item for item in items if item is not None] return items这里因为重新赋值了items并不会改到原来的列表。而如果是在原列表上用items.remove(...)或items.append(...)就会影响外部。4. 参数类型全解析位置参数、默认参数、*args和**kwargs4.1 位置参数按顺序塞进函数最普通的参数就是位置参数调用时按顺序一一对应。def introduce(name, age, city): print(f{name}{age}岁来自{city}) introduce(张三, 25, 上海)调用introduce(张三, 25, 上海)时Python按位置把张三给name25给age上海给city。这种调用方式最直观缺点是调用方必须记住参数顺序。万一写成了introduce(上海, 25, 张三)程序不报错但语义就错了这是位置参数最常见的隐患。4.2 默认参数千万别用可变对象给参数设默认值是很常用的操作def greet(name, greeting你好): return f{greeting}{name}当默认值是不可变对象时很安全。但一旦你写了def func(lst[])这种代码坑就来了。默认值是在函数定义时计算并保存一次的同一个列表对象会被后续所有调用共享def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2]而不是 [2]第二次调用结果不是[2]而是[1, 2]很多人第一次看到都一脸懵。正确的写法是默认值设为None在函数内部重新创建def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [2]这是Python面试里几乎必考的点也是实际开发中绝对不能踩的坑。我的建议是默认值永远用None、数字、字符串、元组这些不可变对象绝对不要用列表、字典、集合当默认值。4.3 关键字参数让调用不再靠记忆顺序调用函数时可以直接用形参名来指定“这个值给那个参数”def introduce(name, age, city): print(f{name}{age}岁来自{city}) introduce(age25, city上海, name张三)关键字参数的最大好处是调用时不用记参数顺序代码的可读性也更高。别人看到introduce(age25, city上海, name张三)一眼就知道这三个值分别是什么含义。需要注意位置参数和关键字参数可以混用但位置参数必须在关键字参数之前introduce(张三, city上海, age25) # 正确 introduce(name张三, 上海, 25) # 报错4.4 *args和**kwargs收集多余参数有时候一个函数要接收数量不定的参数这时候就用*args和**kwargs。*args把多余的位置参数收集成一个元组**kwargs把多余的关键字参数收集成一个字典def log(level, *args, **kwargs): print(级别, level) print(其他位置参数, args) print(其他关键字参数, kwargs) log(INFO, 第一条, 第二条, user张三, actionlogin)输出大概是级别 INFO 其他位置参数 (第一条, 第二条) 其他关键字参数 {user: 张三, action: login}*args和**kwargs最常见的用途有两个。一个是写装饰器和包装函数时需要把任意参数透传给被装饰的函数另一个是封装底层库时不想把库的所有参数都显式写出来直接用**kwargs透传。函数签名里不一定非要叫args、kwargs这两个名字关键是星号但你按惯例写这两个名字别人一看就懂。如果你在调用时反过来用把列表或字典“展开”传进函数那就是参数解包params [1, 2, 3] print(*(params)) # 等价于 print(1, 2, 3)4.5 参数组合的顺序一个函数里可以混用全部参数类型但顺序有强制规则。我的记忆口诀是位置参数在前然后是默认参数然后是*args再然后是只能按关键字传入的参数最后才是**kwargs。def example(a, b2, *args, c, **kwargs): pass这个函数里a是必填位置参数b有默认值*args收集多余的位置参数c是“仅限关键字参数”在*args后面的参数只能通过关键字传**kwargs收集多余的关键字参数。这种复杂签名在一般业务代码里不常用但看第三方库源码时会经常遇到。参数类型对比我整理成了一张表方便你对照参数类型写法调用方式典型场景位置参数def f(a, b)f(1, 2)最常见的参数默认参数def f(a, b2)f(1)可选配置项关键字参数def f(a, b)f(b2, a1)提升调用可读性仅限关键字参数def f(a, *, b)f(1, b2)强制调用方写清参数名任意位置参数def f(*args)f(1, 2, 3)可变参数任意关键字参数def f(**kwargs)f(a1, b2)配置项采集、透传5. return返回值与变量作用域不弄清这俩真会出事5.1 return返回了什么没写return呢return用来指定函数执行完后返回给调用方的结果。一个函数只能有一个返回值其实不是return语句可以执行很多次但碰到第一个就结束了def check_age(age): if age 0: return 年龄不能为负数 if age 18: return 未成年 return 成年这里有三个return但一次调用只会执行到一个一旦执行return函数立刻结束后面的代码不再运行。这也是很多人喜欢用“早返回”风格整理逻辑的原因。如果你没写return或者写了return但不带值函数会返回Nonedef show(): print(我在跑) result show() print(result) # None我见过很多新手在这上面栽跟头函数里用print打印了结果外面一接收发现是None以为函数没执行。你要记住print是把值输出到控制台return才是把值交还给调用方。函数要“给结果”靠的是return。5.2 多个返回值的本质其实是元组Python函数“返回多个值”的写法很简def get_person(): name 张三 age 25 return name, age name, age get_person()这里看起来返回了两个值实际返回的是一个元组(张三, 25)name, age get_person()就是元组解包。你甚至可以这样result get_person() print(result) # (张三, 25)在一个需要同时返回多个状态和数据的函数里这种写法很实用。但要注意返回多个值时调用方必须解包否则你拿到的就是一个元组不拆开的话用起来容易出怪问题。5.3 LEGB规则和global变量在函数里怎么访问遵循LEGB规则Local - Enclosing - Global - Built-in。x 100 # Global def func(): x 10 # Local print(x) # 访问的是局部 x输出 10 func() print(x) # 输出 100外面的 x 没变函数内部的x 10是新建了一个局部变量跟全局的x无关。如果你在函数里想修改全局变量直接写是不行的count 0 def increment(): count 1 # 报错局部变量 count 在赋值前被引用 increment()Python看到函数内部有count 1会把count当成局部变量但它在执行时还没有本地定义于是抛UnboundLocalError。解决方法是加global声明count 0 def increment(): global count count 1 increment() print(count) # 1global的使用原则是能不用就不用。全局变量会让函数依赖外部状态测试起来很麻烦。更推荐的做法是把状态作为参数传进去把新结果作为返回值带出来保持函数的“纯净”。LEGB查找顺序可以总结成下面这张表层级含义说明LLocal当前函数内部的局部变量EEnclosing外层嵌套函数的局部变量GGlobal模块级别的全局变量BBuilt-inPython内置的名字如len、print5.4 嵌套函数、闭包与nonlocalPython允许在函数内部再定义函数这就是“函数的嵌套定义”。嵌套定义最常见的两个用途一个是把只在某函数内部使用的辅助逻辑藏在内部另一个是制造闭包。def outer(x): def inner(y): return x y return inner add_5 outer(5) # add_5 现在是一个函数绑定了 x5 print(add_5(3)) # 8inner引用了外层函数的变量x即便outer已经调用结束inner依然记得x的值这种结构就是闭包。闭包在很多场景下非常有用比如根据配置生成不同的处理函数。如果在嵌套函数里想要修改外层函数的局部变量不能用global要用nonlocaldef outer(): count 0 def inner(): nonlocal count count 1 return count return inner counter outer() print(counter()) # 1 print(counter()) # 2nonlocal告诉Python“这个变量不是本层定义的去上一层作用域找。”没有它count 1同样会报UnboundLocalError。嵌套调用和嵌套定义是两回事需要区分一下。嵌套调用指的是函数A调用函数B这是非常普通的事每天写代码都会发生嵌套定义是在函数里用def再声明一个新函数一般是为了隔离逻辑或构造闭包。6. 函数也是对象回调、内置函数与装饰器入门6.1 函数名其实是个变量函数也能当参数这是Python里改变你写代码方式的一个认知函数名就是一个绑定到函数对象的变量名函数对象可以赋值给别的变量也可以放进列表更可以当作参数传给另一个函数。def say_hello(): return hello my_func say_hello print(my_func()) # hello这个特性带来一个很酷的用法——函数作为参数传递def apply(func, value): return func(value) result apply(len, hello) print(result) # 5len被作为参数传进apply在内部执行len(hello)。这种“把函数传来传去”的能力是后面一切回调、装饰器、高阶函数的基础。6.2 回调函数解决什么问题回调函数就是“先把函数传进去等条件满足时再执行它”。举个后端常见的例子处理一批任务每个任务完成后要通知外部系统。你可以把“通知”这个动作写死也可以把通知函数作为回调传进去def process_task(task, on_done): # 假装处理任务 result task.upper() on_done(result) def notify(result): print(任务完成结果是, result) process_task(abc, notify)把notify作为回调传给process_task处理完成后自动调用。这样process_task不用关心通知细节通知方式改变时只需要换一个回调函数不用改主逻辑。框架代码里这种设计尤其常见比如按钮点击事件、定时器到期和异步任务完成都会调用你注册的回调函数。理解了“函数可以当参数”理解回调函数就水到渠成了。6.3 内置函数其实也是函数sorted的key了解一下Python提供了大量内置函数比如len、max、min、sorted、map、filter。它们本身就是函数用起来跟你自己定义的函数没有本质区别。sorted里的key参数是“函数作为参数”的经典例子。默认情况下sorted按元素大小排序但如果你要按别的规则排就可以传一个函数进去words [bb, a, ccc] print(sorted(words, keylen)) # [a, bb, ccc]按长度排序 scores [(张三, 80), (李四, 95), (王五, 78)] print(sorted(scores, keylambda item: item[1], reverseTrue))这里用了lambda它就是一个匿名函数功能上和def定义的函数一样只是没有名字适合写这种简单的单行表达式。lambda用多了反而会影响可读性我个人建议逻辑超过一行就别用lambda老老实实def。6.4 装饰器原来是“返回函数的函数”装饰器是Python里一个很精妙的设计网上教程特别多其实它背后就一句话装饰器是一个函数它接收一个函数返回一个新函数。def log(func): def wrapper(*args, **kwargs): print(f调用 {func.__name__}) return func(*args, **kwargs) return wrapper def add(a, b): return a b add log(add) print(add(2, 3))输出调用 add 5log(add)执行了什么log接收add函数返回wrapper函数wrapper在执行时会先打印日志再调用原来的add并返回结果。最后add log(add)这一句等价于把原来的add换成了带日志功能的wrapper。装饰器语法糖log只是把“add log(add)”这个赋值动作简化了log def add(a, b): return a b理解装饰器的关键是你最初定义的add函数在执行时已经不再是原来的那个函数了而是被包装后的wrapper只是对外看起来还叫add而已。7. 新手翻车现场常见报错与排查技巧7.1 NameError函数都没定义就调用这是入门阶段出镜率最高的报错print(add(1, 2)) # NameError: name add is not defined def add(a, b): return a b报错原因很直白调用add的时候Python还没见过add这个名字函数定义在后面。解决方法是把定义放在调用之前或者把调用放进一个函数里等模块加载完再执行。我特别建议把“入口调用”写在if __name__ __main__:下面这样既能避免导入模块时误执行调用也能让代码顺序更清楚。7.2 IndentationError缩进逼疯多少人def func(): print(hello) print(world) # IndentationError: unindent does not match any outer indentation levelPython靠缩进区分代码块空格和制表符混用更糟糕报错信息有时候连看都看不懂。解决办法很简单全项目统一用4个空格编辑器把Tab自动转换成空格。说到编辑器我见过太多人问“为什么我的函数总报缩进错”最后发现是代码里既有Tab又有空格把不可见字符显示出来一眼就能看到问题。IndentationError还有个变种就是函数体为空的时候def my_func(): # 这里什么都不写这种必须加一行pass占位否则会报IndentationError: expected an indented block。写骨架代码时先用pass占位是很正常的操作。7.3 TypeError参数数量对不上参数相关报错大多长这样def func(a, b): return a b func(1) # TypeError: func() missing 1 required positional argument: b func(1, 2, 3) # TypeError: func() takes 2 positional arguments but 3 were givenTypeError的报错信息其实已经说得很清楚了一个是“缺少必填参数”一个是“给了太多参数”。很多新手拿到报错不去读后半句只看一个TypeError就开始慌其实把报错信息完整读一遍大半问题都能定位。还有一个很典型的TypeError出现在类方法里class Person: def say_hello(self): print(hello) p Person() p.say_hello() # 正常 Person.say_hello() # 缺少 self 参数报错实例方法至少需要一个self参数用类调用时self没有被自动绑定就会报出类似“takes 0 positional arguments but 1 was given”的错。新手看这个报错经常一头雾水其实是没分清“实例调用”和“类调用”的区别。7.4 pip无法识别和环境问题很多人的第一个Python环境问题长这样在Windows的PowerShell或cmd里敲pip install numpy结果弹出“pip : 无法将‘pip’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这通常意味着pip的可执行文件路径没有被加入系统PATH或者当前Python环境里根本没有pip。我的处理经验是这套先用模块方式调用python -m pip install numpy看看能不能跑通。这个写法不依赖PATH里的pip更稳定。如果python本身也提示无法识别那说明Python没装好或者没进PATH需要重新安装并勾选“Add Python to PATH”。如果你的机器上装了多个Python版本那还得注意pip到底对应哪个Python环境。最稳妥的方式是在命令行里确认python -V和你期望的版本一致再用python -m pip来安装包。环境问题很大概率就是“版本混了”或者“路径不对”不是代码问题。7.5 能不能让IDE准确跳转到函数定义“函数没法跳转”是另一个高频问题搜一下能出来一堆“vscode c所有的函数变量都没办法跳转”“clion无法跳转到函数定义处”的帖子。Python这边遇到跳转失败我通常按顺序排查三件事。第一确认IDE选对了Python解释器。在VSCode里按CtrlShiftP执行“Python: Select Interpreter”把解释器指到你正在用的那个Python否则索引会乱。第二看项目有没有大范围的动态代码比如动态生成函数、大量用装饰器重写函数名这类代码IDE静态分析很难识别跳转失败是正常的。第三检查索引是否过期手动重建索引或者重启IDE很多时候就好了。说实话如果代码是你自己刚写的IDE跳不跳转其实不影响运行。但如果是接手别人的大项目能准确跳转对理解代码帮助很大所以这套排查值得掌握。7.6 排查函数问题的“三板斧”我排查函数相关bug有一套固定的顺序基本能覆盖八成问题。第一板斧先确认函数有没有被调用。很多人以为定义了就会执行结果程序跑完什么都没发生。在函数入口加一行print(函数进入)立刻就知道有没有进来。第二板斧确认参数值是不是符合预期。在函数开头把每个关键参数打印出来看看是不是你想象的值。如果参数不对问题在调用方不在函数内部。第三板斧确认返回值有没有被正确接收。很多函数内部计算结果了但外面没接住或者函数根本没写return结果就是外面拿到None。检查返回值和接收方式是很关键的一步。这三个板斧看着基础但确实能解决绝大多数函数相关的问题。等熟练以后再逐步替换成断点调试效率会更高。8. 综合实操用函数拼出一个真正能跑的词频统计工具8.1 需求拆解与函数划分前面讲了一大堆最后用一个完整例子串一遍。假设我要写一个文本词频统计工具核心需求是读取指定文件清洗非中英文字符统计每个词出现次数按次数排序输出前10名。很多人的第一反应是写一大段代码把整个流程串下来。但读完前面内容你应该已经有“拆函数”的意识了。我把它拆成这几个函数read_text(path)负责读文件返回文本内容。clean_text(text)负责清洗把标点和特殊字符去掉。count_words(text)统计词频返回字典{词: 次数}。sort_by_count(counts)按次数降序排序。main()串联以上步骤输出结果。每个函数只干一件事改动时只动对应的函数就行。比如清洗规则变了改clean_text其他函数不用动。8.2 完整实现直接看代码import re def read_text(path): 读取文本文件返回字符串内容 with open(path, r, encodingutf-8) as f: return f.read() def clean_text(text): 清洗文本只保留中英文和数字统一成小写 text text.lower() return re.sub(r[^\w\u4e00-\u9fa5], , text) def count_words(text): 统计每个词出现的次数返回字典 words text.split() counts {} for word in words: counts[word] counts.get(word, 0) 1 return counts def sort_by_count(counts, top_n10): 按出现次数从高到低排序默认取前10个 return sorted(counts.items(), keylambda item: item[1], reverseTrue)[:top_n] def main(): text read_text(sample.txt) text clean_text(text) counts count_words(text) top_words sort_by_count(counts, top_n10) for word, num in top_words: print(word, num) if __name__ __main__: main()这个例子把之前讲到的点都用上了read_text里有returnclean_text用正则清洗count_words用dict.get简化计数sort_by_count用默认参数top_n10sort_by_count里的lambda就是“函数作为参数”的典型用法main作为入口函数把所有环节串起来最后用if __name__ __main__来保护入口调用。8.3 运行结果与复盘假设sample.txt里是一段英文文章运行后会输出the 15 python 10 code 8 function 6 ...这个输出就是(词, 次数)的元组序列。整个过程里main函数没有处理具体“怎么读、怎么清洗、怎么计数”的细节它只负责调度代码的可读性和可维护性一下子提升了。如果你想把统计结果保存到文件只需要再加一个save_result(words, output_path)函数然后在main里插入一行调用。因为函数划分清晰改动成本很低这就是“重构函数”带来的实际好处。我在写这种工具类脚本时习惯把参数也暴露出来比如让文件路径可以通过命令行传入而不是写死在代码里。等你学会拆函数、用参数和返回值打通数据流再回看之前那些几十行写成一坨的代码你会明显感觉到函数化之后代码才真正“活”了。