☰
Python函数入门到进阶:参数、作用域、lambda与代码重构
2026/10/7 12:00:25 网站建设 项目流程

1. day05到底该学什么:从“能跑”到“会写”的分水岭

很多自学Python的朋友,走到第五天基本都会出现一个典型状态:跟着教程敲过变量、字符串、列表、字典、if判断、for循环,单看每个知识点都懂,但一合起来就懵了——要么不知道什么时候该用函数,要么把代码写成几百行“从上到下执行完就拉倒”的流水账。这正是day05最该解决的核心问题:把零散语法组装成可复用的逻辑单元。

我说句实在话,day05放在任何一套教程里,内容都逃不脱“函数”二字。但它不是让你记住def怎么写、return怎么返回那么简单,而是要完成三个思维转变:

  1. 从“我写代码”变成“我设计代码”——先想清楚输入什么、输出什么、中间怎么处理;
  2. 从“复制粘贴”变成“调用复用”——同一个功能只写一次,下次直接叫名字;
  3. 从“跑通就行”变成“考虑异常”——别人传错参数怎么办、数据为空怎么办、类型不对怎么办。

day05真正卡住大多数人的不是语法,而是“拆解问题的能力”。同样一个需求:统计一段文本中每个单词出现的次数。新手写法是写了50行循环+字典判断,稍微有点经验的会拆成clean_text、count_words、format_result三个函数。两种写法结果一样,但代码的健壮性、可测试性、可维护性完全不在一个级别。

所以这篇文章不讲那种纯API罗列,而是把day05应该掌握的知识点揉进真实场景里,配合代码、报错、排查思路来讲。你看完之后不敢说直接精通,但至少能把函数用得明明白白。

2. 核心语法一次吃透:定义、参数、返回值与调用逻辑

2.1 从数学函数类比理解def的四个组成部分

如果你上过初中数学,一定见过f(x) = x + 1这种表达式。Python中的函数构造和它几乎一模一样:

  • def关键字 = “我要定义一个函数了”;
  • 函数名 = 就是f,只不过Python里名字可以起得更语义化,比如add_one;
  • 括号里的参数 = 就是x,是函数的“入口”;
  • return后面的表达式 = 就是x + 1,是函数的“出口”。
def add_one(x): return x + 1 result = add_one(5) print(result) # 6

就这么简单。但很多教程没讲清楚的一点是:函数体里的代码在定义时不会执行,只有调用时才会执行。你写def add_one(x):后面那一整块缩进代码,Python解释器只是“记下来”,等你在下面某一行写出add_one(5),它才真正把5传进去跑一遍。

这个机制带来的实际影响是:如果函数体里有语法错误,定义时不报错,调用时才报错。我见过不少新手把函数写好了但忘了调用,运行后程序毫无反应,还以为是自己代码写错了。

2.2 参数传递的四种形态,以及最常见的坑

day05必须掌握的参数类型有四种:位置参数、关键字参数、默认参数、不定长参数。

# 位置参数:按顺序传,必须一一对应 def introduce(name, age): print(f"{name}今年{age}岁") introduce("张三", 25) # 关键字参数:用名字指定,顺序可以打乱 def introduce(name, age): print(f"{name}今年{age}岁") introduce(age=25, name="张三") # 默认参数:不传就用默认值 def introduce(name, age=18): print(f"{name}今年{age}岁") introduce("李四") # 李四今年18岁 introduce("李四", 20) # 李四今年20岁 # 不定长参数:不知道有几个参数时用 def sum_all(*args): return sum(args) print(sum_all(1, 2, 3, 4, 5)) # 15

这里面最容易翻车的坑是默认参数的失效问题。看下面这段代码:

def append_item(item, container=[]): container.append(item) return container print(append_item("a")) # ['a'] print(append_item("b")) # ['a', 'b'] —— 第二次调用时居然残留了第一次的结果

问题出在:container=[]这个空列表在函数定义时就创建了,之后每次调用如果没传新列表,用的都是同一个列表对象。这就是典型的“可变默认参数陷阱”。正确写法是:

def append_item(item, container=None): if container is None: container = [] container.append(item) return container

