ARTICLE DETAIL

资讯详情

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

Python函数从入门到实战:核心语法、装饰器与生成器详解

Python函数从入门到实战:核心语法、装饰器与生成器详解 1. 为什么要用函数从一段重复代码说起我第一次接触Python函数的时候其实没有觉得这东西多神奇无非就是把一段代码包起来、起个名字、需要的时候叫它一声。直到有个周末我在处理一份爬下来的商品数据需要把清洗、去重、格式转换、输出统计结果这套流程连续跑二十多遍每换一个数据源就得复制粘贴一遍那几十行代码。复制到第五遍的时候我就知道不对劲了改一个字段名得全局搜索、逐处替换漏掉一处就是线上数据出错。就在那天晚上我意识到函数不是把代码包起来这么简单它是你对抗复杂性的第一道防线。这个内容对刚入门的人最有用因为你早晚会走到重复代码爆炸这个节点。函数能帮你解决的远不只是少写几行字的问题它能让你的代码变成积木一块一块拼起来而不是一坨一坨堆起来。这篇文章我尽量不写成官方文档的复读机而是把Python函数从定义、参数、返回值到装饰器、闭包、生成器这条线结合我实际踩过的坑给你捋一套可以直接上手用的思路。1.1 没有函数时代码是怎么失控的先来看我最常见到的反面教材。很多新手写脚本从头到尾是一根主线走到底# 处理用户输入的原始数据 raw_data input(请输入数据) data raw_data.strip().split(,) clean_data [] for item in data: if item.isdigit(): clean_data.append(int(item)) else: print(f无效数据: {item}) # 计算平均值 total sum(clean_data) avg total / len(clean_data) if clean_data else 0 print(f平均值: {avg}) # 过两天又需要处理另一批数据复制粘贴一遍...这个脚本跑起来完全没问题问题出在三天后。产品经理走过来说你再帮我处理一份导出文件格式稍微不一样。你怎么办复制粘贴把开头的input换成open(export.csv)然后祈祷后面的逻辑不用动。如果运气不好中间还有几处硬编码的变量名那就要从头看一遍、改一遍、测一遍。等这个脚本长到四百行的时候你连改哪里都找不到了。函数就是把这种无序的线性流程拆成有名字、有边界、有输入输出的独立单元。我习惯把函数类比成厨房里的半成品菜提前切好的配菜、调好的酱汁做菜的时候按需取用而不是每次下厨都从洗菜切菜开始。代码复用只是最表面的收益真正的收益是你能在更高的视角上思考问题了——不用关心这个函数内部怎么算的只需要知道它输入什么、输出什么。1.2 函数拆解的正确姿势以做什么为线索拆函数的维度有很多但我强烈建议新手从一个函数只做一件事开始。这个原则听起来空落到实操上有个很好用的判断方法如果你的函数名需要用和或然后来描述那就说明它做了太多事。比如读取数据和清洗数据应该拆成read_data和clean_data两个函数计算平均值和输出报告应该拆成calculate_average和generate_report。拆完之后还有个额外的收获每个函数都能单独测试。我后来做数据清洗的时候经常是某个字段老是漏处理直接写一个针对clean_phone_number的单元测试把各种奇形怪状的手机号扔进去两分钟就能定位问题。这在过去的线性脚本里几乎不可能做到你得构造完整输入、跑到那一步、再打印中间结果排查效率天差地别。2. Python函数的核心语法与细节解析函数的基本语法我相信大多数人看过一眼就会但真正到项目里写得好不好拼的是对细节的理解。这一节我不讲什么是def这种入门科普我讲几个你写代码时大概率会碰到的、容易理解错的点。2.1 定义与调用的基本规则名字、缩进和可读性Python用def关键字定义函数后面跟函数名、括号、参数列表、冒号然后函数体必须缩进def calculate_average(numbers): total sum(numbers) count len(numbers) return total / count if count else 0这里面有个很多人不太在意但很重要的点**Python函数的签名本身就是它的文档。**参数名起得够好别人包括三个月后的你自己看一眼函数名加参数名基本就知道怎么用了。我见过很多人写def func(a, b, c):然后内部全靠注释解释每个参数是什么这是非常糟糕的习惯。更好的方式是def calculate_average(numbers: list[float]) - float:再配合一个简单的docstring说明异常情况这个函数基本不需要额外文档。Python 3.5之后支持类型注解它不会强制你传对类型但配合IDE写在函数签名里兼容性检查、自动补全、错误提示都会强一个档次。建议新项目从一开始就养成写类型注解的习惯成本极低收益却很实在。2.2 参数传递的完整机制位置参数、关键字参数、默认参数和*args/**kwargs这一块是函数面试题的重灾区也是实际项目里最容易写出玄学行为的地方。先说三类最基础的位置参数按顺序传入calculate_average([1, 2, 3])里的[1, 2, 3]就对应numbers。关键字参数calculate_average(numbers[1, 2, 3])好处是可读性强、不易传错顺序尤其当函数有多个参数时。默认参数在定义时给参数一个默认值调用方可以不传。比如def connect(host, port8080):。默认参数有一个著名的坑默认参数不要用可变对象。我之前写过这样一个函数def add_item(item, cache[]): cache.append(item) return cache第一次调用add_item(1)返回[1]第二次调用add_item(2)返回[1, 2]因为默认的[]是在函数定义时创建的同一个对象后续每次调用都在往这个共享列表里塞数据。正确写法是def add_item(item, cacheNone): if cache is None: cache [] cache.append(item) return cache这个坑我至少见过三个项目踩进去其中有一个还是写工具库时踩的测试的时候没测第二次调用上线后数据全串了。核心原因就是Python的默认参数在函数定义时只计算一次后续调用共享同一个默认对象。再说*args和**kwargs。*args接收任意数量的位置参数打包成一个元组**kwargs接收任意数量的关键字参数打包成一个字典。它们不是语法糖这么简单它们是你实现装饰器回调函数函数转发这类高级模式的基础。比如写一个日志装饰器你并不知道被装饰的函数会接收什么参数这时候*args, **kwargs就是必须的后面讲装饰器时再展开。2.3 返回值的正确打开方式return、多返回值与None的语义Python函数可以用return返回任意对象不写return或者只写return不带值函数默认返回None。这个隐式的None是很多人踩坑的地方尤其是写那种处理完数据然后打印结果的函数时调用方以为有返回值结果拿到一个None再往下做result.get(key)就报AttributeError。我自己的习惯是**如果一个函数的核心目的是计算并返回结果那每个分支都要有明确的返回值如果一个函数的核心目的是执行动作比如写文件、发请求、弹窗那它返回None是合理的。**把这两类函数混在一起写是最容易让调用方困惑的设计。多返回值在Python里其实是用元组实现的语法上很灵活def get_user_info(user_id): # 模拟从数据库查数据 user {id: user_id, name: 张三, age: 28} return user[name], user[age] name, age get_user_info(1)这里有个细节如果调用方只写result get_user_info(1)那result是一个元组(张三, 28)你需要知道顺序才能解包。所以当返回值超过三个时我建议用命名元组或数据类否则调用方记住第几个是什么的成本会高到让人想骂人。还有一类特殊函数——bool类型的函数比如is_valid()、has_permission()。这类函数返回True或False用来做条件判断。这里有一个我实际项目中反复强调的规则不要写if is_valid() True:直接写if is_valid():。 True不仅多此一举而且如果你哪天返回了一个非布尔真值比如空列表[][] True是False但if []也是False行为看起来一致可一旦返回的是1数字11 True是Trueif 1也是True好像也一致可如果你返回的是22 True是Falseif 2却是True这就出问题了。直接用if判断是最稳妥的。3. 内置函数与标准库高频函数实战Python之所以好用很大一部分原因是内置函数和标准库已经帮你写好了大量半成品。很多人写代码第一反应是自己造轮子比如想找最大值先写个for循环遍历一遍、比较、更新变量。其实max()、min()、sum()、sorted()这些内置函数早就实现了还经过了几十年的优化比自己写的循环快得多。3.1 与文件打交道open函数与上下文管理器open()是我使用频率最高的内置函数之一。爬虫、数据分析、日志处理几乎每个脚本都要跟文件打交道。但很多人只知道open(file.txt, r)然后read()完事不知道文件用完之后应该关闭甚至不知道不关闭会有什么后果。# 错误示范 file open(data.txt, r) data file.read() # 忘了 file.close() # 正确示范 with open(data.txt, r, encodingutf-8) as file: data file.read()with语句会自动调用__enter__和__exit__不管代码块内部有没有异常文件都会在退出时被正确关闭。这是我在代码评审时最先检查的东西之一。with不是Python内置函数而是上下文管理器协议但它和open是绝配你几乎永远应该这样用。还有两个open的高频参数值得记住encodingutf-8几乎必写不写的话Windows系统默认可能是gbk读UTF-8编码的文件会直接报UnicodeDecodeErrornewline在读CSV时能避免空行问题。这些是很多教程不会细讲的坑我实际处理CSV时踩过现在默认就带上。3.2 与序列打交道map、filter、sorted与lambda组合拳map、filter、sorted这三个内置函数配合lambda表达式是Python函数式编程风格的入门三件套。它们能让你用一行代码完成过去要写四五行的循环逻辑。# 把列表每个元素平方 squares list(map(lambda x: x**2, [1, 2, 3, 4])) # 输出 [1, 4, 9, 16] # 过滤出偶数 evens list(filter(lambda x: x % 2 0, [1, 2, 3, 4])) # 输出 [2, 4] # 按年龄排序假设数据是字典列表 people [{name: 张三, age: 28}, {name: 李四, age: 22}] sorted_people sorted(people, keylambda x: x[age], reverseTrue)lambda是Python的匿名函数它只能写单行表达式不能包含语句和复杂逻辑。很多新手一看到lambda就觉得难其实它就是临时用一下的函数。我个人的经验是**如果lambda内部超过三四个操作或者有if-else分支就别硬写lambda定义一个普通具名函数。**具名函数的好处是能加注释、能单测lambda写得太复杂反而可读性崩盘。这里有个需要注意的版本特性在Python 2里map和filter返回列表Python 3里它们返回迭代器对象所以需要用list()包裹才能看到具体内容。如果你用for item in map(...)迭代也是可以的因为迭代器支持遍历。省内存是好事但调试的时候打印不出来曾经让我困惑了好一会儿。3.3 其他高频内置函数enumerate、zip、isinstance、getattr等enumerate遍历列表时同时拿到索引和值比手动维护计数器要优雅得多。for idx, value in enumerate(data, start1): print(idx, value)zip把多个列表按索引对齐组成元组列表。在同时遍历两个列表时是神器比如把球员名字和得分一一对应起来。isinstance判断对象类型。isinstance(3, int)返回True。它比type(x) int更推荐因为isinstance会考虑继承关系。内置类型继承体系里经常有这种行为差异写通用工具函数时用isinstance更稳。getattr按名字获取对象属性。这个函数在写反射式代码时很有用比如根据配置字符串动态调用某个方法handler getattr(module, handler_name, None)。这些内置函数单独看都挺小但组合起来能写出非常紧凑的代码。我给一个实际例子统计一个文本文件里每个单词出现的次数按出现次数降序输出前十个。from collections import Counter with open(article.txt, r, encodingutf-8) as f: text f.read() words text.lower().split() counter Counter(words) for word, count in counter.most_common(10): print(f{word}: {count})Counter不是内置函数而是collections标准库里的类但它和open、split这些组合起来的行数比手写循环加速字典要少得多而且most_common直接处理了排序不用自己折腾sorted的key参数。4. 函数进阶闭包、装饰器、生成器与偏函数如果你只是写点小脚本前面那些内容完全够用了。但只要你开始写工具库、写Web后端、写爬虫框架或者被要求在不动原函数代码的前提下加功能你就必须接触闭包、装饰器和生成器了。这一节是整篇内容里最有含金量的部分。4.1 作用域与闭包为什么函数记住了外部变量Python有四种作用域局部作用域局部变量、嵌套作用域外层函数的变量、全局作用域、内置作用域。这个体系的重点是内层函数可以读取外层函数的变量但默认情况下不能直接修改除非用nonlocal声明。闭包指的是**外层函数把内层函数作为返回值返回内层函数同时还记住了外层函数局部变量。**最经典的例子def make_multiplier(factor): def multiplier(x): return x * factor return multiplier double make_multiplier(2) triple make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15这里double和triple虽然是同一个make_multiplier造出来的但它们各自记住了不同的factor互不干扰。闭包的威力在于你可以在不污染全局命名空间的前提下给函数绑定状态。我写分析工具时经常用闭包来做带配置的计算函数比如一个带基础税率的计价函数比每次都传那个税率参数干净多了。闭包有个容易踩的坑如果内层函数想修改外层变量直接用factor factor 1会报错或行为不符合预期因为Python解释器把factor当成了内层函数的局部变量。解决办法是加nonlocal factor声明。这个坑我在写计数器闭包时踩过排查了十分钟才发现是赋值语句改变了变量的作用域解析。4.2 装饰器在不改原函数的前提下加功能装饰器是闭包最常见的一种应用形式本质是一个接收函数、返回新函数的函数。它的使用场景很典型日志、性能计时、权限校验、缓存、重试逻辑。来看一个最朴素的装饰器import time def timing_decorator(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(f{func.__name__} 耗时 {end - start:.4f} 秒) return result return wrapper timing_decorator def heavy_compute(n): return sum(range(n)) heavy_compute(1000000)timing_decorator相当于heavy_compute timing_decorator(heavy_compute)所以调用heavy_compute时实际执行的是wrapper它在调用原始函数前后加了计时逻辑最终返回原始函数的结果。因为*args, **kwargs把参数转发给了原函数所以不管原函数签名是什么这个装饰器都能正常工作。这里有个很多教程没说的细节装饰器里返回的wrapper.__name__已经变成wrapper了如果你依赖函数名做日志或者调试会很困惑。解决办法是用functools.wraps它会把原函数的元信息名字、文档字符串等复制到包装函数上。我写所有装饰器时都默认带上functools.wraps(func)这是一个好习惯。装饰器也可以带参数。比如你想写一个能指定日志级别的装饰器那需要在普通装饰器外面再包一层def logger(levelINFO): def decorator(func): wraps(func) def wrapper(*args, **kwargs): print(f[{level}] 调用 {func.__name__}) return func(*args, **kwargs) return wrapper return decorator logger(levelWARNING) def do_something(): pass这个三层嵌套一开始看有点晕但理解了装饰器是接收函数返回函数的函数之后logger(levelWARNING)其实先执行logger(levelWARNING)返回decorator然后decorator(do_something)返回wrapper。拆解下来就不神秘了。我实际项目里用装饰器最多的地方是重试。网络请求偶尔失败加一个带重试次数和延迟时间的装饰器比在每个请求代码块里写try-except循环要清爽得多。4.3 生成器函数用yield改写大文件处理生成器是整个Python函数体系里我特别喜欢的一个特性。它的核心是**函数执行到yield时暂停把值交给调用方下次调用next()时从暂停处继续执行。**这让按需生成序列成为可能不用一次性把整个序列创建到内存里。比如要处理一个5GB的日志文件用普通方式for line in open(large.log)其实已经是逐行读的因为文件对象本身就是迭代器。但如果你要处理的是一个复杂的生成逻辑比如生成斐波那契数列的前若干项一次性生成100万项会很占内存生成器就能省很多def fibonacci(): a, b 0, 1 while True: yield a a, b b, a b fib fibonacci() for _ in range(10): print(next(fib))这个fibonacci是一个无限序列生成器但内存占用恒定的几个变量不会爆炸。生成器的典型应用场景是数据量太大不适合一次性加载、需要惰性计算、或者你想把产生数据的过程和消费数据的过程解耦。yield和return的关系当生成器函数执行完或遇到return时迭代结束。你可以在生成器末尾写不带值的return来表示结束但不能写带值的return。我在处理接口分页数据时经常用生成器把不断请求下一页、解析数据、yield出单条记录封装成一个生成器调用方就可以用for item in fetch_all_pages(url):非常自然地遍历所有数据而不用关心翻页逻辑。4.4 functools.partial用偏函数固定参数functools.partial偏函数可以让你基于一个已有函数创建一个已经填好部分参数的新函数。最典型的场景是某个函数有很多可选参数你经常用同一套参数调用它。from functools import partial def connect(host, port, timeout30): # 模拟连接 pass connect_local partial(connect, host127.0.0.1, port8080) # 之后调用 connect_local(timeout10) 相当于 connect(127.0.0.1, 8080, timeout10)偏函数不会自动帮你把参数填好它只是把预置参数和调用时剩余参数组合起来传给原函数。拿它来复用固定配置很方便而且比每次写完整参数可读性更好。4.5 回调函数异步与事件驱动里的函数传参最后讲一下回调函数。在Python里函数是一等对象意思是你可以把函数当作参数传给另一个函数。回调函数就是在某个事件发生时由调用方主动调用你传进去的那个函数。之前热搜词里有个select函数我猜你可能是在查I/O多路复用里的select它在Python标准库的selectors模块里配合回调使用。另一个更常见的例子是tkinter里的按钮绑定import tkinter as tk def on_click(): print(按钮被点击了) root tk.Tk() btn tk.Button(root, text点我, commandon_click) btn.pack() root.mainloop()on_click就是回调函数它在按钮被点击时被事件循环调用。理解回调函数的核心不是语法而是执行时机的反转不是你在写代码时调用它而是某个事件发生时才由框架调用它。所以回调函数的设计要保证可重入、不阻塞、不抛出未捕获异常因为框架调用你的回调时你不确定异常会被怎么处理。5. 函数调试实战从不是内部命令说起函数写得再好最终要跑起来环境配置不对一切都是空的。我见过不少初学者卡在第一步代码写得没问题但执行时报错说命令不存在然后还以为是自己的函数写错了。5.1 环境问题pip、python命令无法识别的排查思路热搜词里有一条很典型的报错无法将pip项识别为 cmdlet、函数、脚本文件或可运行程序的名称类似的还有python、git、claude、opencode、mvn这些命令。这个报错的本质只有两种可能你对应的软件没安装。软件装了但安装路径没有加入系统的PATH环境变量。排查步骤我建议按这个顺序来先确认软件是否真的装了。Windows下打开设置-应用搜索Python看看有没有。如果装了打开PowerShell手动执行python.exe的完整路径。比如C:\Users\你的用户名\AppData\Local\Programs\Python\Python312\python.exe --version如果能执行说明是PATH的问题。如果是PATH的问题需要手动把Python的安装目录和Scripts子目录pip在这里加到环境变量里。在Windows上配置Python环境变量我整理了一个标准流程第1步找到Python实际安装位置。一般可以用where python命令但where的前提是python已在PATH中如果不在就去安装目录或者开始菜单里找到Python快捷方式右键打开文件位置。第2步打开系统环境变量设置。Win键搜索环境变量选择编辑系统环境变量点环境变量按钮。第3步在系统变量里找到Path双击编辑点新建把Python安装目录和Scripts目录添加进去C:\Users\你的用户名\AppData\Local\Programs\Python\Python312\C:\Users\你的用户名\AppData\Local\Programs\Python\Python312\Scripts\第4步重新打开一个终端窗口执行python --version和pip --version验证。注意修改完环境变量后必须新开一个终端窗口才能生效旧窗口不会刷新PATH。这个步骤我写得很细因为做技术的人经常遇到的一个尴尬情况是网上教程都写把Python加入环境变量但没人教你具体怎么找到路径、怎么编辑Path。我记得自己第一次配环境时找了半天才搞明白PATH是以分号分隔的路径列表每一个条目就是一个可执行文件的搜索目录。5.2 VSCode中Python环境配置要点代码编辑器里的Python环境配置是另一个高频问题。VSCode里装了Python扩展不等于就能正确运行你的Python代码。最常见的坑是VSCode默认选择的解释器不是你装的那个Python导致代码能打开但运行时报错说某些模块找不到。正确配置流程安装VSCode官方Python扩展由Microsoft发布。打开一个包含.py文件的项目文件夹。按CtrlShiftPmacOS是CmdShiftP输入Python: Select Interpreter选择你已经安装好的解释器。如果你用虚拟环境VSCode会自动检测项目目录下的.venv选那个环境这样依赖隔离最干净。判断是否配置成功的最快方式是看VSCode右下角状态栏它应该显示当前的Python解释器路径。另外请一定配置好默认终端让终端自动激活虚拟环境。我自己最常用的做法是在项目根目录创建.venv虚拟环境然后VSCode会自动识别到它每次打开终端它会自动激活。这样不同项目之间的依赖互不干扰函数调试也稳定得多。5.3 函数调用常见报错排查速查表函数写多了什么报错都遇得到。这里整理一份我实际工作中出现频率最高的函数相关报错和排查思路报错信息常见原因解决方向NameError: name xxx is not defined函数名拼写错误或函数定义在调用之后检查拼写把函数定义放在调用之前TypeError: xxx() takes 1 positional argument but 2 were given参数数量不匹配检查函数签名和调用参数是否一致IndentationError: unexpected indent缩进不一致混用了空格和Tab统一用4个空格缩进UnboundLocalError: local variable x referenced before assignment函数内赋值导致变量被当作局部变量需要使用global或设计为传入参数ValueError: I/O operation on closed file文件已被关闭但仍尝试读写检查with块内是否还有文件读写操作AttributeError: NoneType object has no attribute xxx函数没有返回值调用方拿到的结果是None检查函数是否有return覆盖所有分支这些报错都有一个共同排查技巧报错信息里会带文件名、行号、具体变量名先别看整段报错先去看它指的那一行。我遇过很多新手一看到红字就慌其实报错已经告诉了你行号和变量名照着改就行。6. 一个完整案例用函数重构一个数据抓取与分析流程理论讲了再多不如完整跑一遍。这一节我用一个接近真实的场景把前面所有内容串起来从抓取网页到数据处理到可视化输出全部用函数组织。6.1 原始脚本的问题分析假设我要做一个抓取某公开网站文章标题和发布时间统计各个月份发文数量的小项目。很多人一开始会写成一个大脚本import requests from bs4 import BeautifulSoup from collections import Counter url https://example.com/articles response requests.get(url) soup BeautifulSoup(response.text, html.parser) titles [] dates [] for article in soup.select(.article): title article.find(h2).text date_text article.find(time)[datetime] titles.append(title) dates.append(date_text.split(-)[:2]) for date in dates: print(date) counter Counter(dates) for month, count in counter.most_common(): print(month, count)这段代码的问题很明显抓取、解析、统计、输出全混在一起如果我想换一个网站或者想只统计不打印或者想加一个缓存都得大改。咱们把它函数化。6.2 函数化重构的完整实操先拆函数边界。我的设计思路是抓取页面、解析文章列表、统计月份分布、输出结果是四个完全独立的职责每个都能单独测试。import requests from bs4 import BeautifulSoup from collections import Counter from typing import List, Tuple def fetch_page(url: str) - str: 获取网页内容返回HTML字符串 response requests.get(url, timeout10) response.raise_for_status() # 状态码不是2xx时抛异常 return response.text def parse_articles(html: str) - Tuple[List[str], List[str]]: 解析HTML返回(标题列表, 年月列表) soup BeautifulSoup(html, html.parser) titles [] months [] for article in soup.select(.article): title article.find(h2).text date_text article.find(time)[datetime] # 例如 2024-05-20 month date_text[:7] # 取到年月 titles.append(title) months.append(month) return titles, months def count_by_month(months: List[str]) - Counter: 统计各月份文章数 return Counter(months) def print_report(counter: Counter) - None: 打印统计报告 for month, count in counter.most_common(): print(f{month}: {count} 篇) def main(url: str) - None: 主流程抓取 - 解析 - 统计 - 输出 html fetch_page(url) titles, months parse_articles(html) month_counter count_by_month(months) print_report(month_counter) if __name__ __main__: main(https://example.com/articles)重构以后每个函数都只干一件事fetch_page只负责网络请求和异常处理parse_articles只负责解析HTMLcount_by_month只负责统计print_report只负责输出。你想要换一个数据源只需要改main里传的URL或者写一个适配器函数把别的格式转成(titles, months)。想要把结果存数据库只需要改print_report其他函数完全不用动。这里重点讲一下main函数的设计。Python脚本作为主程序执行时__name__变量会被设置为__main__所以if __name__ __main__:这个判断能保证只有你直接运行这个文件时才执行main()如果你从其他模块import这个文件它不会自动跑流程而是把函数暴露给导入方使用。这个写法是Python项目的基本规范我的所有脚本都遵守这个惯例。6.3 用函数与可视化验证结果统计出各月文章数之后如果你想画一张趋势图可以再加一个函数。这里用到matplotlib和前面讲的zip、sorted这些工具import matplotlib.pyplot as plt def plot_monthly_trend(counter: Counter) - None: 绘制月度发文趋势图 # 按月份排序 sorted_items sorted(counter.items()) months [item[0] for item in sorted_items] counts [item[1] for item in sorted_items] plt.figure(figsize(10, 5)) plt.plot(months, counts, markero) plt.title(每月发文数量趋势) plt.xlabel(月份) plt.ylabel(文章数) plt.grid(True) plt.show() def main(url: str) - None: html fetch_page(url) titles, months parse_articles(html) month_counter count_by_month(months) print_report(month_counter) plot_monthly_trend(month_counter)这个案例说明了一件事**函数化不是把代码变多而是把功能边界变清晰。**同样一个爬虫项目线性脚本一天跑完就扔函数化之后你可以逐步扩展加缓存、加异常重试、加可视化、加定时任务每一步都只需要新增或修改一个函数不会牵一发动全身。7. 我对Python函数的一些体会与建议函数写了好多年普通语法早已不是瓶颈真正拉开差距的是设计品味。这里分享几个我这几年实际得出来的经验。第一函数的命名。Python官方推荐小写加下划线的命名方式比如calculate_average、fetch_page。你可能会觉得这是小事但等代码量上来以后你搜索、引用、回忆的难度完全取决于名字是否清晰。我见过一个项目里一百多个函数名字全是func1、func2这种别说维护就连调用关系都理不清。一个好名字胜过一段注释。第二函数的长度。一条经验法则如果函数体超过五十行大概率该拆了。不是硬性规定但很管用。五十行以内的函数你一眼能看完心智负担低调试容易超过五十行你就要上下滚动盯着看边界很容易漏。第三默认参数的坑。前面讲过的[]共享问题是Python新手到中级最容易踩的陷阱之一。我自己写工具库时为了避免这个问题所有默认参数遇到可变类型一律用None加if x is None处理宁可多写两行也不留隐患。第四关于使用global。Python的函数默认读取全局变量但修改需要global声明。我的建议是在项目代码里尽量不要使用global因为它会让函数之间产生隐式耦合改一个全局变量所有读它的函数全受影响排查脑壳疼。真需要共享状态时优先考虑传入参数、使用类来封装或者使用生成器/闭包来管理状态。第五多写类型注解和docstring。Python是动态类型语言类型注解是可选的但你写了之后IDE的提示、代码评审的理解、自动化测试的边界检查都会上一个台阶。docstring我用的是Google风格简洁明了配合一些工具还能自动生成文档。第六最后一点这可能是我最想强调的**函数是给人看的不是给机器看的。**机器执行你的代码不管写成什么样都能跑但你的同事、三个月后的你自己需要读懂你的代码。所以写函数的时候多想一想如果我不知道这个函数的上下文我能看懂它在干什么吗如果能那这就是一个及格的函数如果还能看明白为什么这样写那这就是一个优秀的函数。我实际使用Python这么多年函数从语法到设计就是这样一个从知道到理解再到有自己的品味的过程。如果你刚开始学先把参数、返回值、作用域搞扎实如果你已经在写项目试着用装饰器、生成器、闭包去优化你那些重复的、凌乱的代码。不用急着一次学会所有东西但每学一个概念都把它用到一个真实场景里去这样你才能真正掌握它。
返回列表