多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

函数基础详解:从参数传递到作用域,理解模块化编程的核心

函数基础详解:从参数传递到作用域,理解模块化编程的核心 第一次接触函数的时候我还在写那种一页能铺好几屏的脚本。复制、粘贴、微调遇到需求改动就得一个文件一个文件地改改到后面连自己都分不清哪个版本是最新的。后来翻别人的代码发现人家用几十个短小而整齐的函数就把整个项目搭得像积木一样我才意识到函数不是语法书里的一个考点而是一种把代码“化整为零”的思维方式。这篇文章不聊高深的理论只把函数的定义、参数、返回值和作用域这些基础用法一条条拆开来讲配合随手就能跑的示例代码。适合刚学会变量和循环、正准备迈入结构化编程这一步的初学者也适合那些写了很久脚本但总觉得代码越写越乱的开发者。看懂函数你就能开始真正“设计”代码而不是“堆”代码。1. 函数到底是个什么东西藏在名字背后的核心逻辑1.1 从生活场景理解函数一台“加工机器”函数这个概念听起来很抽象但它对应到生活里其实非常常见。你把脏衣服放进洗衣机按下启动键洗衣机内部怎么翻滚、怎么排水你不用管过一会儿拿出来就是干净衣服。对你来说这台机器的“输入”是脏衣服“输出”是干净衣服中间过程被封装在机身里。函数干的就是这件事接收你给的数据按照你预先写好的步骤处理然后把结果送出来。这种“不关心内部过程只看输入输出”的思维方式编程里叫黑盒思维。写函数的时候你可以把注意力集中在“我需要什么输入、要得到什么输出、中间如何处理”这三件事上。至于这个函数以后被谁调用、在哪个模块里调用调用者根本不需要知道你函数里的一个临时变量叫什么名字。如果非要说函数和洗衣机的区别那就是洗衣机只能洗衣服而函数处理的是数据。数据可以是数字、字符串、列表、字典也可以是对象。但这没关系函数只负责一件事接收、处理、返回。1.2 为什么函数是代码里最重要的一块积木很多初学者会问我把所有逻辑从上到下写在一个文件里也能跑出正确结果为什么非要拆成函数这个问题我自己也纠结过。直到有一次我需要在一个项目里同时处理订单页和后台管理页的折扣计算两处逻辑各写了一份结果某天产品经理改了折扣规则我改了订单页却漏了后台那一份线上就出了价格不一致的问题。那次之后我才真正明白函数解决的问题远不止“代码复用”这么简单。函数至少带来四个层面的收益。第一是可复用性写一次、调用多次修改只在定义处进行不会出现“改一处漏一处”的连锁事故。第二是可读性给一段操作起一个清晰的名字比如calculateDiscount别人读代码时不用钻进逻辑细节只看函数名就知道这段在干什么。第三是可测试性独立函数可以单独喂数据验证结果出问题时能快速定位到具体函数而不是在大段脚本里翻找。第四是抽象与拆分能力一个复杂需求可以拆成多个小函数每个函数只做一件事最后像搭积木一样组合起来。这里有一个很重要的观念转变函数不只是语法更是一种“模块化思考”的训练。当你面对一个复杂需求时先不要急着写代码而是先问自己“这个需求可以拆成哪几个步骤每个步骤的输入输出分别是什么”一旦你习惯用函数来拆分问题再大的项目在你眼里也会变成一组小而清晰的函数组合。2. 函数的定义与基本调用先把架子搭起来2.1 不同语言里的函数定义长什么样先看两种最常见的语法Python版本def calculate_discount(price, rate): discount_price price * rate return discount_priceJavaScript版本function calculateDiscount(price, rate) { const discountPrice price * rate; return discountPrice; }两种写法的骨架其实完全一致先声明这是一个函数然后给函数起一个名字括号里写参数可以没有参数后面跟一对花括号或者冒号加缩进的代码块最后用return把结果返回出去。price和rate是参数也就是函数接收的输入函数体里的discount_price是内部变量return discount_price则是把计算结果递出来。关于函数名我的建议是使用“动词开头的命名方式”getUser、calculateTotal、formatDate、validateInput。因为函数本质上是“动作”名字里带动词读代码的时候一眼就能看出它在做什么。Python社区喜欢用下划线分隔单词JavaScript社区习惯用驼峰命名哪种风格并不重要重要的是在一个项目里保持一致。命名清晰的好处过两个月你再回来看自己的代码时体会最深。2.2 调用一个函数时程序到底发生了什么函数定义本身只是“声明”它不会立刻执行。真正让代码跑起来的是调用在函数名后面加一对括号括号里填上参数。def add(a, b): return a b result add(3, 5) print(result) # 8这里add(3, 5)就是调用程序执行到这一行时会暂时跳到函数定义的位置把3和5分别绑定给a和b执行完函数体后带着返回值8回到调用处再把8赋给result。这个过程很像你拿着菜谱做菜菜谱只是写在那里只有你真正动手做的时候才会产生一道菜。初学阶段最容易犯的一个错误是忘记加括号。如果我写的是result add而不是add(3, 5)那么result得到的不是一个数字而是这个函数本身的内存引用。打印出来可能是function add at 0x...而不是8。这在Python里会让人很困惑但在JavaScript里同样存在类似问题。记住一个判断标准如果你想拿到函数的处理结果就必须加括号如果你只是想把这个函数当作值传来传去那才不需要括号。另外要注意函数体里的print和return的区别。print是打印到控制台方便调试但它不是返回值return才是把数据交给调用者。如果你写了一个函数里面只有print没有return那么调用它后拿到的结果是None。这个细节我在下一节还会重点展开。3. 参数传递与返回值数据进出的关键细节3.1 形参和实参一个管“接口”一个管“原料”函数定义时的price和rate叫形式参数简称形参调用时传入的实际值比如calculate_discount(100, 0.8)里的100和0.8叫实际参数简称实参。形参相当于函数对外公布的“接口说明书”我接受两个参数第一个是原价第二个是折扣率。实参则是你真正递给它的“原料”。大部分语言支持两种传参方式。Python里除了按位置传参还可以使用关键字参数def calculate_area(length, width): return length * width print(calculate_area(5, 3)) # 按位置传参 print(calculate_area(width3, length5)) # 关键字参数顺序可调关键字参数的好处是调用时一目了然尤其当参数很多或者有多个是数字时不会因为顺序搞错而出错。函数还可以定义默认参数这样调用者可以少传一些值def power(base, exponent2): return base ** exponent print(power(5)) # 25 print(power(5, 3)) # 125这里exponent2就给exponent设了一个默认值调用时不传就使用默认值。默认参数一定要放在非默认参数的后面否则程序会直接报语法错误。这在Python里是硬性规则。3.2 值传递与引用传递改没改原值就看这一条初学者最容易踩的坑之一是搞不清在函数里修改传入的变量到底会不会影响函数外面的原值。这里必须分两种情况。先看数字这种不可变类型def change_num(x): x 10 a 1 change_num(a) print(a) # 1输出是1不是10。原因是什么a本身是个整数对象把a传给change_num时函数内的x只是指向了同一个整数对象。当执行x 10时并没有修改原来的整数对象而是让x重新指向了另一个整数对象10外部的a当然不受影响。再看列表这种可变类型def append_item(lst): lst.append(99) my_list [1, 2] append_item(my_list) print(my_list) # [1, 2, 99]这次输出变成[1, 2, 99]。因为lst和my_list指向同一个列表对象append操作是在这个列表对象内部做的改动函数外能直接看到结果。画一个简单比喻值传递相当于你把一张纸的复印件交给别人别人在上面涂改你的原件不受影响引用传递相当于你把原件交给别人别人在原件上写字你拿回来时已经变了。实际开发中的建议是除非这个函数的目的就是修改传入的对象否则尽量避免在函数内部修改可变参数。如果确实需要修改要么在函数命名中明确说明比如sort_list_in_place要么先在函数内复制一份数据再操作保持函数的“纯净性”。这个习惯会让函数的副作用大大减少调试起来轻松很多。3.3 return语句函数怎么把结果“递”出来函数里的return有两个作用立即结束函数的执行并把后面的值返回给调用者。如果没有写return或者return后面没有值函数执行结束后会返回None。def no_return(): x 1 print(no_return()) # None很多初学者写了半天函数发现结果总是None原因就是漏写了return。注意函数里可以出现多个return但每次调用只会执行其中一个。最常见的写法是“守卫条件”模式def divide(a, b): if b 0: return None # 提前终止不执行下面的代码 return a / b当b为0时直接返回None并结束函数后面的除法不会执行。这种写法可以避免深层嵌套的if-else代码会更平直、更好读。Python还支持多个返回值本质上是返回一个元组def get_min_max(numbers): return min(numbers), max(numbers) mn, mx get_min_max([3, 1, 4, 1, 5]) print(mn, mx) # 1 5声明一个函数时最好先想清楚“它到底要返回什么”。一个函数只返回一个明确的结果会让接口更干净。如果发现一个函数既想返回计算结果又想返回错误信息又想返回状态码那通常意味着这个函数做的事情太多了建议拆开。4. 作用域与生命周期变量不是哪里都能用4.1 全局作用域和局部作用域同名变量为何互不干扰函数内部定义的变量默认是局部变量只在函数内部可见函数外定义的变量叫全局变量从定义处开始到文件结束都可见。看这个经典例子total 100 def show_total(): total 50 print(total) # 50 show_total() print(total) # 100函数内部的total 50创建了一个新的局部变量它遮蔽了外部的同名变量所以函数内打印的是50。函数结束后局部变量total随之销毁外部的total仍然是100。两个同名变量生活在两个独立的空间里互不干涉。在Python里如果想在函数内修改全局变量需要先声明global在JavaScript的严格模式下局部变量用let或const声明行为类似。但我的建议是尽量少用全局变量。原因很简单全局变量让函数变得“不透明”——你调用一个函数不确定它到底改了多少外部状态而且一旦程序变大多个函数共享同一个全局变量排查问题时会非常痛苦。4.2 变量的生命周期从创建到销毁函数的局部变量和函数调用绑定在一起调用开始时创建调用结束时就销毁。每次调用函数这些局部变量都会重新初始化互不共享。看这个例子def counter_demo(): count 0 count 1 return count print(counter_demo()) # 1 print(counter_demo()) # 1不是2函数连续调用了两次但每次都是重新创建count 0所以两次都返回1。如果想保存状态就需要把状态放在函数外部或者使用更高级的技术比如闭包。闭包在初学阶段可以只留个印象不需要马上掌握。简单说闭包就是“函数里嵌套函数并且内部函数可以引用外部函数的变量”。Python里可以写成这样def make_adder(n): def adder(x): return x n return adder add_10 make_adder(10) print(add_10(5)) # 15这里的adder记住了n的值10哪怕make_adder已经执行完毕adder依然可以使用n。这就是闭包的作用——让函数“记住”一些外部信息。这个概念现在不理解没关系但它能帮你理解为什么函数是一种“数据”可以被传递、返回和组合。等你学到装饰器、高阶函数的时候会回头感谢这里留下的伏笔。5. 踩坑与排查从业者路上常见的问题清单5.1 新手最容易踩的5个坑我把带过的新人和自己当年的常见错误做了一份清单建议逐条对照检查。第一个坑是把函数名当成结果。写了result add而不是add(3, 5)导致结果是个函数引用。解决办法很简单想拿结果就带括号。第二个坑是忘记return。函数是执行了但调用方拿到的是None然后继续拿None做运算报出各种莫名其妙的错误。排查时先看函数的每个分支是不是都有return特别是if-else里容易漏掉某个分支。第三个坑是用常见词汇给变量命名把内置函数给遮蔽了。比如有人写了个list [1, 2, 3]之后再调用list()想创建列表时就会报错“list is not callable”。这也是为什么变量命名最好具体一点比如user_list、item_list避免抢占关键字和内置函数。第四个坑是默认参数用可变对象。下面是典型反例def add_item(item, items[]): items.append(item) return items print(add_item(1)) # [1] print(add_item(2)) # [1, 2]第二调用时列表还是上一次的Python的默认参数只在函数定义时求值一次所以items[]这个空列表会在多次调用之间被共享造成数据意外累积。正确做法是用None作为默认值在函数内再新建一个列表def add_item(item, itemsNone): if items is None: items [] items.append(item) return items第五个坑是函数内部依赖了不该依赖的全局状态。比如函数直接读了一个外部变量来计算结果看起来方便但换一个环境、换一批数据时就容易“灵异”。好的函数应该尽量做到全部输入走参数全部输出走return。这样的函数也叫纯函数它不依赖外部状态、不修改外部状态同样的输入永远得到同样的输出测试和复用都最轻松。5.2 排查函数问题的实用策略当你遇到函数相关的问题光看代码往往看不出所以然。我自己的排查步骤一般是这样的。第一步用打印插桩。在函数入口打印所有参数的值看输入是否符合预期在return之前打印输出看结果是否正确。两步一对比问题立即缩小到“输入不对”还是“处理过程不对”。第二步使用调试器的断点功能。不需要在所有行打断点重点看函数调用处和函数入口。单步执行能清楚地看到参数如何绑定、局部变量如何变化。很多编辑器都支持鼠标点击设置断点非常直观。第三步做最小复现。把有问题的函数复制出来单独放到一个新文件里用固定的几个测试数据调用。函数是很适合隔离调试的单元如果单独测试没问题那问题多半出在调用方式或外部数据上如果单独测试就有问题那问题就在函数内部。第四步认真读报错信息。Python的Traceback会显示完整的调用链从最外层调用到最内层函数每一行是哪个文件的哪一行。沿着这个链从下往上读很快就能定位到出错源头。JavaScript的堆栈信息同理。下面这个速查表是我平时解决函数问题时经常对照的现象可能原因优先排查点函数返回None没有写return或某个分支漏了return检查函数所有分支的return路径传入的列表/Dict被意外修改函数内修改了可变参数检查是否有append、赋值操作是否需要复制变量名报未定义局部变量在函数外访问或拼写错误查看变量作用域和拼写多次调用结果相互影响默认参数是可变对象检查函数定义时默认参数是否为[]或{}函数不更新外部变量不知道全局变量需要global声明重新设计让函数通过return返回结果排查函数问题的核心心法是一次只改一个变量一次只测一个函数。不要同时猜三个原因那样只会越改越乱。最后再分享一点个人体会我最初写代码时特别热衷于把所有逻辑都堆在一个大函数里觉得这样“一气呵成”很爽。直到有一次改需求新加的会员折扣规则同时影响三处计算逻辑我改完A处忘了B处上线后用户在群里发了价格截图才老老实实回去把函数拆小。那次之后我养成一个习惯凡是达到十行以上的逻辑块我都会先停下来想一想能不能拆成两个更小的函数每个函数能不能只说清楚一件事学函数最值得养成的习惯不是背语法而是“先定义输入输出再写实现”。哪怕只是一个几行的功能也先在纸上写下“这个函数接收什么参数返回什么结果”然后再动手写函数体。你会发现自己写的代码越来越像说明书而不是一坨别人看不懂的流水账。这篇内容之后你还会有几个很适合继续深入的方向高阶函数和回调函数、匿名函数lambda、装饰器以及一个很重要的概念“函数的组合”。函数这条路走到后面你会慢慢感受到它不只是编程语法的基础更是你思考问题、组织业务逻辑的核心方式。第一步已经迈出去了剩下的就是多写、多拆、多复盘。
返回列表