ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python函数入门到进阶:参数、作用域、lambda与代码重构

Python函数入门到进阶:参数、作用域、lambda与代码重构 1. day05到底该学什么从“能跑”到“会写”的分水岭很多自学Python的朋友走到第五天基本都会出现一个典型状态跟着教程敲过变量、字符串、列表、字典、if判断、for循环单看每个知识点都懂但一合起来就懵了——要么不知道什么时候该用函数要么把代码写成几百行“从上到下执行完就拉倒”的流水账。这正是day05最该解决的核心问题把零散语法组装成可复用的逻辑单元。我说句实在话day05放在任何一套教程里内容都逃不脱“函数”二字。但它不是让你记住def怎么写、return怎么返回那么简单而是要完成三个思维转变从“我写代码”变成“我设计代码”——先想清楚输入什么、输出什么、中间怎么处理从“复制粘贴”变成“调用复用”——同一个功能只写一次下次直接叫名字从“跑通就行”变成“考虑异常”——别人传错参数怎么办、数据为空怎么办、类型不对怎么办。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(age25, name张三) # 默认参数不传就用默认值 def introduce(name, age18): 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, containerNone): 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当成函数内部的局部变量可你在赋值之前又去读取它做加法于是报“局部变量未定义”。如果你确实想在函数里修改全局变量有两个办法用global关键字声明count 0 def increment(): global count count count 1 increment() print(count) # 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有一种“最内层作用域优先”的查找顺序业内常缩写为LEGBLocal函数内部→ Enclosing外层嵌套函数→ Global模块全局→ Built-inPython内置。也就是说当你在函数里写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, reverseTrue): 按温度排序默认从高到低 return sorted(city_temp_pairs, keylambda item: item[1], reversereverse) 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表达式附带的简单介绍上面用到了keylambda item: item[1]lambda是Python里创建匿名函数的方式适合那些只用一次、逻辑简单的场景。完整写法相当于def get_temp(item): return item[1] sorted(city_temp_pairs, keyget_temp, reverseTrue)但用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会把键盘中断CtrlC也吞掉调试时特别难受失败时返回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: zhangsanexample.com}提供add_contact(contacts, name, phone, emailNone)、remove_contact(contacts, name)、find_by_name(contacts, name)、list_all(contacts)四个函数contacts是列表大家共享同一个数据容器函数内部要处理重复添加、找不到联系人、参数缺失等情况。参考答案关键部分def add_contact(contacts, name, phone, emailNone): 新增联系人。如果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最好的几个训练点函数职责划分查找和删除分离删除内部复用了查找逻辑的思想返回值的语义设计True/False/None各有含义调用方可以根据返回值判断下一步动作对共享数据的操作contacts列表作为参数进进出出不靠全局变量符合工程习惯边界处理重复添加有保护、删不存在的名字不会崩。做完这个练习后你可以试着扩展把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, keylambda s: s[score], reverseTrue)关键区别在于key并不是直接对元素排序而是把每个元素映射成一个“排序键”再按排序键排。比如上面例子实际参与比较的是85、92、78这三个数字整个字典只是跟着移动。这个机制也解释了为什么key的写法那么灵活——你可以让函数返回元组实现多级排序# 先按分数降序分数相同按姓名字母升序 sorted(students, keylambda s: (-s[score], s[name]))-s[score]用负数实现“数字降序”这个技巧在实际开发里特别好用。8. 常见报错排雷UnboundLocalError、TypeError、IndentationError8.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那行的参数个数看清楚有没有默认值默认值参数可以省略。还有一种更隐蔽的类型匹配。比如你写了年龄: ageage是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-pythonimport时写的是cv2这种“安装名”和“导入名”不一致的现象在Python生态很常见注意区分。9. 怎么检验自己真的学会了自测清单与扩展思路学完day05建议先用下面这套问题考考自己能顺畅回答就算过关为什么函数内部不能直接修改外部不可变对象而列表为什么可以return和print的区别是什么位置参数、关键字参数、默认参数的混用规则是什么if __name__ __main__:到底有什么意义try-except捕获异常后怎么区分是哪种异常lambda表达式的限制是什么其中第四题值得多说两句。__name__是模块的内置变量当Python文件被直接运行时__name__的值是__main__当这个文件被其他文件import时__name__的值是模块名本身。所以if __name__ __main__:保证了“只有直接运行这个文件时才执行测试代码被导入时不会执行”。这是Python里最常用的工程惯例day05必须掌握。扩展思路上如果你还有精力我推荐把你的温度统计程序升级成支持文件输入def main(file_path): with open(file_path, encodingutf-8) as f: raw_text f.read() ...这时候“函数”的价值会体现得更明显——你只需要改main函数的输入来源其余三个函数一行不动。这就是模块化设计带来的直接收益。于我而言每次带新人走到day05我最看重的不是他记住了多少语法而是他有没有开始习惯性地问自己“这段逻辑能不能提成一个函数这个函数该放在哪里调用它的人会怎么使用”一旦开始问这三个问题就说明你从“学Python”过渡到了“用Python思考”后面的路会顺很多。
返回列表