1. 函数的本质:把积木拼成机器
先说个场景。你刚学完变量和循环,写了这段代码:
score1 = 85 if score1 >= 60: print("及格") else: print("不及格") score2 = 42 if score2 >= 60: print("及格") else: print("不及格")写两个判断还好,如果写十个、一百个呢?键盘盲打速度再快,这种复制粘贴也难免出问题——漏改一个变量名就是Bug。更麻烦的是,某天产品经理过来跟你说:“及格线改成70分”。你得从头到尾把所有if重新翻一遍,漏改一处就等着背锅吧。
这正是函数的诞生动机。函数 = 一段可以被反复调用的命名代码块,它把“判断是否及格”这个逻辑封装成积木,每块积木做好接口(参数和返回值),剩下的就是拼装。Python里万物皆对象,函数也不例外——它能被赋值给变量、塞进列表、传进另一个函数,这些高阶玩法以后会碰到,但今天先把地基打好。
这堂课的内容是Python语法第三课,我会从零开始讲清楚四件事:
- 如何定义一个函数(
def语句、函数名规则、缩进陷阱) - 参数到底有哪些玩法(位置参数、默认参数、
*args、**kwargs) - 返回值是什么(
return的机制、多值返回、隐式None) - 作用域怎么影响你的变量(局部变量、全局变量、
global、nonlocal)
每部分都会配上直接能跑的代码和解释为什么这么做,不整虚的。
2. 函数的定义与调用:先学走,再学跑
2.1 def语句的完整语法
定义一个函数,Python的语法非常简洁,只需要def关键字:
def greet(name): """接收一个名字,打印问候语""" print(f"你好,{name}!")拆开看,一个函数定义由四个部分组成:
- def关键字:告诉Python“我要定义一个函数了”
- 函数名:遵循变量命名规则,只允许字母、数字、下划线,且不能以数字开头。建议用下划线分隔多个单词(snake_case),比如
calculate_average_score,别用calculateAverageScore那种驼峰风格,会被人嫌弃 - 参数列表:写在圆括号里,多个参数用逗号分隔。参数可以没有,但不能省略圆括号
- 冒号和缩进体:冒号表示函数体的开始,下面的缩进代码块就是函数的具体逻辑。缩进必须一致,同一个函数内用4个空格(千万别混Tab)
这里还有一行"""接收一个名字,打印问候语""",这叫文档字符串(docstring)。它不是注释,而是函数的元数据,可以通过greet.__doc__访问,很多工具能自动提取它生成API文档。养成写docstring的习惯,三个月后回头看自己的代码会感谢当时的手。
调用函数的方式也很直接:函数名加上一对括号:
greet("张三") greet("李四") greet("王五")输出三行问候语。每当执行到greet(...),Python就跳进函数体内,跑完再回到原来的位置继续往下走。这个“跳进去再跳回来”的过程叫函数调用栈,理解它有助于理解后面的作用域和递归。
2.2 函数定义放在哪里
Python是一门解释型语言,代码从上到下执行。所以函数必须先定义,后调用。你把调用写在定义前头,解释器会甩给你一个NameError: name 'greet' is not defined。
初学者常犯的错误是把函数定义放在文件末尾,然后顶部直接调用。如果是因为主流程的逻辑更清晰而想把函数堆到底部,可以这样做:
def main(): greet("张三") def greet(name): print(f"你好,{name}!") if __name__ == "__main__": main()main()在文件末尾才被真正调用,而调用它的时候前面的greet已经定义完成,所以不会报错。这也是Python项目最常见的组织方式:函数定义在前,主入口在后。注意if __name__ == "__main__":这个判断的用途是“只有直接运行当前文件时才执行里面的代码,被import时不执行”,现在不理解没关系,先把结构记住。
2.3 空函数怎么写
调试阶段经常需要先搭框架,函数体还没想好。Python不允许空的函数体,但你也不想写一堆占位垃圾代码。解决办法是用pass占位:
def command_parse(raw_input): # TODO: 解析输入格式,这一步还没实现 passpass是一条什么都不做的语句,专门用来占位,让语法合法。类似的还有...(Ellipsis),也能起到相同效果:
def command_parse(raw_input): ...不过从可读性角度,pass加注释比...更直观。记住:pass不是注释,它是实际执行的空操作,只是副作用为零。
3. 参数详解:函数和外部世界的桥梁
参数是函数设计的核心。设计得好的参数列表就像一把设计精巧的钥匙,能适应多种锁,又不会让使用者一头雾水。Python提供了整整一套参数体系,我按从简单到复杂的顺序逐个拆。
3.1 位置参数:最朴素的传参方式
调用函数时,实参按顺序依次对应到形参,这就是位置参数:
def introduce(name, age, city): print(f"我叫{name},今年{age}岁,来自{city}") introduce("张三", 25, "杭州")传入的三个值跟name、age、city一一对应。这套机制很直觉,但有个明显的缺点——可读性差且容易错位。如果实参顺序颠倒了,比如introduce("杭州", 25, "张三"),程序不会报错,但输出完全错乱。三个参数还好,八个参数出错概率飙升。
解决之道是使用关键字参数:
introduce(name="张三", city="杭州", age=25)调用时指定形参名字,Python会按名字对号入座,顺序随便排。更妙的是,位置参数和关键字参数可以混用:
introduce("张三", city="杭州", age=25)这里有两条硬性规则值得死记:
- 位置参数必须出现在关键字参数之前,
introduce(city="杭州", "张三")直接语法错误 - 同一个参数不能用两种方式传两次
我把这些规则整理成一张速查表,后面实操时候可以直接回来看:
| 传参方式 | 示例 | 适用场景 |
|---|---|---|
| 纯位置参数 | introduce("张三", 25, "杭州") | 参数少、顺序好记、调用频率高 |
| 纯关键字参数 | introduce(name="张三", age=25, city="杭州") | 参数多、意义不直观的名称 |
| 混合传参 | introduce("张三", age=25, city="杭州") | 前面几个必须传的用位置,其他用关键字 |
3.2 默认参数:省去重复输入的烦恼
很多函数参数有“绝大多数情况下都取这个值”的特征。比如写日志函数,正常都要带时间戳,但偶尔不需要。用默认参数解决:
def log_msg(message, level="INFO", with_time=True): if with_time: print(f"[{datetime.now()}] [{level}] {message}") else: print(f"[{level}] {message}") log_msg("服务启动") # 用默认级别和时间戳 log_msg("磁盘空间不足", "WARNING") # 改级别,时间戳照旧 log_msg("敏感操作审计", "ERROR", False) # 三个参数全部覆盖默认参数的语法规则很简单:带默认值的参数必须出现在不带默认值的参数右边。为什么?如果写成def log_msg(message="默认消息", level),调用时log_msg("外部输入")到底该把它当成message还是level?Python为了避免歧义,直接禁止这种写法。
这里有一个最著名的坑,新手踩了无数次:默认参数如果是可变对象(列表、字典、集合),函数会保留上次调用的状态。
def add_item(item, container=[]): container.append(item) return container print(add_item("苹果")) # ['苹果'] print(add_item("香蕉")) # ['苹果', '香蕉']?!第二次调用结果出乎意料,因为container=[]这个列表在函数定义时被创建了一次,之后所有调用共享同一个列表。正确做法是——默认值设为None,函数体内每次新创建:
def add_item(item, container=None): if container is None: container = [] container.append(item) return container这个技巧的经验之谈是:默认参数尽量用不可变对象(None、数字、字符串、元组)。id同一个对象导致的状态泄露问题,排查起来极其隐蔽。
3.3 可变参数:*args与**kwargs
有时候你写一个函数,到底几个参数在调用时才能确定。比如求平均值、拼接多个字符串、配置几十个不确定键值的选项。这时候就要用到可变参数。
*args接收任意多个位置参数,打包成一个元组:
def average(*numbers): if len(numbers) == 0: return 0 return sum(numbers) / len(numbers) print(average(1, 2, 3)) # 2.0 print(average(10, 20)) # 15.0 print(average()) # 0参数名不一定是args,重点是单星号。*nums、*numbers、*nums_list都行,但业界约定俗成用*args。函数内部,numbers是一个元组,可以遍历、切片、求长度。
**kwargs接收任意多个关键字参数,打包成一个字典:
def register_user(name, **info): print(f"用户:{name}") for key, value in info.items(): print(f"{key}: {value}") register_user("张三", age=20, city="杭州", phone="13800138000")**info把调用时多余的关键字参数收集成字典。用途很广:写配置类、ORM查询条件、装饰器、类继承中的super().__init__等。
组合起来的基本范式是:
def func(必需参数, *args, 默认参数, **kwargs): pass顺序规矩是:普通参数 →*args→ 默认参数 →**kwargs。这里面有个细节,*args后的默认参数必须用关键字传参:
def func(a, *args, b=10): print(a, args, b) func(1, 2, 3, b=100) # 输出: 1 (2, 3) 1003.4 强制关键字参数:*的独特用法
有时候你希望某些参数“只能用关键字方式传”,避免使用者搞错顺序。Python支持一种天才般的语法——在参数列表里放一个单独的*:
def register_user(name, *, age, city): print(f"{name}, {age}岁, {city}") register_user("张三", age=20, city="杭州") # 正常 register_user("张三", 20, "杭州") # TypeError: 多了位置参数*之后的参数被视为强制关键字参数。这种设计这么写的原因很实际——age和city都是容易混淆的概念,禁止位置传参可以杜绝错序。Python很多内置函数都这么干,比如sorted(iterable, *, key=None, reverse=False),强行让你写key=...而不是sorted(lst, None, True),从源头规避低级失误。
3.5 参数解包:把列表字典拆开传
与*args收集相反的操作是解包。一个函数接收三个固定参数,你偏偏有一个装好三个值的列表,可以用星号拆开:
def spread(a, b, c): return a + b + c nums = [10, 20, 30] print(spread(*nums)) # 60字典也能用双星号解包成关键字参数:
config = {"a": 1, "b": 2, "c": 3} print(spread(**config)) # 6这两个技巧在封装接口、从配置文件读取参数时非常实用。记住一个对应关系:一个星号拆可迭代对象当位置参数,两个星号拆字典当关键字参数。
4. 返回值详解:函数与调用方的协议
4.1 return到底做了什么
函数计算的最终结果必须通过return送出去,不然调用方拿到的全是None。遇到return,Python会做两件事:
- 立刻结束函数执行,后面的代码不会跑
- 把
return后面的表达式的值返回给调用方
def check_score(score): if score >= 60: return "及格" print("这句话只有在不及格时才会输出") return "不及格"return可以出现在函数体的任意位置,用多重return做条件早退是常见写法——效率高,还减少了嵌套层级。
4.2 多值返回的本质是元组
一个函数能不能返回多个值?能,但要理解本质:
def min_max(nums): return min(nums), max(nums) # 看起来返回了两个值? result = min_max([3, 1, 4, 1, 5]) print(result) # (1, 5),一个元组!你其实是返回了一个元组,Python自动把逗号分隔的表达式打包成元组。既然调用方拿到元组,就可以用解构赋值:
low, high = min_max([3, 1, 4, 1, 5]) print(f"最小值{low},最大值{high}")这种“多值返回 + 解构赋值”的组合是Python里非常优雅的惯用写法,大量内建函数和第三方库都这么设计。
4.3 没有return的函数返回None
所有函数都有返回值,即使你没写return。这时候Python隐式返回None。比如:
def do_nothing(): pass print(do_nothing()) # NoneNone在Python里是一个特殊的单例对象,表示“什么都没有”。在函数设计中,返回None有几层含义:
- 函数只负责副作用(打印、写文件、改列表),不产出计算结果
- 函数正常结束但没找到合适的结果,用
None表示“无匹配”
但这里有个常见的逻辑陷阱——布尔判断与None的混淆。写代码时注意区分:
result = find_user(user_id) if result is None: print("用户不存在")而不是:
if not result: print("用户不存在")因为如果find_user返回空列表[]、空字符串""、数字0,它们也是“假值”,但含义跟“用户不存在”可能完全不同。正确姿势是用is None判断,保持逻辑精确。
4.4 返回值的类型一致性
这是很多函数设计问题的根源——同一个函数尽量不要一会儿返回字符串,一会儿返回整数,一会儿返回None。如果你面试造轮子,或者维护一个被五十个函数调用的公共接口,类型不统一的返回值会让调用方苦不堪言。
反面例子:
def get_score(student_id): if student_id == "001": return 90 elif student_id == "002": return "缺考" else: return None你拿到这个结果想算平均值?90 + "缺考"直接TypeError。好的设计是:
def get_score(student_id): """ 返回成绩,如果学生不存在或缺考,返回None """ if student_id not in students: return None score = students[student_id] return score if isinstance(score, int) else None至少在接口层面约定好——同一函数返回值类型一致。Python的类型注解def get_score(student_id: str) -> Optional[int]:是你以后控制这个的有力工具,但现在先建立意识。
5. 作用域详解:变量活在哪里
作用域是我见过最让初学者头晕的概念之一,但它的底层逻辑其实特别简单——你定义一个变量,它能被谁看见?
5.1 LEGB法则
Python查找变量时遵循LEGB规则,顺序是:
- L - Local(局部作用域):当前函数内部定义的变量
- E - Enclosing(外层嵌套作用域):外层函数里定义的变量(闭包场景)
- G - Global(全局作用域):模块顶层定义的变量
- B - Built-in(内建作用域):Python预置的
len、print、range等
看个例子:
x = "全局x" # Global def outer(): x = "外层x" # Enclosing,相对inner是外层 def inner(): x = "内层x" # Local print(x) # 输出"内层x" inner() outer()程序执行到print(x)时,按LEGB顺序先找Local里的x,找到了就用它,不会往上找。这个查找机制解释了“为什么我在函数里定义的变量,外面用不了”。
5.2 局部变量和全局变量
默认情况下,在函数内部给一个变量赋值,Python把它当成全新的局部变量,即使用这个名字跟全局变量一样:
count = 10 def add_one(): count = count + 1 # UnboundLocalError!这个报错是新手最经典的谜题。表面看count是全局的,加一不就行了?但Python的规则是——只要函数体内出现赋值语句,解释器就在编译阶段把这个名字标记为局部变量。于是执行到count + 1时,count还是局部变量,但还没赋值,就报“使用前未绑定”的错误。解决办法是用global关键字声明:
count = 10 def add_one(): global count count = count + 1 add_one() print(count) # 11但我得说句实话,global尽量少用。全局变量把函数从“独立积木”变成了“依赖外部环境的顺风车”,导致调试困难、函数无法复用。业务上实在需要共享状态,优先考虑传参和返回值:
count = 0 def add_one(n): return n + 1 count = add_one(count)这样add_one完全不依赖外部环境,测试起来倍儿轻松。
5.3 闭包与nonlocal声明
有嵌套函数时,内层函数可以访问外层函数的变量,这叫闭包。但如果你想在内层函数里修改外层函数的变量,global做不到,因为它作用于模块全局,在外层函数里定义一个变量之后用global会直接跑去动模块级变量。这时候需要nonlocal:
def counter(): total = 0 # Enclosing作用域 def increment(): nonlocal total # 声明要修改外层变量 total += 1 return total return increment c1 = counter() print(c1()) # 1 print(c1()) # 2 print(c1()) # 3闭包能实现“记忆功能”——函数记着上次调用的状态,还不污染全局变量。装饰器模式就是基于闭包实现的,以后你会大量遇到。nonlocal的关键在于,它只向上找最近的Enclosing作用域,不会管Local和Global。
实际经验里,闭包很强大但别滥用。状态多到五六个的时候,写个类往往比闭包好维护得多。函数闭包适合状态少、逻辑精简的场景。
5.4 作用域对函数设计的影响
作用域规则对日常编程的启示有三条:
第一,函数内部不要随意修改全局变量。真要改,用return把新值传出来由调用方决定。这种单向数据流让代码可预测性大大提升。
第二,不要在函数内部直接依赖全局变量来修改逻辑。全局变量的存在让函数变“脏”——你没传它,它却影响结果。如果某个值函数要用,就明确通过参数传,宁可调用麻烦点。
第三,正式代码里,少用单字母变量名。也许在一个几十行的函数里,i、x、t完全看得懂;但当函数超过100行、嵌套三层循环之后,单字母变量必然会让人抓狂。用index、position、elapsed_time花不了几秒打字,却能让后来维护代码的人少掉很多头发。
我用一个表格帮大家记忆重点:
| 场景 | 访问外层变量没问题 | 修改外层变量 | 关键字 |
|---|---|---|---|
| 函数内访问全局 | ✔ | ✖(默认) | global |
| 内层函数访问外层 | ✔ | ✖(默认) | nonlocal |
| 内层函数访问全局 | ✔ | ✖(默认) | global |
“✔”只代表访问,不代表修改。修改永远是那个需要显式声明的例外。
6. 实操:写一个成绩统计工具
理论和语法讲了这么多,我们直接做一个完整的小项目把这些知识串起来。需求:写一个成绩统计模块,支持录入学生成绩、计算平均分、判断及格、打印排名,并统计不同分数段人数分布。
6.1 明确功能拆分
任何项目第一步不是写代码,而是拆功能。这个成绩统计工具按函数划分:
record_score(name, score):录入一条成绩(用全局字典存储)average_score():计算平均分pass_rate():计算及格率rank_students():按分数从高到低排名score_distribution():统计优(≥90)、良(80-89)、中(70-79)、及格(60-69)、不及格(<60)各组人数
每个函数各司其职,互不越界,这就是函数式拆分的核心思想。
6.2 代码实现
"""成绩统计工具:综合运用定义、参数、返回值与作用域""" # 全局变量:保存所有学生成绩 student_scores = {} def record_score(name, score): """录入学生成绩。 参数: name: 学生姓名(字符串) score: 学生分数(数值,0-100) 返回: bool: 是否录入成功 """ if not (0 <= score <= 100): print(f"分数不合法:{score},应在0-100之间") return False student_scores[name] = score print(f"已录入:{name} -> {score}分") return True def average_score(scores=None): """计算平均分。 参数: scores: 可选的成绩字典,不传则使用全局数据 返回: float: 平均分,没有成绩时返回0 """ if scores is None: scores = student_scores if len(scores) == 0: return 0.0 return sum(scores.values()) / len(scores) def pass_rate(scores=None): """计算及格率(≥60分)。""" if scores is None: scores = student_scores if len(scores) == 0: return 0.0 passed = sum(1 for s in scores.values() if s >= 60) return passed / len(scores) * 100 def rank_students(scores=None): """按分数降序排列学生。 返回: list[tuple]: [(分数, 姓名), ...] 从高到低 """ if scores is None: scores = student_scores return sorted( [(score, name) for name, score in scores.items()], reverse=True ) def score_distribution(scores=None): """统计分数段人数分布。 返回: dict: {'优': n, '良': n, '中': n, '及格': n, '不及格': n} """ if scores is None: scores = student_scores levels = {"优": 0, "良": 0, "中": 0, "及格": 0, "不及格": 0} for s in scores.values(): if s >= 90: levels["优"] += 1 elif s >= 80: levels["良"] += 1 elif s >= 70: levels["中"] += 1 elif s >= 60: levels["及格"] += 1 else: levels["不及格"] += 1 return levels if __name__ == "__main__": record_score("张三", 88) record_score("李四", 59) record_score("王五", 93) record_score("赵六", 75) record_score("陈七", 43) print(f"平均分:{average_score():.1f}") print(f"及格率:{pass_rate():.1f}%") print("排名(高到低):") for score, name in rank_students(): print(f" {name}: {score}分") print("分数段分布:") for level, count in score_distribution().items(): print(f" {level}: {count}人")运行结果:
已录入:张三 -> 88分 已录入:李四 -> 59分 已录入:王五 -> 93分 已录入:赵六 -> 75分 已录入:陈七 -> 43分 平均分:71.6 及格率:60.0% 排名(高到低): 王五: 93分 张三: 88分 赵六: 75分 李四: 59分 陈七: 43分 分数段分布: 优: 1人 良: 1人 中: 1人 及格: 0人 不及格: 2人6.3 代码里的设计心思
这段代码虽然短,但每一处都有设计的考量,逐条解读:
默认参数scores=None的使用。我原本想设计成“不传就操作全局数据,传入就用局部数据”,这样既方便直接用全局数据跑,也能随时传任意字典做单元测试。在def阶段我们说过,默认参数别用可变对象——这里None是绝佳的占位符,完美避开了共享状态坑。
有return的录入函数。record_score返回了bool,这样调用方可以判断录入是否成功,决定下一步逻辑。如果它就是默默打印,外部完全无法控制流程。真正的函数设计,返回值是最重要的“沟通协议”。
统计函数里的生成器表达式。sum(1 for s in scores.values() if s >= 60)这句本质上是一个极简的循环,语义是“对满足条件的每一项贡献计数”。它等价于:
count = 0 for s in scores.values(): if s >= 60: count += 1在掌握了基础写法之后,熟练使用这种表达式能大幅减少样板代码。
if __name__ == "__main__":把主流程包起来了。以后你把这个模块import进其他项目,不会触发测试代码的执行;只有直接运行它才走主流程。这是Python模块化开发的惯例,从第一课就养成这个习惯。
7. 常见问题与排查技巧
操作中踩坑是难免的,把这些高频问题提前过一遍,能省下大量抓耳挠腮的时间。
7.1 常用参数相关报错速查表
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
TypeError: missing 1 required positional argument: 'name' | 少了必要位置参数 | 调用时检查是否漏传参数,或给参数设默认值 |
TypeError: got multiple values for argument 'name' | 同一个参数被位置传一次、关键字又传一次 | 要么只用位置,要么只用关键字 |
TypeError: unexpected keyword argument 'xxx' | 函数定义的参数列表里没有xxx | 检查参数名是否拼写正确,或调整函数定义加**kwargs |
SyntaxError: non-default argument follows default argument | 默认参数前面还有非默认参数 | 把无默认值的参数移到有默认值的参数前 |
UnboundLocalError: local variable 'x' referenced before assignment | 函数内对全局变量赋值,Python识别成局部变量 | 在函数开头加global x,或用返回值绕过 |
7.2 参数默认值共享的惨案
刚才讲过def add_item(item, container=[])的坑。这里再多说一句——其实很多教科书都会强调“不要用可变对象当默认参数”,但大家依然会踩,原因是它不会立即报错,而是隔几次调用后突然蹦出诡异结果。我的建议是,凡是默认参数需要用到列表、字典这类容器,一律用None+手动创建。这不是风格之争,是bug的物理免疫。
7.3 return位置写错
初学者常把return写在for循环里面,导致循环只跑第一次就提前退出:
def first_over_sixty(scores): for name, score in scores.items(): if score >= 60: return name # 只找第一个就返回了如果这不是你要的行为,应该在外面收集结果:
def all_over_sixty(scores): result = [] for name, score in scores.items(): if score >= 60: result.append(name) return result判别方法非常直接——return一执行函数就结束,所以想累积多个结果,把这个逻辑想清楚了再写。
7.4 忘记调用括号
这算是最隐蔽的错误之一:
def greet(): return "你好" print(greet) # <function greet at 0x...>你没有写括号,Python不会报错,它打印的是函数对象本身,而不是调用结果。排查这类问题,先在脑子里过一遍“这行代码执行的是函数体还是函数对象本身?”这个问题。函数名加上括号才表示“执行它”,没有括号就只是“引用它”。
7.5 可变参数的顺序陷阱
写带多个不同类型的参数时,顺序写错是另一个高频报错。比如:
def func(a, *args, b=10, **kwargs): pass注意,b=10必须放在*args后面,否则Python无法区分这个默认参数究竟属于哪一类。想验证的同学可以试试这个顺序:
def func(a, b=10, *args): # 这样其实合法,但b不再是“强制关键字”了 print(a, b, args)func(1, 2, 3, 4)会得到1 2 (3, 4),b被位置参数2填掉,3, 4进了args。这与你预期的“b是默认10,args收集所有剩余参数”完全不同。所以如果想让默认参数不被位置参数抢占,就把它放在星号后面。
8. 写在最后:函数是思维的单元
这堂课的内容如果浓缩成一句话:函数就是给一段逻辑起个名字,并规定好输入和输出的边界。参数是你接收外部输入的方式,返回值是你跟调用方的沟通协议,作用域是你变量的领土边界。三个概念一盘棋,缺一个函数都没法优雅地设计。
我之前带过不少零基础学员,观察到一个共性现象:一开始写作业都把全部代码堆在平级的主流程里,一改需求就全局变量乱飞。后来慢慢学会拆函数,初学代码反而变得支离破碎——一个个小函数彼此割裂,不知道谁调谁。这是正常的过程,先学会拆,再学会组装,需要经历一个“从一团乱麻到流程清晰”的演化。随着练习增多,你会慢慢掌握“多大的函数算合适”“哪些逻辑值得独立成函数”的手感,那时候看代码就仿佛看到了脉络,而不是平面的符号堆。
实际操作中我最后再分享一个小技巧:写函数时先写文档字符串,再写函数体。哪怕心里只有一个模糊的“这个函数需要根据日期算出季度”,先把"""根据日期返回季度"""这行字敲上去,你会发现后面的实现思路会被这行文字牵引出来,定义边界也顺理成章。这个习惯对我个人帮助极大,从写小脚本到持续维护万行级别的项目都适用。下一步你可以试试自己写一个猜数字游戏,把输入处理、逻辑判断、结果输出分别拆成独立函数——拆得越干净,你就越能体会到函数式思维带来的清爽感。