为什么None就没问题?因为None是不可变对象,每次调用时都会重新走一遍if container is None,创建一个新列表。这种细节教程里不会专门提醒你,但实战中写爬虫、写缓存经常会遇到。

2.3 return的四个隐藏细节

  • 没有return的函数,返回None。如果你写了result = print("你好"),result的值就是None,因为print本身就返回None。
  • return会立即结束函数。return下面的代码永远不会执行,如果你在return后面还写了逻辑,那就属于“死代码”。
  • return可以返回多个值,底层其实返回了一个元组,Python自动帮你解包。
def get_min_max(nums): return min(nums), max(nums) low, high = get_min_max([3, 1, 4, 1, 5]) print(low, high) # 1 5
  • 函数也可以不写return,这种函数通常称为“过程”,它只负责做事,不负责产出结果。比如一个向日志文件写入内容的函数,写完就结束,不需要返回值。

3. 变量作用域与生命周期:为什么函数内改不了外部变量

新手在day05最容易懵掉的就是作用域。举一个最常见的问题:

count = 0 def increment(): count = count + 1 # 报错:UnboundLocalError increment()

为什么会报错?直觉上讲,count不是已经定义了吗?但Python有一条规则:在函数内部,如果对一个变量进行赋值,它就会被视为局部变量。count = count + 1这行让Python把count当成函数内部的局部变量,可你在赋值之前又去读取它做加法,于是报“局部变量未定义”。

如果你确实想在函数里修改全局变量,有两个办法:

  1. 用global关键字声明:
count = 0 def increment(): global count count = count + 1 increment() print(count) # 1
  1. 更推荐:不要修改全局变量,把count作为参数传入、返回值带出:
count = 0 def increment(count): return count + 1 count = increment(count) print(count) # 1

第二种方式看起来绕,但它是函数式编程的核心思想:函数应该是“纯”的,同样的输入永远得到同样的输出,不偷偷修改外部状态。实际工程中(尤其是多线程、多人协作场景),大量使用全局变量会让程序状态难以预测,所以要养成“参数进、返回值出”的习惯。

还有一种局部变量,叫做嵌套函数中的nonlocal。它解决的是函数里套函数时,内层函数想修改外层函数局部变量的问题:

def outer(): x = 10 def inner(): nonlocal x x = x + 1 inner() print(x) # 11 outer()

这个语法稍微冷门,但写装饰器、闭包时会频繁遇到,day05可以先有个印象。

另外,Python有一种“最内层作用域优先”的查找顺序,业内常缩写为LEGB:Local(函数内部)→ Enclosing(外层嵌套函数)→ Global(模块全局)→ Built-in(Python内置)。也就是说,当你在函数里写print时,Python先找函数内有没有叫print的变量,再找外层函数,再找全局,最后才找到内置函数print。这就是为什么如果你在全局定义了一个叫print的变量,下面的所有print调用都会报TypeError。

4. 实战拆解:把一段啰嗦的“流水账代码”重构为函数版

4.1 原始需求描述

下面我用一个非常贴近真实工作的需求来演示day05函数知识的综合用法:

给你一个包含多行文本的字符串,每行是“城市,温度”的格式,例如:

北京,23 上海,28 广州,31 深圳,29

请统计所有城市中温度最高的城市名称和温度值,并按温度从高到低输出完整列表。

这个需求拆出来,包含三件事:解析文本、按温度排序、格式化输出。新手最常犯的毛病是把三件事全揉进一个脚本里。

4.2 先看反面教材

data = """北京,23 上海,28 广州,31 深圳,29""" city_list = [] for line in data.split("\n"): parts = line.split(",") city = parts[0] temp = int(parts[1]) city_list.append((city, temp)) # 按温度从高到低排 for i in range(len(city_list)): for j in range(i + 1, len(city_list)): if city_list[i][1] < city_list[j][1]: city_list[i], city_list[j] = city_list[j], city_list[i] max_city = city_list[0][0] max_temp = city_list[0][1] print("温度最高的城市是:" + max_city + "," + str(max_temp) + "度") for item in city_list: print(item[0] + ":" + str(item[1]))

这段代码能跑,但无法复用。第二数据来了、格式变了、排序规则改了,你就得重新写一遍。而且程序逻辑全在一个流程里,没法单独测试“解析”对不对。

