ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

函数从入门到实战:参数传递、闭包与高阶函数用法详解

函数从入门到实战:参数传递、闭包与高阶函数用法详解 函数这东西一开始接触的时候很容易被当成一个“必须完成的作业题”来写老师让定义几个函数按题目要求算个结果跑通就算交差。但等到真正做项目、写脚本、处理数据才意识到函数是所有代码里最值得花时间琢磨的东西。这篇围绕“函数的运用”展开的内容适合刚开始学编程、正在啃函数概念、或者想系统梳理一遍函数用法的朋友。我会从函数声明、参数传递、返回值这些基本功讲起一路聊到作用域、闭包、递归、回调和高阶函数最后把实际调试里踩过的典型报错和排查思路整理出来希望能给你带来实实在在的帮助而不是又一篇“从入门到放弃”的说明书。1. 函数的本质与设计思路1.1 函数为什么存在从“重复”到“抽象”函数最朴素的价值就是干掉重复代码。我见过不少初学者写程序两段逻辑几乎完全相同只是参数不一样就直接复制粘贴。需求一改所有粘贴过的地方都得跟着改漏掉一处就是bug。把一段逻辑封装成函数之后同样的操作只需要调一个名字修改也只需要改一处这是DRY原则最直接的体现。再往深一层看函数是“抽象”的基本工具。调用函数的人只需要知道三件事函数叫什么、需要传什么参数、返回什么结果完全不需要关心内部怎么实现。这和用手机是一个道理你只需要知道按哪个按钮、得到什么结果不需要知道CPU怎么调度、内存怎么分配。这种“把复杂度包起来”的能力正是模块化设计的起点。第三个作用容易被忽视函数让调试和测试变得可控。逻辑全都揉在一坨代码里的时候出问题只能从头到尾人肉扫描拆成函数之后就可以逐个函数单独验证。写单元测试时函数天然就是最小的被测单元。很多团队把代码的可测试性作为重要规范背后依赖的正是函数拆分的质量。1.2 函数的分类与声明方式函数按来源可以分成几大类内置函数是语言官方提供的工具箱比如Python的len()、abs()、sum()JavaScript的parseInt()、Math.sqrt()C语言的strlen()、sizeof()等内置函数优先使用能少造很多轮子自定义函数是根据业务自己定义的匿名函数/箭头函数比如Python的lambda、JavaScript的箭头函数写法适合短小、一次性使用的逻辑库函数/第三方函数比如NumPy的np.arange().reshape()、Excel里的VLOOKUP、SUMPRODUCT这些是特定领域里已经封装好的解决方案。不同语言的函数声明方式差异很大列几个典型对比。Python自定义函数def greet(name): 返回问候语 return Hello, nameJavaScript里至少有三种写法// 函数声明 function greet(name) { return Hello, name; } // 函数表达式 const greet function(name) { return Hello, name; }; // 箭头函数写法 const greet (name) Hello, ${name};C语言声明与定义分开#include stdio.h // 函数声明原型 int add(int a, int b); int main(void) { printf(%d\n, add(3, 4)); return 0; } // 函数定义 int add(int a, int b) { return a b; }这里有个值得单独记录的坑JavaScript的函数声明会提升可以在定义之前调用但函数表达式和箭头函数不会提升。如果你把const greet ...放在调用之后运行时会直接报greet is not defined。Python和C语言则要求函数在调用之前已经定义或声明Python因为模块整体加载完之后才会执行模块级代码所以把调用代码放在文件末尾是常见习惯。很多人问“函数声明到底有什么意义”C语言里它的意义是让编译器知道函数签名从而在调用处做类型检查不声明直接用大概率会有警告甚至错误。2. 函数的参数与返回值细节2.1 参数传递的本质值传递与引用传递函数传参是所有语言里都容易踩坑的点。很多初学者以为参数就是“把变量复制一份进去”实际上要区分数据类型。Python里不可变对象int、str、tuple在函数内部重新赋值不会影响外部变量因为修改的是局部引用的指向而可变对象list、dict、set在函数内部原地修改会直接影响外部对象。举个例子def add_item(lst, item): lst.append(item) my_list [1, 2] add_item(my_list, 3) print(my_list) # [1, 2, 3]外部列表被修改了JavaScript的规则类似原始类型按值传递对象类型按“共享传递”处理。函数内部修改对象的属性会反映到外部但如果直接对参数重新赋值外部引用不会变。C语言默认是纯值传递想在函数里修改外部变量必须传指针。数组作为参数传入时会退化成指针所以函数内部用sizeof(arr)拿不到整个数组长度这也是为什么C语言里经常要额外传一个长度参数。热词里有人问“sizeof函数需要头文件”其实sizeof是个运算符不是函数但想打印结果通常需要包含stdio.h头文件这个细节新手容易卡住。实际项目中参数设计的另一个重点是默认参数和可变参数。Python支持默认参数、*args、**kwargsJavaScript有默认参数、arguments对象和rest参数。比如def calc(a, b10, *args, **kwargs): print(a, b, args, kwargs)我个人的建议是默认参数的默认值尽量用不可变对象不要用[]或{}当默认值。因为Python的默认参数只会在定义时求值一次多次调用会共享同一个对象容易出来特别诡异的bug。比如def append_item(item, lst[]): lst.append(item)连续调用几次你会发现列表里莫名多了之前调用的数据。2.2 返回值与多个返回值的处理return是最容易被忽略的细节。很多初学者写函数时忘了写return函数返回None。函数本身没有报错一切看起来正常但拿到的结果却是None排查半天才发现是返回值丢了。单个返回值很简单多个值怎么办Python的惯例是返回元组然后解包def get_user(): return Alice, 25 name, age get_user() print(name, age)JavaScript的惯例是返回对象或数组function getUser() { return { name: Alice, age: 25 }; }C语言函数只能返回一个值想返回多个值通常用传出参数传指针或返回结构体。真实项目里函数返回值的设计会影响调用方的体验。很多内置函数就是用特殊返回值表示失败比如C语言的fopen失败返回NULLstrlen正常返回长度。Python里更推荐抛异常调用方用try/except捕获。我的原则是预期内的边界情况用返回值处理真正的异常情况用异常机制不要用返回值模拟异常流否则调用方要写一堆判断很不优雅。再看几个内置函数的返回值abs(-5)返回5math.sqrt(16)返回4.0np.sum(arr, axis0)按列求和还是按行求和完全取决于axis参数。这些函数看似简单用错场景结果会完全对不上。尤其NumPy里axis这个概念axis0是按列方向操作axis1是按行方向操作记牢这一条能省下大量Debug时间。3. 作用域、闭包与生命周期3.1 作用域规则与变量提升作用域说白了就是变量“能被看到”的范围。不同语言规则略有差别但核心思路相似。Python里有一条著名的LEGB查找规则从内往外找Local局部→ Enclosing外层函数→ Global全局→ Built-in内置。如果你在函数里给一个全局变量赋值Python会认为你创建了一个新的局部变量除非加global声明。一个特别容易踩的坑count 0 def inc(): global count count 1如果不加global运行时会报UnboundLocalError: local variable count referenced before assignment。这个报错几乎每个Python新手都会遇到一次原因就是Python在编译函数时发现函数体内部有对count的赋值操作就把它标记为局部变量后面的读取自然也按局部变量处理。JavaScript里var是函数作用域且有变量提升let和const是块级作用域。if语句的大括号不会限制var但会限制let和const。这也是为什么现在新代码基本都推荐用let/const因为块级作用域的约束更符合直觉不会出现变量飘到意料之外的位置。C语言里函数外定义的全局变量在整个文件可见函数内定义的局部变量只能在自己的函数里使用。嵌入式领域特别典型比如51单片机的代码经常看到main()里放一个大循环循环里反复调用各种子函数处理传感器数据、控制外设。很多人问“51单片机主函数为什么是循环”其实是因为嵌入式程序的核心逻辑是持续扫描外部事件子函数负责拆解任务这样整体才清晰可维护。全局变量在这种场景里能简化数据共享但一定要克制否则多文件编译时符号冲突、值被意外修改的问题会让人崩溃。我个人的体会是全局变量能不用就不用函数之间的耦合度会随着全局变量数量直线上升排查问题时你根本不知道是哪个函数改了它。3.2 闭包函数记住了自己的“出身”闭包是函数进阶里绕不开的概念。简单理解闭包就是“函数 它创建时所在的作用域环境”。即使外层函数已经返回内层函数依然能访问外层函数的局部变量。经典计数器例子def counter(): count 0 def inc(): nonlocal count count 1 return count return inc c counter() print(c()) # 1 print(c()) # 2JavaScript版本function counter() { let count 0; return function() { count; return count; }; } const c counter(); console.log(c()); // 1 console.log(c()); // 2闭包的实际用途很多生成私有变量、实现柯里化、在事件回调里保存状态等。但它也是一把双刃剑。最典型的坑是“在循环中创建闭包”for (var i 1; i 3; i) { setTimeout(function() { console.log(i); }, 1000); } // 输出3 3 3因为var没有块级作用域所有闭包共享同一个i等到定时器触发时i已经变成4了。改成let i就正常了因为每次循环都会创建独立的绑定。如果只能用var则需要通过立即执行函数或者闭包工厂把当前值固化下来。我在实际项目里遇到过几次线上bug最后定位都是这类闭包使用不当。用闭包时心里一定要有根弦它捕获的到底是哪个变量的“哪一份值”而不是笼统地觉得“闭包就是内部函数能访问外部变量”。4. 进阶函数运用递归、回调与高阶函数4.1 递归自己调用自己的艺术递归适合处理“规模不断缩小、结构自相似”的问题树形结构遍历、目录扫描、数学归纳式问题都是典型的递归场景。递归三要素第一基线条件也就是递归终止的边界第二递归调用函数调用自身第三逼近基线每次递归都让问题规模更小。缺一个递归都会变成死循环或者无限递归最终栈溢出。阶乘是最经典的教学例子def factorial(n): if n 1: return 1 return n * factorial(n - 1)目录遍历是实际工程里最常见的递归场景import os def walk_dir(path, depth0): for item in os.listdir(path): full os.path.join(path, item) if os.path.isdir(full): walk_dir(full, depth 1) else: print( * depth item)递归虽然简洁性能问题却非常突出。比如斐波那契数列用朴素递归实现时间复杂度是O(2^n)n稍微大一点就卡死。解决办法是加缓存也就是记忆化from functools import lru_cache lru_cache(maxsizeNone) def fib(n): if n 2: return n return fib(n - 1) fib(n - 2)或者直接用循环递推根本不用递归。我的经验是如果递归深度可能很大优先考虑迭代或显式栈如果问题本身就是树形结构递归通常是最优雅的写法。Python默认递归深度上限在1000左右超过会抛RecursionError必要时可以用sys.setrecursionlimit()调整但这只是治标本质还是要优化算法结构。4.2 回调函数与高阶函数在真实项目中的应用高阶函数是“把函数当参数或返回值”的函数回调函数是高阶函数最典型的应用形态。很多人听到“回调”就发怵其实它很朴实——你给别人留一个电话号码让对方有事时打给你这就是回调你定义一个函数传给别人让别人在合适的时机调用它这也是回调。JavaScript的setTimeout是入门的绝佳例子setTimeout(function() { console.log(2秒后执行); }, 2000);事件监听、网络请求成功后的处理、Node.js里的IO操作本质都是回调。Python里同样常见sorted()的key参数接收一个函数map()、filter()接收函数并作用于每个元素nums [1, 2, 3, 4, 5] squares list(map(lambda x: x * x, nums)) evens list(filter(lambda x: x % 2 0, nums))JavaScript版本的三件套const nums [1, 2, 3, 4, 5]; const squares nums.map(x x * x); const evens nums.filter(x x % 2 0); const sum nums.reduce((acc, x) acc x, 0);这种“把函数传入集合处理函数”的写法比for循环更简洁也更贴近数据处理的思考方式。数据分析里的agg聚合函数本质也是把聚合逻辑作为参数传入按列做求和、均值、最大值。很多语言都支持lambda表达式比如Java里的list.sort((a, b) - a.compareTo(b))本质就是把一个短小而明确的逻辑作为参数传递。lambda的意义不在于它能做什么新事情而在于让“把函数当参数”这件事写起来不啰嗦。不过回调函数滥用会带来“回调地狱”。回调嵌套好几层之后代码可读性会变得非常差。现代JavaScript用Promise和async/await缓解Python用协程。但理解回调的核心思想仍然重要因为读老代码、看第三方库文档时还是会大量遇到。5. 常见问题与排查技巧实录5.1 “函数未定义”类报错的排查这篇文章写到这里已经有不少实操细节了但真正写代码的人都知道报错才是日常打交道最多的东西。下面梳理几个高频问题。最经典的报错就是NameError: name xxx is not defined翻译过来就是“xxx函数或变量未定义”。常见起因有几种拼写错误函数名写错一个字母。这种情况最常见也最无语检查拼写就好。定义在使用之后JavaScript里用了函数表达式调用代码却写在定义之前或者Python模块之间先调用后导入。没有导入模块想用math.sqrt(16)却忘了import math直接写sqrt(16)就会报NameError: name sqrt is not defined。C语言里则表现为隐式声明警告或错误。作用域问题函数定义在某个if或try块内部条件不满足时函数实际上没有创建。变量把函数名覆盖了比如局部变量叫list之后再调用list(x)就会报list object is not callable。排查流程我建议固定下来看报错行号 → 确认名字拼写 → 确认定义位置在当前作用域可见 → 确认模块是否导入 → 用print或调试器打印类型和值。提示命令行里常见的“无法将npm项识别为cmdlet、函数、脚本文件或可运行程序的名称”这类报错本质和“函数未定义”类似。操作系统把命令当作“可执行程序/函数”在PATH路径里查找找不到就报错。解决思路也相通确认软件是否安装、路径是否加入PATH、是否需要通过npx这类前置调用器或者直接用完整路径。理解了这一层逻辑再看到类似的报错就不会慌了。5.2 参数个数、类型错误与返回值为None的问题参数层面的报错五花八门但核心就几类。Python会报TypeError: f() missing 1 required positional argument: b代表参数没传全。处理方式简单按函数定义补全参数或者给参数设置默认值。反过来也有TypeError: f() takes 1 positional argument but 2 were given也就是参数传多了这种往往是因为调用方和定义方对函数签名理解不一致。C语言里自定义函数如果缺少声明或者声明和定义不一致编译器会警告甚至产生未定义行为。所以写C语言时“先声明、后定义、调用处保持一致”这三件事必须到位。字符串函数也经常踩这类坑比如strlen返回的是size_t你拿int去接收当然也能跑但潜在截断和符号问题在后面会找上门。返回值为None的问题更隐蔽。通常是因为某个分支没有return或者写了return但没带值结果调用方拿到None还继续往下调用方法于是报AttributeError: NoneType object has no attribute xxx。排查时可以用断言帮忙result do_something() assert result is not None, do_something() 返回了 None这类报错整理成速查表会更直观报错信息常见原因排查方向NameError: name xxx is not defined拼写、未导入、作用域不可见检查拼写、import、定义位置TypeError: f() missing 1 required positional argument参数没传全查看函数签名补全参数TypeError: f() takes 1 positional argument but 2 were given参数传多了检查参数数量与默认值AttributeError: NoneType object has no attribute xxx函数返回了None检查所有return分支是否覆盖完整RecursionError: maximum recursion depth exceeded递归没有基线条件检查递归终止条件5.3 面向数据分析/表格场景的常用函数笔记很多读者接触“函数”不只是写代码还有Excel和数据分析场景这里的函数同样是核心生产力。Excel里有两个高频经典。第一个是VLOOKUP按某一列的键去查找另一张表或同表其他列的值。需要注意查找值必须位于表区域的第一列返回列序号是从第一列开始数的。数据不存在时显示#N/A所以实际使用中最好用IFERROR包一层让结果更友好。第二个是SUMPRODUCT它能把对应位置的数相乘后再求和比如SUMPRODUCT(A2:A10, B2:B10)等价于A2*B2 A3*B3 ... A10*B10。它还能做多条件计数SUMPRODUCT((A2:A100华东)*(B2:B100100))可以统计“华东区域销售额大于100”的记录数这是很多人不知道的隐藏用法。数据分析里NumPy和Pandas的函数体系让操作极简。np.arange(12).reshape(3, 4)先生成0到11的序列再重组成3行4列的矩阵np.sum(arr, axis1)按行求和axis0按列求和。把axis的方向理解清楚很多困惑都会消散。Pandas里的str.split()用于拆分列groupby().agg()用于分组聚合函数名都很直观但要注意返回类型有时候是Series有时候是DataFrame有时候是标量处理方式差异很大。6. 实操案例用函数封装一个完整小工具6.1 一个完整的词频统计小工具纸上谈兵不如落一段实际代码。用一个经典的小工具来展示函数的综合运用统计一篇文章里出现频率最高的词。import re from collections import Counter def read_file(path): 读取文本文件返回字符串 with open(path, r, encodingutf-8) as f: return f.read() def tokenize(text): 把文本切分成单词列表转为小写 text re.sub(r[^\w\s], , text) return text.lower().split() def count_words(words): 统计单词出现次数返回Counter对象 return Counter(words) def print_top(counter, top_n10): 打印出现最多的前N个单词 for word, cnt in counter.most_common(top_n): print(f{word}: {cnt}) def main(path): text read_file(path) words tokenize(text) counter count_words(words) print_top(counter) if __name__ __main__: main(article.txt)这个小工具用了四个函数读取、分词、统计、输出每个函数只干一件事。调用关系完全线性出问题时可以逐个函数测试。觉得分词不对单独调用tokenize(Hello, world!)看输出觉得统计不准单独检查count_words([a, a, b])。这就是模块化函数的好处任何一环可独立验证。6.2 模块化思路与函数命名的经验最后聊点组织代码的经验。拆分函数不是越细越好也不是越少越好。一个实用的标准是如果一个函数里同时存在“输入获取、业务计算、结果输出”三类完全不同的事情就该考虑拆分。如果一段逻辑超过二三十行或者有多个嵌套循环和条件分支也建议拆成子函数。函数命名我习惯用动词或动词短语开头比如read_file、count_words、print_top。名字要让人仅凭名称就猜到功能不要用do_something、handle_data这种泛泛的名字。参数名也要有含义别清一色用a、b、c除非是纯数学公式场景。写docstring或注释时重点说明“输入是什么、输出是什么、有什么前提条件”而不是复述实现过程。因为实现过程会变接口契约相对稳定。写这篇关于“函数的运用”的内容我最大的体会是函数能力的高低本质上不是语法熟练度而是拆解问题的能力。同一个需求有人写出来是一大坨顺序代码有人写出来是若干个小函数各司其职后者的可读性和可维护性要好太多。如果你手头正好也有一份“函数的运用”相关的作业或者实际任务别急着一次写完先花几分钟想想输入是什么、输出是什么、中间能拆成哪几步每个步骤能不能用一个函数来承载。最后再分享一个小技巧给函数写注释时多写一句“这个函数什么时候会被调用、它的返回值应该怎么用”一个月之后你回来看代码会感谢自己。
返回列表