1. 从“会写函数”到“写好函数”:进阶到底在进什么
先聊个现象。我带过不少新人,也看过大量项目代码,发现很多人写 Python 写到一定阶段,会卡在一个很微妙的位置:函数能写、能调、能跑,但写出来的代码总有一种“差口气”的感觉。具体表现就是——参数列表长得吓人,函数体里一堆 if else 在判断参数类型,想扩展一个新功能就得把原来的函数复制一份改改,或者代码里到处都是重复的装饰逻辑,改一处漏十处。
这个“差口气”,就是函数基础到进阶之间的那道坎。函数是 Python 里最核心的抽象机制之一,你写的每一行非声明式代码,几乎都在和函数打交道。把函数玩明白,不只是学会几个语法糖,而是真的要理解 Python 这门语言的设计哲学:一切皆对象、函数是一等公民、约定优于配置。
这篇博文,我基于实际项目中反复用到的函数进阶技术点,把参数机制、作用域与闭包、装饰器、生成器这些内容掰开揉碎讲清楚。每个部分都会交代“为什么这样设计”“实际项目中怎么用最稳”“有什么坑”。不管你是刚学完 Python 基础、准备系统进阶的初学者,还是写了两三年脚本但没系统梳理过函数机制的开发者,这篇都会给你一些有用的东西。
先说结论吧:函数进阶的核心,其实就两件事——更灵活地传递和接收数据,以及更优雅地复用代码逻辑。围绕这两件事,Python 给出了参数解包、可变参数、闭包、装饰器、生成器等一系列解决方案。下面逐个拆。
2. 参数的艺术:从直来直去到百变灵活
2.1 位置参数、默认参数与关键字参数的协作逻辑
大多数 Python 学习者第一个接触的是位置参数,因为最直观:调用函数时,传参顺序和定义时的形参一一对应。但这只是最基础的用法。真正写项目时,你会发现一个设计良好的参数列表,能直接影响函数的可读性和可维护性。
位置参数适合那些“缺了就不行、且顺序天然固定”的核心输入。比如文件处理函数,open(file, mode)就是典型的例子,路径和模式顺序约定俗成,基本没人会搞混。默认参数则适合“大多数场景下有一个最合理取值,但允许调用方按需覆盖”的情况。比如网络请求的超时时间,默认 30 秒,某些慢接口可以单独调大。
这里有个容易踩的坑:默认参数的求值时机。Python 的默认参数只会在函数定义时求值一次,之后每次调用都复用同一个对象。如果你写def append_item(item, lst=[]):这种代码,多次调用会发现列表里持续累积数据,因为大家共享的是同一个列表对象。
提示:默认参数务必使用不可变类型。如果确实需要默认容器,标准写法是
def append_item(item, lst=None): if lst is None: lst = []。这个坑我至少在生产代码里见过五次,每次都折腾半天。
关键字参数则提供了另一种调用维度:调用方可以按名字传参,不依赖顺序,也大大提升了代码自文档性。以create_user(name, age, email=None, phone=None)为例,调用时写create_user("张三", 28, email="zs@example.com")比光靠位置传参清晰得多,尤其是当默认参数变多时。
实用建议是:函数参数设计遵循“必选位置参数在前,带默认值的在后,关键字参数收尾”的基本原则,这条规则从语言层面保证了调用时的可读性和灵活性。
2.2 星号魔法:*args与**kwargs的接收和解包
*args和**kwargs是函数进阶越不过去的一道坎。很多人知道它能接收不定量参数,但理解多停留在死记硬背的层面。我换个角度讲:这其实是 Python 解包机制的两种体现。
先说接收端。定义一个函数def log(level, *args, **kwargs):,调用log("INFO", "msg1", "msg2", user="alice")时,args会收走所有多余的位置参数,变成一个元组("msg1", "msg2"),kwargs收走所有多余的关键字参数,变成一个字典{"user": "alice"}。打印日志时就可以统一处理:
def log(level, *args, **kwargs): parts = [f"[{level}]"] parts.extend(str(a) for a in args) for k, v in kwargs.items(): parts.append(f"{k}={v}") print(" ".join(parts))再看发送端。调用函数时用*和**,可以把一个可迭代对象或字典解包成位置参数和关键字参数传递进去。这个技巧在调用第三方库时特别实用。比如某库的函数签名是def draw(x, y, width=10, height=10, color="red"),你手里有一份配置字典,直接draw(**config)就可以,不用写一行行的显式传参。
更进一步,*args, **kwargs在装饰器、函数封装、中间件场景里是必须品。因为你封装回调函数时,往往不知道被包装函数到底接收什么参数,用星号魔法把参数原样接住、原样转发,是最稳的做法:
def safe_execute(func, *args, **kwargs): try: return func(*args, **kwargs) except Exception as e: log_exception(e) return None注意下命名。args和kwargs只是约定俗成的名字,真正代表语义的是那枚星号。你也可以写*items、**options,行为完全一样。但既然社区大家都这么写,保持惯例更利于协作。
2.3 强制关键字参数与参数设计的真实业务案例
Python 3 之后新增了一个很实用的语法特性:在*args之后的参数,强制要求以关键字形式传入。这有什么用?看一个实际场景。假设你在维护一个支付接口,签名是:
def create_payment(order_id, amount, *, channel="alipay", callback_url=None): ...这里的channel和callback_url只能通过关键字传入,不能靠位置猜。这种做法把调用方强行按在了“每条参数都必须写明用途”的轨道上,对于参数多、容易混淆的接口来说,防呆效果极好。我在内部工具类库里就大量使用这个写法,尤其是那些同时有mode、path、timeout、retries这种含义相近参数的方法。
参数设计还有一个更根本的问题:什么时候该把多个参数收拢成一个对象?经验法则很简单——如果一个函数的参数里出现三个及以上同时变化的业务字段,就该考虑用一个小对象或命名元组收拢起来。比如register_user(name, age, email, phone, address, level)这种,调用方记参数顺序都累,重构时新增一个字段还会造成大范围改动。改成register_user(user: User),不仅调用清晰,而且未来加字段只需要改User定义。
再看一个平时写代码常见的优化思路:函数参数尽量保持只读。如果一个函数内部要修改传入的可变容器,应该先拷贝再操作。这不是洁癖,而是因为调用方往往还在用着同一个列表,你在函数里偷偷改了,外面查半天都不知道数据跑哪去了。
3. 作用域、闭包与函数式编程:理解 Python 的变量查找规则
3.1 LEGB 规则与global、nonlocal的适用边界
函数进阶绕不开一个基础机制:变量作用域。Python 的变量查找遵循 LEGB 规则,即局部(Local)→ 嵌套函数外层(Enclosing)→ 全局(Global)→ 内置(Built-in)。理解这个查找顺序,很多怪异行为就能解释通了。
举个例子,一个常见误解:
count = 0 def add_one(): count += 1这段代码会直接报UnboundLocalError。原因是在函数内部给count赋值,Python 解释器在编译函数时就把count标记为局部变量,所以执行count += 1时不会向外层去找全局的count,但此时局部count尚未绑定,于是直接报错。
这个设计初看反直觉,其实是为了性能和安全。如果函数内的赋值操作随时可能意外修改全局变量,程序的行为就完全不可控了。真要改全局变量,用global声明。但我的经验是,生产代码里global用得越多,模块间的耦合就越严重,状态越难追踪,调试成本成倍上升。
注意:能用参数传递解决的数据传递,绝对不要用全局变量绕过去。全局变量是隐式依赖,函数定义和调用处隔着十万八千里,改起来牵一发动全身。
nonlocal则是用于嵌套函数中修改外层函数的局部变量。它和global类似,但作用范围更精确——专门解决闭包中“我确实要更新外层状态”的场景。下面这个计数器,就是nonlocal的经典用法:
def make_counter(): count = 0 def add_one(): nonlocal count count += 1 return count return add_one3.2 闭包的本质:函数与自由变量的绑定
闭包这个概念,说起来玄,拆开看就是一句话:内层函数引用了外层函数的变量,并且外层函数已经返回,内层函数仍然持有那个变量的引用。这种绑定关系,让函数有了“记忆”。
闭包最常见的应用之一是替代简单类。比如你要给多个用户分别维护不同的访问计数,用类要定义属性、方法,考虑__init__存状态;用闭包轻量得多,外层函数一调用就生成独立的计数环境,内层函数每次执行都在操作自己的自由变量。
我实际项目里用闭包用得比较多的场景是配置复用。比如一个数据处理流程需要多次调用同一个算法,但每次的配置参数略有差异:
def make_normalizer(mean, std): def normalize(value): return (value - mean) / std return normalize normalizer_a = make_normalizer(0.5, 0.1) normalizer_b = make_normalizer(1.0, 0.3)闭包的另一个良性副作用是信息隐藏。外层函数的变量对外部完全不可见,外部只能通过返回的函数来间接操作,相当于天生自带了私有变量机制。
闭包有个经典坑必须单独拎出来说。如果你在一个循环里创建闭包,并且闭包引用循环变量,那么所有闭包共享的是同一个循环变量的最终值——这就是著名的延迟绑定问题。看代码:
funcs = [] for i in range(3): funcs.append(lambda: i) for f in funcs: print(f()) # 输出 2, 2, 2原因在于lambda内部的i是自由变量,等到最终调用时它才被求值,而那时循环已经结束,i停在 2。解决方法有几种:一种是把i作为默认参数绑定进去,lambda i=i: i;另一种是用 functools.partial;还有一种就是干脆写个工厂函数,每次循环传入当前值生成新闭包。我习惯最后一种,语义最清晰。
3.3 高阶函数:lambda、map/filter 与 sorted 的实战选择
高阶函数指的是接收函数作为参数或把函数作为返回值的函数。Python 内置的map、filter、sorted都是典型代表。配合 lambda 可以写出很简洁的代码:
names = ["alice", "bob", "carol"] lengths = list(map(len, names)) adults = list(filter(lambda u: u.age >= 18, users)) sorted_users = sorted(users, key=lambda u: u.last_login, reverse=True)但这里我必须说一句可能得罪人的话:lambda 不是万能的,它不是用来写复杂逻辑的。lambda 的设计初衷是“一句话能说清楚的简单函数”,表达式里写循环、写多分支,甚至写半个业务逻辑,都是滥用,只会让代码变成天书。
我的实操原则是:lambda 体超过一行直接不写,改用def定义具名函数。具名函数的额外好处是能单测,能复用,报错时 traceback 里能看到函数名而不是<lambda>。很多面试题喜欢考怎么用 lambda 写复杂表达式,我建议你了解即可,别把花活写进生产代码。
sorted的key参数是我最推荐养成习惯的内置函数用法。很多人排序时经常先搞个循环把键提取成新列表,再排,纯属多此一举。key=lambda u: u.age一行就解决了,内部实现还更高效,因为 key 函数只执行一次,算完缓存起来参与排序。
函数式编程、面向对象和面向过程不是互斥的,它们各有所长,能在合适的地方用合适的技术,才是“进阶”的真正含义。
4. 装饰器:Python 最优雅的代码复用机制
4.1 装饰器的本质:一个接收函数、返回函数的普通函数
很多人第一次接触装饰器就觉得“魔法”。拆掉这层魔法,装饰器不过是一个语法糖:它把“把函数 A 传给函数 B,再把 B 的返回值赋回给 A”这个过程包装了起来。
看一个最简单的装饰器:
def my_decorator(func): def wrapper(*args, **kwargs): print("before") result = func(*args, **kwargs) print("after") return result return wrapper @my_decorator def say_hello(): return "hello"等价于:
def say_hello(): return "hello" say_hello = my_decorator(say_hello)理解了等价关系,你就明白了几个关键事实:装饰器是在函数定义完成后立即执行的,不是调用时才执行;装饰器返回的wrapper替代了原来的函数名;所以如果你在wrapper里不调用原始func,原函数就等于被“屏蔽”了。
在企业级开发中,装饰器最常见的用途是横切关注点,即那些与业务逻辑无关、但又横跨各个模块的公共逻辑:日志、鉴权、限流、缓存、重试、事务控制。这些功能如果散落在每个函数内部,代码会严重重复,而且容易在不同地方实现得不一致。
4.2 实用装饰器案例:计时、日志、重试和结果缓存
我在生产代码中写过的装饰器少说几十个,挑三个最常用且能直接“抄作业”的分享出来。
计时装饰器是最容易上手的练习,也是性能排查的利器:
import time import functools def elapsed_time(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) cost = time.perf_counter() - start print(f"{func.__name__} took {cost:.4f}s") return result return wrapper重试装饰器是处理网络抖动、临时性错误的实用小工具。核心参数是重试次数和重试间隔,实现重点在于异常处理逻辑清晰,同时要避免无限重试把服务拖垮:
def retry(max_retries=3, delay=1.0, exceptions=(Exception,)): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except exceptions as e: if attempt == max_retries - 1: raise time.sleep(delay * (attempt + 1)) return None return wrapper return decorator结果缓存装饰器,适用于那些计算昂贵但输入确定、结果可复用的函数。如果数据量不大,可以直接放内存字典;数据量大或希望跨进程重用的,可以对接 Redis。自己写缓存要慎重,缓存键设计、过期策略、并发写入都是坑,不是简单存个dict就行。好在标准库已经有functools.lru_cache,可以满足大多数单进程场景:
@functools.lru_cache(maxsize=128) def fetch_config(key): # 模拟耗时配置读取 return {"config": key}4.3 必须掌握的functools.wraps与装饰器嵌套顺序
装饰器最大的隐性坑,是它会覆盖原函数的元信息。wrapper.__name__会变成"wrapper",__doc__变成None,这会导致函数签名工具(如 IDE 提示、文档生成器)失灵,甚至某些依赖函数名做处理的框架(如 Flask 的端点路由)直接报错。
解决方式简单粗暴:在定义wrapper时加上@functools.wraps(func)。它的内部实现就是把func的__name__、__doc__、__module__、__dict__等元信息拷贝到wrapper上,同时还更新了__wrapped__属性指回原函数,便于inspect等工具继续查看原始签名。
多个装饰器叠加执行时,顺序很容易绕晕。记住一条规律:装饰器从底部向上包裹,执行时从上向下生效。
@timer @retry(max_retries=5) def call_api(): ...这段代码的执行顺序是:先执行retry生成重试包装函数,再把这个包装函数传给timer。最终调用call_api()时,先计时,然后进入重试逻辑,重试逻辑内部才真正调用原函数。如果反过来写,每次重试都会被单独计时,计时结果会覆盖重试总耗时,语义就完全错了。
装饰器还要考虑一个问题:它是否适用带参函数、类方法、静态方法?类方法和静态方法的第一个参数机制不同,直接套装饰器有时会出诡异问题,稳妥做法是装饰器内部统一用*args, **kwargs接参数并原样转发,绝不假设位置参数的个数和含义。这也是上面所有示例都在贯彻的原则。
5. 生成器与惰性求值:处理大数据量的更优方案
5.1 从yield关键字看生成器的运行机制
生成器函数在 Python 函数进阶里地位特殊,因为它是对函数执行流程的彻底重构。普通函数从第一行跑到最后一行,中途 return 就结束;生成器函数则像是被装了暂停键,每次执行到yield就挂起,把值交给调用方,等到下次迭代请求再继续往下走。
理解生成器,关键在于理解 Python 的迭代协议。一个对象能够被for循环遍历,并不要求它必须是列表、元组这些容器类型,只要它实现了__iter__或__next__方法就行。生成器函数天然满足这个协议,所以可以直接被for消费。
举一个对新手很直观的对比:
def make_list(n): result = [] for i in range(n): result.append(i * 2) return result # 一次性生成全部数据,占内存 def make_generator(n): for i in range(n): yield i * 2 # 每次只生成一个数据,其余状态保留在函数内部调用make_list(10000000),内存暴涨;调用make_generator(10000000),几乎不占额外内存。区别不在于语法,而在于惰性求值——生成器只在被请求的那个时刻才执行一次 yield。
我在处理日志解析、接口分页拉取、超大文件读取这些场景时,几乎无条件选择生成器。比如读取一个 10GB 的日志文件筛选关键信息,for line in file本身就是逐行读取的,不会一次性载入内存。但如果你在图方便时用了.readlines(),那内存立刻就爆。
5.2 生成器表达式与推导式:选择背后的性能和语义考量
生成器表达式在语法上和列表推导式极其相似,只是把方括号换成圆括号:
even_squares = [x * x for x in range(100) if x % 2 == 0] # 列表 even_squares_gen = (x * x for x in range(100) if x % 2 == 0) # 生成器区别有两层意涵。内存上,列表推导式一次性创建完整列表,生成器表达式按需产出,不缓存所有结果。传输语义上,列表是“已计算好的快照”,生成器是“可迭代的生产线”,后者无法知道自身长度、不能随机索引、也不能重复遍历。
所以这里有一个常见的误区:“一切都用生成器表达式是不是就最好了?”并不是。如果你需要反复遍历同一个数据集,或者需要切片、索引访问、求长度,生成器做不到,硬套只会让代码绕圈子。正确做法是:一次遍历足够且数据量大时,选生成器;数据量小或需要多次随机访问时,用列表。
yield from是在多层嵌套迭代时非常实用的语法。比如处理分页接口,每一页返回一批数据,你想把它们拍平成一个连续的迭代流:
def fetch_all_pages(): page = 1 while True: data = fetch_page(page) if not data: break yield from data page += 1yield from data相当于把子迭代器里的每个元素yield出来,省去了手写内层循环的样板代码。此外,yield from还能把send、throw等操作透明的传递到子生成器,这些机制是理解 Python 原生协程的重要前置知识。作为常规业务开发,能主动用生成器处理大数据集、用yield from组织嵌套迭代,已经算进阶到位了。
5.3 案例:用生成器实现流式日志分析
讲这么多理论,我分享一个综合运用生成器特性的实战案例。假设你有大量服务日志,想看每个接口的调用次数、最大耗时、平均耗时。朴素的实现是逐行读文件,遇到符合格式的行就更新统计字典。但这中间其实分为了“筛选行—解析字段—聚合统计”三个职责,用生成器逐层拆分,会让代码结构清晰很多。
第一层,逐行读取日志文件:
def read_log_lines(file_path): with open(file_path, "r", encoding="utf-8") as f: for line in f: yield line第二层,按正则筛选并解析出目标字段:
import re pattern = re.compile(r'api=(?P<api>\S+)\s+cost=(?P<cost>\d+)') def parse_log(stream): for line in stream: m = pattern.search(line) if m: yield m.group("api"), int(m.group("cost"))第三层,真正的聚合统计:
from collections import defaultdict def aggregate(stream): counts = defaultdict(int) total_cost = defaultdict(int) for api, cost in stream: counts[api] += 1 total_cost[api] += cost return {api: {"count": n, "avg": total_cost[api] / n} for api, n in counts.items()}调用时是aggregate(parse_log(read_log_lines("app.log")))整个流水线串起来。每一层只做一件事,每一层都惰性执行,对上亿行的日志也不会造成内存压力。更重要的是,任何一层的逻辑想替换都很容易,比如从读文件换成从 Kafka 消费消息流,只需要换掉第一层。
注意:生成器的最大缺陷是只能遍历一次。如果你在遍历生成器时把它拆给两个消费者,第二个消费者会拿到空结果。需要多次处理时,要么先落地成列表,要么重新创建生成器。这个特性在工程上经常被忽略,导致数据“神秘消失”。
6. 常见问题与实战排查:函数进阶路上的典型坑
6.1 可变默认参数导致的“幽灵数据”
这是 Python 面试必考题之一,也是生产环境里真实发生过的诡异 bug。核心现象:函数使用可变类型作为默认参数,则多次调用共享同一个对象,前一次调用对默认参数的修改会保留到下一次调用。
def add_item(item, container=[]): container.append(item) return container print(add_item("a")) # ['a'] print(add_item("b")) # ['a', 'b'] ← 你以为应该是 ['b']我在一个外部 API 封装里见过因为这个导致的线上问题:一个函数带着默认的空列表作为参数,结果把所有请求的中间结果全部累积了起来,最后那个请求返回了之前所有请求的拼接数据。排查的难点在于,它只在请求量大的时候才异常,小规模测试根本看不出问题。
修复套路是统一的:默认值一律写None,函数内部再做空值处理。这样每次调用都创建全新的容器对象,行为完全符合直觉。
6.2 循环中创建 lambda 的延迟绑定问题
前面讲闭包时提过,这里再展开说,因为这个坑在真实代码中的出现频率远超想象。典型业务场景:给一组按钮绑定不同的事件处理函数,或者给表格的每一行生成一个回调。如果代码写成了循环内用 lambda,最终所有按钮点击都触发同一个结果。
buttons = [] for name in ["save", "delete", "cancel"]: buttons.append(lambda: print(name)) # 逐个调用后,全部打印 "cancel"原因在于闭包捕获的是变量name的引用,而非定义那一瞬间的值。循环结束后name的值已经是最后一个元素,所有 lambda 里看到的自然都是"cancel"。
推荐修复方式是使用默认参数绑定当前值:
buttons.append(lambda name=name: print(name))因为在函数定义阶段,默认参数表达式会被求值,name=name等价于把当前循环变量值“固化”进函数。另一种做法是用 functools.partial:
from functools import partial buttons.append(partial(print, name))partial的语义更明确,而且在处理“提前绑定参数,后续再传剩余参数”的需求时比 lambda 更优雅。
6.3 装饰器元信息丢失与堆叠顺序错误
装饰器用完发现函数名全变成了wrapper,这个问题在初学者中非常普遍。出现的原因就是没有加@functools.wraps(func)。影响不只在调试输出,很多框架(如 Flask 路由、Django 的@login_required、FastAPI 的依赖注入)都依赖被装饰函数的元信息来注册路由或生成接口文档。一旦元信息丢失,轻则路由名变成 wrapper 导致 404,重则导致整个应用启动失败。
多个装饰器堆叠顺序错误的情况也很常见,尤其是在一个函数上同时使用“登录校验”和“日志记录”时。到底哪个在外面哪个在里面,取决于你的语义需求。比如先日志记录再执行鉴权,还是先鉴权成功后才记日志,这是完全不同的两种行为。我建议在装饰器命名时就体现层次关系,并且在注释里写明执行顺序,避免过几个月自己都救不回来。
6.4 递归深度限制与性能陷阱
Python 的递归有默认深度限制(通常 1000),超过会抛RecursionError。很多人遇到这个问题第一反应是调大sys.setrecursionlimit(),但这只是把问题往后推。更深层的问题是:Python 的函数调用开销大,递归调用栈帧占用高,纯递归处理大深度数据往往比显式迭代慢且容易崩。
工程上用递归前要评估深度规模。如果只是树形结构的有限深度遍历,递归完全没问题。如果处理的是一个深度可能上万的数据结构,就应该改成显式栈+循环:
def traverse(root): stack = [root] while stack: node = stack.pop() do_something(node) stack.extend(node.children)这样写既避免了递归深度限制,也规避了调用栈溢出,而且在性能上通常优于递归版本。
6.5 函数打包与分发时的注意事项
函数进阶到一定阶段,会面临“函数作为参数传递”和“函数作为返回值”之外的另一个问题:如何把一组函数打包成可复用的库。这里有几个隐藏技巧。
一是参数的透传封装。当你写一个包装函数时,尽量不要在包装层把参数一个个写死再传进去,除非你明确需要改变签名。合理使用*args, **kwargs透传,可以保证被封装函数签名变化时,封装层不用跟着改。
二是模块内使用__all__控制对外暴露的函数列表。这不只是为了好看,更是为了避免from module import *时导入到内部实现函数。建议在新项目里都养成显式声明的习惯。
三是函数注释与类型标注。Python 3 的类型注解不是强制约束,但它是给 IDE 和未来维护者最好的文档。写函数时标上参数类型和返回值类型,变量类型错误就能在写代码阶段被发现,而不是运行到生产环境报错。
7. 最后分享几条实操体会
函数进阶的这趟梳理到这里,核心内容都讲完了。最后说几条我个人在实际项目里的体会。
第一,遇到第三次重复的代码,就抽成函数。这是我一直坚持的底线。前两次可能是巧合,第三次大概率就是设计缺陷。抽的时候顺带想想参数该怎么设计、合理的范围是什么,比在多个文件里来回复制粘贴要省太多事。
第二,装饰器能不用就不用,但用了一定要用对。不要为了炫技给每个函数都加装饰器,过度抽象会让调用链变得极难追踪。但该用的时候也别客气——日志、重试、缓存、鉴权这类横切逻辑,装饰器就是最自然、最 Pythonic 的解法。
第三,生成器是处理数据流的第一选择,而不是最后手段。碰到大数据集,先想想能不能用生成器串起流水线,这样写出的代码天然具备内存友好和职责清晰两个优点。
第四,调试函数相关问题时,先用inspect.signature看一下真实的参数签名。很多“函数调用报错”的问题,归根到底是签名对不上。把这一招养成习惯,能省去大量毫无头绪的排查时间。
函数进阶的本质不是背下几个 API 和语法,而是建立对 Python 数据流和控制流的直觉。参数怎么传、状态怎么存、逻辑怎么复用、数据怎么分批处理,这些设计问题想明白了,你写的 Python 自然就有进阶的样子。