4.3 函数版重构

def parse_temperature_data(raw_text): """解析文本,返回[(城市, 温度), ...]列表""" result = [] for line in raw_text.strip().split("\n"): city, temp_str = line.split(",") result.append((city.strip(), int(temp_str.strip()))) return result def sort_by_temperature(city_temp_pairs, reverse=True): """按温度排序,默认从高到低""" return sorted(city_temp_pairs, key=lambda item: item[1], reverse=reverse) def format_report(city_temp_pairs): """格式化输出,并找出温度最高的城市""" if not city_temp_pairs: return "没有数据" max_city, max_temp = city_temp_pairs[0] lines = [f"温度最高的城市是:{max_city},{max_temp}度"] lines += [f"{city}: {temp}" for city, temp in city_temp_pairs] return "\n".join(lines) def main(raw_text): pairs = parse_temperature_data(raw_text) sorted_pairs = sort_by_temperature(pairs) print(format_report(sorted_pairs)) if __name__ == "__main__": data = """北京,23 上海,28 广州,31 深圳,29""" main(data)

你对比一下这两段代码,函数版本看起来行数更多,但每个函数职责单一:

  • parse_temperature_data只负责解析,输入原始文本,输出结构化数据;
  • sort_by_temperature只负责排序,跟数据格式无关;
  • format_report只负责展示,并且内部处理了空数据的情况;
  • main把上面三步串起来,if __name__ == "__main__"保证模块被导入时不会误执行。

这才是day05该有的水平——不是把代码写“少”,而是把代码写“清晰”。函数名的注释代替了整段流程注释,别人看到parse_temperature_data这个函数名,不用读代码就知道它是干什么的。

4.4 lambda表达式,附带的简单介绍

上面用到了key=lambda item: item[1],lambda是Python里创建匿名函数的方式,适合那些只用一次、逻辑简单的场景。完整写法相当于:

def get_temp(item): return item[1] sorted(city_temp_pairs, key=get_temp, reverse=True)

但用lambda写,省去一行def,更紧凑。注意,lambda后面只能写一行表达式,不能写复杂的多行逻辑。如果你发现lambda表达式太长、太绕,那就老老实实写def。

5. 类型转换与边界情况:int("abc")这种错误必须主动拦截

day05还会牵扯到一类非常重要但经常被忽略的内容:函数内部的数据类型转换与异常处理。很多初学者写完上面那个温度统计,马上会问:如果文本里有一行是北京,abc怎么办?

直接跑一下:

int("abc") # ValueError: invalid literal for int() with base 10: 'abc'

程序直接崩掉。开发环境里会报红,生产环境里这就是一次线上事故。所以写函数时,必须对可能出现的脏数据做防护。

5.1 try-except的正确姿势

def parse_temperature_line(line): """解析单行文本,返回(城市, 温度)。解析失败返回None。""" parts = line.split(",") if len(parts) != 2: return None city, temp_str = parts try: temp = int(temp_str.strip()) except ValueError: return None return city.strip(), temp

这里有几个细节值得注意:

  • 先用if len(parts) != 2判断格式,避免"北京,23,extra"这种多列数据导致解包报错;
  • 只用except ValueError,而不是裸except:。裸except会把键盘中断(Ctrl+C)也吞掉,调试时特别难受;
  • 失败时返回None,调用方通过if item is not None过滤掉脏数据。这是一种常见的约定。

5.2 更加Pythonic的方式:类型注解与文档字符串

day05如果能顺带养成写类型注解的习惯,后面读代码、写代码都会舒服不少:

def parse_temperature_data(raw_text: str) -> list[tuple[str, int]]: """ 解析城市温度文本。 Args: raw_text: 多行字符串,每行格式为“城市,温度”。 Returns: 由(城市, 温度)组成的列表,解析失败的行会被跳过。 """ ...

Python 3.9之后可以用list[tuple[str, int]]这种写法,老版本则用List[Tuple[str, int]](需从typing导入)。类型注解在运行时不会强制校验,但它有两个作用:给IDE做代码提示,给人做自文档。

