十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

函数定义与参数设计:从代码复用到可维护架构

函数定义与参数设计:从代码复用到可维护架构 最近后台收到好几条私信都是问同一个问题“函数到底怎么定义才不是瞎写参数啥时候该放哪”仔细一看都是刷到函数这一章卡住了。函数这个东西确实很奇妙代码量少的时候你觉得它多余代码量一旦上来没有它你根本活不下去。今天这篇就把“函数的定义和参数”彻底掰开揉碎讲清楚所有观点都来自我自己写过、重构过、也踩坑反悔过的真实项目经验。你不需要有任何基础但如果你已经写过一些循环和判断读起来会更有感觉。day25 函数的定义和参数从被调用到被需要在代码里函数就是“把一段反复要用的逻辑装进一个盒子以后每次要用喊一声盒子上的名字就行”。它解决的核心问题只有一个别重复自己。但很多人把“函数定义”和“声明一个def”划了等号这是最大的误解。定义函数只是第一步怎么把参数设计好、怎么让调用者一眼看懂、怎么避免“这参数传还是不传”的纠结才是真正值的吃的部分。这篇内容适合所有正在学编程、刷到函数章节卡住、或者在写小工具时发现自己老在复制粘贴的读者。我会先用一个完整案例把定义函数时你必须做出的几个决策说清楚然后重点拆解参数的各种写法背后到底在解决什么痛点最后附上我整理的一份常见报错排查表尤其是一些反复出现的环境报错和参数数量不匹配问题。不绕弯子直接开始。1. 先从需求说起为什么编程一定要有函数函数不是语法设计者闲得无聊搞出来的概念它是在解决“代码变多之后根本没法维护”这个真实痛点的产物。你写一个计算器小工具只有加减乘除可能5个函数都用不上。但当你开始写订单系统、写爬虫、写数据分析脚本你会发现同样的校验逻辑、同样的格式化输出的代码会在不同地方出现三次五次甚至十次。这时候你面临两个选择继续复制粘贴或者把它提炼成函数。复制粘贴的后果一开始不明显多复制几次就开始疼了。比如订单金额校验逻辑出现三次某天产品经理说“金额不能为负”改成“金额不能为负且不能超过十万”你要改三个地方。漏改一个线上就会出现一个可以下十万以上订单的漏洞。函数解决的就是这个**“单点修改”**的问题逻辑只写一遍所有调用它的地方自动获得新行为。这一点是函数价值的第一层也是绝大多数人第一次真正“需要用函数”的瞬间。第二层价值在于抽象和可读性。你去看一份好的项目代码主流程读起来像读文章摘要比如def process_order(order_id): validate_order(order_id) calculate_total_price(order_id) apply_discount(order_id) send_confirmation_email(order_id)你没有必要知道每个函数内部怎么写的但通过函数名就能知道这个订单处理经历了哪些环节。这种“把细节藏起来把流程露出来”的能力是代码从“能跑”到“能维护”的分水岭。还有第三层隔离错误。你不用函数时所有变量和逻辑搅在一起报错了一长串根本不知道是哪里出了问题。有了函数报错信息会告诉你“是calculate_total_price里面的第3行出了问题”排查范围瞬间缩小到几行代码。这对调试效率的提升是质的改变。2. 函数定义从基础语法到可读性设计2.1 def语句怎么用才规范在Python里面定义一个函数用的是def关键字后面跟函数名、括号、冒号函数体注意缩进。最简单的函数可以是这样的def greet(): print(你好)这个函数没有任何参数也没有返回值。调用就一行greet()。但在实际项目中这种“无参数、无返回”的函数其实很少见。大部分函数至少要满足下面两个条件之一接收输入参数或者产生输出返回。为什么因为函数的存在意义是“把输入变成输出”如果既没有输入也没有输出它的作用就只能局限在打印或者修改全局状态而这种写法多了之后代码会变得很难追踪。所以先提醒一句定义函数之前先问自己三个问题这个函数需要哪些外部数据才能工作这些就是参数这个函数计算完成之后调用方需要拿到什么结果这就是返回值这个函数会不会对传入的数据造成意外修改这涉及参数传引用的问题后面细说2.2 一个合格的函数应该长什么样我之前在另一个教程里看到过一个例子今天拿来改造一下写一个“计算学生成绩等级”的函数。很多人第一版会写成这样def get_grade(score): if score 90: return A elif score 80: return B elif score 70: return C elif score 60: return D else: return F这个函数有输入score有输出等级字符串看着没问题但实际使用中马上会遇到新需求等级对应的分数区间能不能灵活调整出错时能不能给出明确提示空值怎么处理这些需求不在定义函数时考虑后面就只能在调用方做一堆判断那函数的封装价值就打了折扣。我通常会加一个grade_table参数把分数段和等级的映射做成可配置def get_grade(score, grade_table): for threshold, grade in grade_table.items(): if score threshold: return grade return F这样调用方可以传入不同的映射规则比如{90: 优, 80: 良}或者{60: 及格}。函数本身不关心规则怎么变这就是“参数设计”的意义。你看定义函数不是把逻辑写进去就行而是要把未来可能要变的东西提前设计成参数。这是初学者最容易忽略的一环。3. 参数是函数的灵魂一套组合拳彻底搞懂3.1 位置参数和关键字参数两条传值路径Python函数参数最基础的两种形式一种是位置参数一种是关键字参数。位置参数就是按顺序传第一个值给第一个参数第二个值给第二个参数def introduce(name, age): print(f{name}今年{age}岁) introduce(张三, 20) # 位置传参 introduce(age20, name张三) # 关键字传参顺序可以颠倒两种方式核心区别在于可读性和耦合性。位置传参代码短但要记住参数顺序关键字传参写起来长但调用处一目了然。我的习惯是参数少2~3个且含义清晰用位置传参参数多4个或以上或者参数含布尔值尽量用关键字传参。这里有个很多人不知道的细节在函数定义中如果参数列表里有/那么/前面的参数只能按位置传入不能用关键字如果参数列表里有*那么*后面的参数必须用关键字传入。这个设计在某些框架源码里经常能看到目的就是强制调用方写清楚意图防止误传def register(username, password, /, *, emailNone): print(username, password, email) register(张三, 123456, emailzhangexample.com) # 合法 register(username李四, password123) # 报错username和password不能用关键字传3.2 默认参数与可变参数如何应对不确定的调用场景实际项目里你的函数不可能永远只被一种方式调用。不同调用方可能想传的参数数量不一样。这时候就需要默认参数和可变参数。默认参数就是在定义时给参数一个初始值def connect(host, port3306, timeout5): ...调用时connect(127.0.0.1)会默认连3306端口connect(10.0.0.1, 5432, timeout10)则可以覆盖默认值。默认参数让函数兼容了“常见场景”和“特殊场景”调用方不需要每次都把参数写全大幅降低了使用成本。这里有一个必须刻在脑子里的坑默认参数不能设为可变对象。比如千万不要写def foo(items[])。因为默认值是在函数定义时创建一次之后每次调用如果没有传入该参数用的都是同一个列表。你在一次调用里往列表里添加了元素下一次调用会带着上一次的数据继续添加这这几乎必现的bug。正确的做法是def foo(itemsNone): if items is None: items [] ...再说可变参数。当你的函数不知道调用方会传多少个值或者想接收任意数量的参数时可以用*args和**kwargs。def add_all(*args): result 0 for num in args: result num return result add_all(1, 2, 3) # 6 add_all(1, 2, 3, 4, 5) # 15 def register_user(**kwargs): for key, value in kwargs.items(): print(f{key}: {value})*args把所有多余的位置参数打包成一个元组**kwargs把所有多余的关键字参数打包成一个字典。这两个名字只是约定俗成换成*numbers、**options效果一样真正起作用的是星号。3.3 参数的传递本质引用传递与可变对象陷阱新手最容易懵的一个点函数里修改参数外面的变量到底变不变def change_list(items): items.append(新元素) my_list [1, 2] change_list(my_list) print(my_list) # [1, 2, 新元素]你发现my_list变了。为什么因为Python传参的本质是——传的是对象的引用不是对象的副本。当参数指向一个可变对象列表、字典、集合时函数内部对对象的修改会直接作用于原对象。那为什么下面的代码没有改变原值呢def change_value(x): x 100 num 5 change_value(num) print(num) # 5因为整数是不可变对象。当你执行x 100时实际上是让形参x重新指向了一个新的整数对象没有影响外面的num。这个区别可以概括为如果函数内部只是“重新绑定”参数名外面不受影响如果函数内部“修改对象内容”外面会变。应对策略很简单如果你不希望外部数据被修改在函数内部先做一份拷贝def process_items(items): items list(items) # 先创建副本 items.append(新元素) return items这种细节在写业务逻辑时特别重要。以前我写过一个批量导入功能函数里有个默认参数是字典配置因为没处理可变对象默认值问题导致服务跑了一段时间后第一批导入的配置污染了后续所有批次的校验结果。排查了两天才找到原因就是从那时起我把“默认参数不用可变对象”写进了自己的代码规范里。4. 函数的高阶玩法回调、作用域与函数式工具4.1 回调函数与函数作为一等公民你写出一个函数之后可以把它当作参数传给另一个函数这就是“函数是一等公民”的含义。被传入的函数通常称为回调函数。这种模式在事件处理、异步编程、排序规则定制里到处都是。举一个最常用的例子排序时自定义规则students [ {name: 张三, score: 85}, {name: 李四, score: 92}, {name: 王五, score: 78}, ] def by_score(student): return student[score] students.sort(keyby_score)by_score传给sort的key参数sort在内部会对每个元素调用这个函数用返回的值作为排序依据。这就是回调函数最直观的应用。这种写法比你写一个循环自己比较、自己交换要清晰得多。再比如很多热词里提到的“claude无法识别”“git无法识别”“npm无法识别”这类报错本质上也是“系统找不到对应程序”。很多同学遇到这种问题第一反应是重装软件实际上多数情况是把可执行文件所在的目录没有加入环境变量PATH。比如Windows上你装了Git但git.exe在C:\Program Files\Git\cmd目录下如果这个路径不在PATH里你在命令行敲git就会提示“无法将‘git’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”。这跟函数参数里的“变量没定义”是一类问题——你要调用的那个名字在当前环境下根本不存在或没被正确绑定。解决方式也类似要么把工具路径加进PATH要么在调用时写全路径。这在第5节我会再展开。4.2 作用域规则LEGB与闭包当你开始写函数的时候作用域就会进入你的视野。Python查找一个变量的顺序是局部作用域Local→ 嵌套函数外层作用域Enclosing→ 全局作用域Global→ 内置作用域Built-in合称LEGB规则。x 10 # 全局 def outer(): x 20 # 嵌套函数的外层 def inner(): x 30 # 局部 print(x) inner() outer() # 30这里inner里打印的是30因为Python会先在局部作用域里找x找到了就不会往外找。如果把inner里的x 30去掉输出的就是20如果再把outer里的x 20去掉输出的就是10。闭包是作用域的进阶用法。当一个内部函数引用了外层函数的变量并且外层函数已经把内部函数返回出去这个内部函数连同它引用的变量一起就构成了闭包def make_multiplier(factor): def multiplier(x): return x * factor return multiplier double make_multiplier(2) print(double(5)) # 10double这个函数即使make_multiplier已经执行完毕它内部引用的factor仍然被保存着。这种能力在一些需要“函数携带状态”的场景非常有用比如计数器、配置文件读取器、缓存工具等。4.3 常用内置函数与函数式编程三件套Python自带很多内置函数它们本身就是“函数的定义和参数”最直接的实战范例。比如abs、len、sum、max、min这些个个都是接收参数、返回结果的典型。理解它们的参数设计也能反哺你自己的函数设计。map、filter、reduce这三个是函数式编程的三件套。map对序列中每个元素执行同一个操作nums [1, 2, 3, 4] square list(map(lambda x: x * x, nums)) # [1, 4, 9, 16]filter根据条件过滤元素nums [1, 2, 3, 4, 5, 6] evens list(filter(lambda x: x % 2 0, nums)) # [2, 4, 6]reduce则把序列累积成一个值from functools import reduce nums [1, 2, 3, 4] total reduce(lambda a, b: a b, nums) # 10这三个函数都接收“函数”作为参数也就是前面说的回调函数。理解它们你就自然理解了“参数可以是函数”这个抽象层级。不过我也要说句实话生产环境里这些函数式写法在简单场景下很好用但逻辑一旦复杂可读性反而不如普通的循环和判断。我自己的原则是能用一句lambda表达清楚的用函数式要写两行以上的逻辑就老老实实写循环。5. 你的代码真的需要函数吗判断标准与常见问题排查实录5.1 函数设计中的常见败笔把不该写成函数的写成函数把该写成函数的用复制粘贴两个方向都是坑。我总结了几个最典型的设计败笔你可以对照自己的代码检查一个函数做太多事。比如一个函数既读了文件又解析数据还写进了数据库。这种函数改一次就要重新测一遍所有流程问题定位也很难。好的函数应该职责单一一个函数只做一件事。参数过多。如果一个函数需要六七个参数说明这个函数的抽象层次有问题。很多时候可以把相关参数封装成一个对象或字典减少调用方的心智负担。用布尔值参数控制流程。def process(order, is_urgentFalse)这种写法在函数内部会不断if is_urgent:看起来方便但实际上这个函数同时承担了两种完全不同的行为。更好的做法是拆成process_normal(order)和process_urgent(order)或者把is_urgent改成策略参数。返回不一致的类型。有些函数正常时返回字符串出错时返回None调用方每次都要判断。改进方法是出错时抛出异常让调用方用try/except处理。5.2 参数报错排查与命令找不到报错速查表我在教学和写项目的过程中遇到过大量参数相关的报错。这里整理一份速查表都是实打实踩过的坑报错信息原因解决方法TypeError: add_all() takes 1 positional argument but 3 were given函数定义的参数数量少于调用时传入的数量检查函数定义是否漏写参数或调用时是否多传了参数TypeError: missing 1 required positional argument: age调用时少传了必填参数要么调用时补齐参数要么定义时给该参数加默认值TypeError: get_grade() got an unexpected keyword argument score_table调用时传入了函数定义里没有的参数名检查拼写或者在函数定义里加上该参数或用**kwargs接收SyntaxError: non-default argument follows default argument默认参数后面还有非默认参数调整参数顺序带默认值的参数必须放到参数列表最后Terminal报错无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称没有安装对应工具或工具路径没加到环境变量PATH安装对应软件并把可执行文件所在目录添加到PATH然后重开终端表格里的第一类和第二类是所有初学者都会遇见的。有意思的是这两种报错有时候会让人误以为是“电脑坏了”或者“代码被神秘力量改了”。其实你只要把函数定义和调用处放在一起对比通常几秒钟就能定位问题。第三类“unexpected keyword argument”在改代码时特别容易出现。你之前用get_grade(score)调用没有问题后来给函数加了个grade_table参数结果忘了改别的文件里的调用处报错就来了。这类问题最好的预防方式不是靠肉眼而是用好编辑器的函数签名提示。比如VS Code里装上Python插件后鼠标悬停在函数调用处就能看到这个函数接受哪些参数很大程度上能帮你减少这种低级错误。最后一类“命令无法识别”的报错跟函数参数其实非常有类比性。命令行工具本质上也是一个函数它的“参数”就是命令行里的选项和值它的“环境变量PATH”就是系统查找这个“函数”的定义时的搜索路径。当系统提示找不到npm、git、cmake时你首先要检查的不是代码而是这个“函数”有没有被正确安装和暴露。我的排查习惯是先运行where npmWindows或which npmmacOS/Linux看系统能不能找到这个程序的路径。如果返回为空就说明它根本没在当前环境里需要安装或者添加环境变量。5.3 几个实用小技巧把函数当工具而不是负担总结几个我用了很多年的经验希望能帮你减少使用函数时的心理负担第一不要一开始就追求完美的函数设计。你完全可以先把逻辑写在一个大while循环里跑通了再提炼成函数。重构式开发比“想清楚再写”更适合大多数人。每当你发现自己又在复制粘贴代码那就是提炼函数的时机。两处重复可以忍三处重复必须提炼。第二给函数起一个能表达“做什么”的名字而不是“怎么做”的名字。process_data就不如validate_order和calculate_discount表达清晰。读代码时大部分时间是在读函数名和调用关系名字起好了代码就自然可读了一半。第三写一点文档字符串。不用多一两句话说明函数是干什么的、参数是什么、返回值是什么就行。三个引号包起来写在函数体第一行。这个习惯在代码量超过1000行之后会给你带来超乎预期的回报。很多框架和IDE都能直接读取这个字符串生成文档或提示等于你只花了几分钟就省下了未来无数次回忆代码逻辑的时间。第四使用类型注解。Python是动态类型语言参数传什么类型全靠自觉。但从Python 3.5开始你可以给参数和返回值加类型注解def get_grade(score: float, grade_table: dict) - str: ...类型注解不会改变运行时行为但它让读代码的人包括未来的你不用去翻调用处就能知道参数该传什么。配合mypy这类静态检查工具还能在运行前发现一批类型错误。我的项目从去年开始全面使用类型注解大型重构的出错率明显下降。6. 从day25到日常函数思维如何改变你的编码方式函数学到一定程度你会发现它不只是一门语法更是一种拆解问题的思维方式。每次接到一个复杂的开发任务我都会先在纸上把需求拆成几个步骤每个步骤对应一个函数。然后定义好这些函数的参数和返回值像一个接口设计。最后再一个函数一个函数地填充实现。在写“day25”这个阶段你不需要考虑设计模式、面向对象这些东西你只需要掌握一件事如何合理地把功能切块并把这些块的输入输出关系理清楚。怎么理先问自己这个步骤需要什么数据这个步骤的结果是什么如果你能清晰回答这两个问题函数定义就已经成功了一大半。我见过太多初学者卡在“什么算一个函数”这个问题上。有人觉得两行代码也要写函数有人觉得等代码写了一千行再抽象也不迟。说实话函数这种东西写少了你不理解它的价值写多了你会陷入“过度封装”的泥潭。我的建议是先从10行以上的重复逻辑开始提炼让函数出现在它真正有用武之地的位置然后逐渐积累手感。还有一个点容易被忽略函数的学习不是线性的。你可能今天学会了定义和参数明天遇到闭包、装饰器又懵了。这很正常。我自己重新学了三遍装饰器才真正理解它。关键不在于一次看懂而在于每次遇到都往前推进一点理解。今天就当打好地基之后遇到任何语言里的函数概念你都能快速迁移。最后分享一个我最近在代码评审时常说给团队的话写函数之前先想想怎么调用它。调用体验是函数设计的指南针。如果你是一个调用者你希望这个函数叫什么名字、接受什么参数、返回什么结果把这个想明白了函数自然就定义出来了。祝你在代码世界里写出真正被需要的函数。
返回列表