如果你想真正在运行时校验类型,需要配合pydantic、mypy这类工具,但day05阶段先别引入那么重的依赖,能把类型注解写出来就算入门了。

6. 一个综合练习:编写通讯录增删查改函数库

为了巩固day05所学的全部内容,我建议你亲手实现一个通讯录函数库。它不需要界面,就用纯函数实现增删查改,保存在一个列表里。这算是“函数模块化”最好的练手项目。

需求定义:

  • 通讯录条目为字典{"name": "张三", "phone": "13800000000", "email": "zhangsan@example.com"};
  • 提供add_contact(contacts, name, phone, email=None)、remove_contact(contacts, name)、find_by_name(contacts, name)、list_all(contacts)四个函数;
  • contacts是列表,大家共享同一个数据容器;
  • 函数内部要处理重复添加、找不到联系人、参数缺失等情况。

参考答案(关键部分):

def add_contact(contacts, name, phone, email=None): """新增联系人。如果name已存在,返回False,否则追加并返回True。""" if find_by_name(contacts, name) is not None: return False contact = {"name": name, "phone": phone, "email": email} contacts.append(contact) return True def find_by_name(contacts, name): """按姓名查找,返回联系人字典或None""" for contact in contacts: if contact["name"] == name: return contact return None def remove_contact(contacts, name): """删除联系人。删除成功返回True,未找到返回False""" for i, contact in enumerate(contacts): if contact["name"] == name: del contacts[i] return True return False

这个练习看似简单,其实涵盖了day05最好的几个训练点:

  1. 函数职责划分:查找和删除分离,删除内部复用了查找逻辑的思想;
  2. 返回值的语义设计:True/False/None各有含义,调用方可以根据返回值判断下一步动作;
  3. 对共享数据的操作:contacts列表作为参数进进出出,不靠全局变量,符合工程习惯;
  4. 边界处理:重复添加有保护、删不存在的名字不会崩。

做完这个练习后,你可以试着扩展:把contacts持久化到文件里,每次程序启动时读取。这一步会自然引出open、with、pickle或json模块,而这些恰好是day06的内容,衔接得非常顺畅。

提示:如果用del contacts[i]删除,注意倒序循环删除的问题——如果你用如下写法删除多个匹配项,会漏删:

for i, contact in enumerate(contacts): if contact["name"] == name: del contacts[i]

因为删除一个元素后,后续元素下标前移,enumerate继续取下一个下标时就越过了一个元素。正确做法是反向遍历或者先收集下标再统一删除。这种问题初学者很容易踩,建议你在练习时专门验证一下。

7. 函数式思维速成:map、filter、sorted的小场景应用

day05后半程,你还可以接触一批“把函数当成参数传来传去”的内置函数。它们不算新手必修,但学会了写代码会很爽。

7.1 map与filter的典型应用

temps = [23, 28, 31, 29] fahrenheit = list(map(lambda t: t * 9 / 5 + 32, temps)) print(fahrenheit) # [73.4, 82.4, 87.8, 84.2] hot_cities = list(filter(lambda item: item[1] >= 30, city_temp_pairs)) print(hot_cities) # 只保留温度30以上的城市

map的作用是“对序列每个元素做同样操作”,filter的作用是“把序列中满足条件的元素筛出来”。它们替代了传统的for循环加append,代码可读性反而更好。

不过需要说明:在Python 3中,map/filter返回的是迭代器(惰性求值),必须用list()包一层才能看到完整结果。如果你不包list,直接print出来会看到<map object at 0x...>,这不是报错,是它还没“真正计算”。

7.2 sorted的key用法

sorted是day05必须掌握的内置函数。它的key参数接收一个“单参数函数”,使用方式可以说是函数式编程的启蒙:

students = [ {"name": "张三", "score": 85}, {"name": "李四", "score": 92}, {"name": "王五", "score": 78}, ] # 按分数从高到低排序 by_score = sorted(students, key=lambda s: s["score"], reverse=True)

关键区别在于:key并不是直接对元素排序,而是把每个元素映射成一个“排序键”,再按排序键排。比如上面例子,实际参与比较的是85、92、78这三个数字,整个字典只是跟着移动。这个机制也解释了为什么key的写法那么灵活——你可以让函数返回元组,实现多级排序:

# 先按分数降序,分数相同按姓名字母升序 sorted(students, key=lambda s: (-s["score"], s["name"]))

-s["score"]用负数实现“数字降序”,这个技巧在实际开发里特别好用。

8. 常见报错排雷:UnboundLocalError、TypeError、IndentationError

8.1 IndentationError:缩进即语法

Python用缩进表示代码块,这点和绝大多数语言不同。day05最常见的缩进错误有两类:

def foo(): print("第一行") print("第二行") # IndentationError: unexpected indent

另外一种是tab和空格混用。很多编辑器默认把tab显示成4个空格,但Python解释器严格区分它们。如果你的代码一部分用tab缩进,一部分用4个空格缩进,运行时就会报TabError: inconsistent use of tabs and spaces in indentation。

解决方案很简单:VS Code里把"editor.renderWhitespace": "all"打开,或者干脆设置"editor.defaultFormatter"统一用prettier/autopep8自动格式化。我的习惯是从第一天就只按空格键缩进,避免一切tab相关问题。

8.2 TypeError:参数个数不匹配与类型不匹配

def greet(name, greeting="你好"): return greeting + "," + name greet("张三", "早上好", "额外参数") # TypeError: greet() takes from 1 to 2 positional arguments but 3 were given

这个报错信息已经写得很直白了,问题在于传参数量和函数签名对不上。解决方案就是数一数def那行的参数个数,看清楚有没有默认值,默认值参数可以省略。

还有一种更隐蔽的:类型匹配。比如你写了"年龄:" + age,age是int,直接拼字符串报TypeError: can only concatenate str (not "int") to str。正确的做法是用f-string或者str(age)。

8.3 NameError:变量名拼写错误

非常经典:

def process(): result = compute() return reslut # 注意reslut拼错了 NameError: name 'reslut' is not defined

这类错误几乎都是手误,IDE会标红提示,但如果你用记事本或者在线编辑器写,就得靠肉眼排查。养成一个习惯:变量名长度不要取太像的(如data1和data2),拉长到有意义的单词(如parsed_data和raw_data),能减少很多这种问题。

8.4 模块导入位置的坑

如果你在函数内部写import json,那json只在函数内部可用,函数外部用不了。如果你在文件顶部写import json,但实际调用的文件名恰好在某个子目录,Python找不到模块时,报的错是ModuleNotFoundError。day05阶段最常见的解决方式是:

  • 把import放到文件最前面;
  • 确认你安装的包名和你import的名字一致(比如安装的是opencv-python,import时写的是cv2,这种“安装名”和“导入名”不一致的现象在Python生态很常见,注意区分)。

9. 怎么检验自己真的学会了:自测清单与扩展思路

学完day05,建议先用下面这套问题考考自己,能顺畅回答,就算过关:

  1. 为什么函数内部不能直接修改外部不可变对象?而列表为什么可以?
  2. return和print的区别是什么?
  3. 位置参数、关键字参数、默认参数的混用规则是什么?
  4. if __name__ == "__main__":到底有什么意义?
  5. try-except捕获异常后,怎么区分是哪种异常?
  6. lambda表达式的限制是什么?

其中第四题值得多说两句。__name__是模块的内置变量:当Python文件被直接运行时,__name__的值是"__main__";当这个文件被其他文件import时,__name__的值是模块名本身。所以if __name__ == "__main__":保证了“只有直接运行这个文件时才执行测试代码,被导入时不会执行”。这是Python里最常用的工程惯例,day05必须掌握。

扩展思路上,如果你还有精力,我推荐把你的温度统计程序升级成支持文件输入:

def main(file_path): with open(file_path, encoding="utf-8") as f: raw_text = f.read() ...

这时候“函数”的价值会体现得更明显——你只需要改main函数的输入来源,其余三个函数一行不动。这就是模块化设计带来的直接收益。

于我而言,每次带新人走到day05,我最看重的不是他记住了多少语法,而是他有没有开始习惯性地问自己:“这段逻辑能不能提成一个函数?这个函数该放在哪里?调用它的人会怎么使用?”一旦开始问这三个问题,就说明你从“学Python”过渡到了“用Python思考”,后面的路会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询