
1. 先搞清楚你的函数到底在接谁的数据很多朋友学Python时函数声明、调用都能写但一旦参数多起来就乱了——哪些参数必传哪些可以不传什么时候用星号什么时候用两个星号函数定义是def func(a, b1, *args, **kw)的时候到底该怎么调于是面试前、考试前、或者半年没写Python突然要捡起来的时候总觉得函数这块知识是散的翻文档又嫌啰嗦。这篇文章就把Python函数传参的全部方式拉通讲一遍。从最基础的位置参数到默认参数、关键字参数再到*args和**kwargs最后讲清楚它们混在一起时的匹配规则和Python 3.0以后才有的强制关键字参数。看完之后你能做到看任何函数签名一眼就知道调用时该怎么传遇到函数定义很复杂、一调就报错的情况能自己定位是少传了还是多传了还是参数顺序不对。适合正在准备面试的、刚学完基础想系统过一遍的以及平时写脚本但很少接触复杂函数设计的朋友。函数的传参问题说白了就是两件事定义时怎么写形参调用时怎么传实参以及两者之间靠什么规则完成匹配。这块一旦通了很多报错就是你一眼能看穿的纸老虎。2. 基础传参方式位置参数、关键字参数与默认参数2.1 位置参数最朴素的匹配规则位置参数是最原始、最直观的传参方式。函数定义时有几个形参调用时就按顺序把实参填进去第一个实数给第一个形参第二个实数给第二个形参一一对应。比如def greet(name, message): print(f{name}{message}) greet(小明, 你好吗)这里小明传给name你好吗传给message。就是这么简单没什么弯弯绕。但要注意调用时传的实参数量必须和形参数量一致少传一个会报missing 1 required positional argument多传会报takes 2 positional arguments but 3 were given。位置参数最大的问题在于调用方必须记住每个位置的语义。如果函数有六七个参数光靠位置去对应代码写出来别人很难一眼看懂时间一久连自己都容易搞混。举个真实场景def config_output(file_path, encoding, mode, overwrite, backup): ...调用时写config_output(a.txt, utf-8, w, True, True)后面三个布尔值各代表什么没人知道得回去翻定义。所以位置参数适合参数少、语义清晰的场景一旦参数多了就要靠关键字参数来救命。2.2 关键字参数调用时指名道姓关键字参数keyword argument就是在调用函数时通过形参名值的方式来传参。好处有两个一是顺序可以打乱二是一眼就能看出传的什么值。比如def config_output(file_path, encoding, mode, overwrite, backup): print(file_path, encoding, mode, overwrite, backup) config_output( a.txt, utf-8, modew, backupTrue, overwriteFalse, )注意这里a.txt和utf-8是位置参数必须放在前面后面的mode、backup、overwrite都是关键字参数顺序随便换都没事。Python在匹配时是这样处理的先把位置参数按顺序分配到前面的形参剩下的形参再用关键字参数去匹配。关键字参数还有一个实际好处——以后函数维护起来更轻松。比如你调用时全都用关键字参数传函数内部后来调整了参数顺序调用方代码完全不用改。这在写公共接口、给别人提供函数时尤其重要接口稳定性的价值在这种细节里就体现出来了。提示位置参数和关键字参数可以混用但位置参数必须出现在所有关键字参数之前。写成func(a1, 2)会直接语法报错。2.3 默认参数让函数有备选方案默认参数是在函数定义时直接给形参赋一个初始值比如def connect(host, port3306): print(f连接 {host}:{port}) connect(127.0.0.1) # 端口取默认值 3306 connect(127.0.0.1, 5432) # 覆盖默认值 connect(127.0.0.1, port8080) # 关键字方式覆盖默认值核心逻辑是调用时如果没有传这个参数就使用定义时的默认值传了就用自己的值。默认参数大大提高了函数的灵活性让函数的低频参数不用每次都显式传。比如print函数的sep、end、flush这类参数就是典型的默认参数。不过默认参数有一个特别经典的坑默认值在函数定义时就被计算并保存了而不是每次调用时重新计算。如果默认值用的是可变对象比如列表、字典就会出问题。这个坑太大我在后面的章节专门讲。2.4 基础传参的完整调用组合把上面的知识点组合一下定义一个函数然后尝试各种调用方法def introduce(name, age18, city北京): print(f我叫{name}今年{age}岁来自{city}) # 只传必填参数 introduce(Alice) # 传一个默认参数的 introduce(Bob, 20) # 指定age不指定city introduce(Cathy, city上海) # 全部用关键字参数顺序完全打乱 introduce(city广州, age22, nameDavid)这段代码可以自己在本地跑一跑把每一行的输出看一遍你就能感受到位置匹配和按名匹配的区别。实际写代码的时候我的习惯是必填参数往前提有默认值的参数往后放超过三个参数且语义不够直观的调用时优先用关键字参数。3. 可变参数星号是如何改变传参格局的3.1 *args把任意多个位置参数收进一个元组先从一个实用场景说起。你想写一个求和函数但不确定调用方会传几个数可能是2个可能是5个也可能是20个。这时候用*args就对了def total(*numbers): print(f收到的参数:{numbers}) return sum(numbers) print(total(1, 2, 3)) # 收到的参数:(1, 2, 3) # 6*numbers的作用是把调用时多出来的所有位置参数打包成一个元组tuple塞给numbers。所以函数内部numbers就是(1, 2, 3)你把它当成元组处理就行。名字不一定要叫args*numbers、*items都可以只是Python圈子里约定俗成用*args你自己写的时候怎么语义化怎么来。*args的典型应用场景包括不知道调用方会传多少个参数比如日志函数、事件处理函数。给函数做一层包装需要把接到的所有参数原样转发给另一个函数。写装饰器时要保证装饰后的函数和原函数签名兼容。其中装饰器这块最常见。你写一个装饰器根本不知道要装饰的函数长什么样那就用*args, **kwargs把参数全部接收下来再原样传进去这是Python装饰器最标准的写法后面我详细拆。3.2 **kwargs把关键字参数收进一个字典*args处理位置参数**kwargs就负责处理关键字参数。它的行为是把调用时传来的所有关键字参数打包成一个字典dict。比如def print_config(**kwargs): for key, value in kwargs.items(): print(f{key} {value}) print_config(hostlocalhost, port3306, debugTrue) # host localhost # port 3306 # debug True**kwargs在什么样的场景下特别有用我觉得最典型的两个场景第一个是框架代码。你写一个底层工具函数希望定义时根本不管上层会传什么额外配置全用**kwargs接住再决定怎么处理。这种设计让函数对未来的变化保持开放新增配置项不用改签名。第二个是配置项合并。比如你需要把默认配置和用户传入的自定义配置合并def setup_server(**custom_config): default_config {host: 0.0.0.0, port: 8080, debug: False} default_config.update(custom_config) print(default_config) setup_server(port9000, debugTrue) # {host: 0.0.0.0, port: 9000, debug: True}注意**kwargs收到的是一个字典所以你可以直接调用update、items、get这些字典方法操作起来非常灵活。3.3 调用端的解包调用函数时用 * 和 **很多人只知道定义函数时*args能收参数却不知道在调用函数时*和**还有镜像作用——把列表、元组、字典展开后传给函数。这个操作叫解包unpacking或者展开。比如你有一个列表想把列表里的元素作为位置实参传进去def calculate(a, b, c): return a b * c nums [1, 2, 3] print(calculate(*nums)) # 相当于 calculate(1, 2, 3) # 输出 7反过来你有一个字典想把字典展开成关键字参数传进去def register(name, age, city): print(f{name},{age}岁,来自{city}) info {name: Tom, age: 20, city: 杭州} register(**info) # 相当于 register(nameTom, age20, city杭州)字典解包有个要求字典里的键必须和函数形参名完全一致多一个都不行否则会报unexpected keyword argument。这个操作在配置文件、数据组装这类场景里非常常用——工具函数定义好了数据在字典里一个**就完成传递不用写一堆赋值语句。提示定义函数时的*args、**kwargs与调用函数时的*list、**dict一个叫收集packing一个叫展开unpacking方向完全相反但符号长得一样。学的时候一定要把定义端和调用端分开记。4. 参数顺序与混合使用一份直接能用的规则表4.1 Python对参数顺序的硬性规定当一个函数同时用上位置参数、默认参数、*args、**kwargs的时候声明顺序必须遵守规则否则解释器直接报语法错误SyntaxError。正确的顺序是def func(a, b, c10, *args, d20, e30, **kwargs): ...按顺序拆解一下a、b普通位置参数必传。c10默认参数可传可不传。*args收集多余的位置参数。d20、e30强制关键字参数也叫命名关键字参数只能通过关键字方式传。**kwargs收集多余的关键字参数。为什么顺序必须是这个因为Python解释器要保证匹配规则无歧义。想象一下如果你把默认参数写在*args后面但不在关键字区那它到底该从哪取所以语言设计者直接从语法层面锁死了这个顺序。实操时各位记住一句话就够了必填位置参数在前默认参数随后然后是*args接着是强制关键字参数最后是**kwargs。你写的函数超过四个参数类型时照这个顺序排不会错。4.2 强制关键字参数Python 3.0加入的防呆设计如果你想让调用方必须以形参名值的方式传入某个参数不允许靠位置来传该怎么办答案就是强制关键字参数keyword-only argument。它有几种写法def compare(a, b, *, ignore_caseFalse): ...注意这里的单独一个*。它的意思是*之后的所有参数都必须是关键字参数不能按位置传。所以compare(a, b, True)会报错必须写成compare(a, b, ignore_caseTrue)。也可以在*args后面定义强制关键字参数def print_items(*items, prefix, suffix): for item in items: print(prefix str(item) suffix) print_items(1, 2, 3, prefix[, suffix]) # [1] # [2] # [3]这里prefix和suffix就是强制关键字参数*items把1、2、3全收走了剩下的prefix、suffix只能用关键字传。强制关键字参数最大的价值是防呆。比如函数有两个布尔参数调用方如果按位置传True, False意义不明而且容易搞反把它们变成强制关键字参数之后调用方必须写sort_ascTrue, dedupFalse代码可读性立刻上来了。4.3 完整混合调用示例我们来看一个综合案例把上面所有规则串起来def create_job(name, retries3, *tags, timeout30, verboseFalse, **extra): print(任务名:, name) print(重试次数:, retries) print(标签:, tags) print(超时时间:, timeout) print(冗长模式:, verbose) print(额外配置:, extra) # 第一次调用基础用法 create_job(备份) # 第二次调用位置参数默认参数args关键字参数 create_job(同步, 5, daily, prod, timeout60, verboseTrue, ownerops)输出结果你自己跑一遍就能直观感受到同步给了name5给了retriesdaily和prod被收集进tagstimeout和verbose通过关键字给了强制关键字参数最后那个owner被**extra字典收走。函数签名设计得复杂时建议配套写清晰的使用注释说明每个参数的含义、类型、默认值。我见过太多复杂函数定义但完全不写说明的调用方只能靠猜这是非常危险的事。5. 传参背后的机制引用传递与默认参数的坑5.1 传进去的到底是值还是引用这个问题面试经常问实际写代码时理解不透也会出bug。Python官方的准确说法是传对象引用pass by object reference但更通俗的理解方式是变量名实际上是对对象的引用传参的本质是让函数内部的新变量名指向同一个对象。看这个经典例子def change_value(x): x 100 num 10 change_value(num) print(num) # 10没变化num还是10因为整数是不可变对象x 100只是让x指向了一个新的整数对象没有改变原来的num。再看这个def add_item(lst): lst.append(4) my_list [1, 2, 3] add_item(my_list) print(my_list) # [1, 2, 3, 4]变了列表是可变对象lst.append(4)直接修改了原对象本身所以函数外部也受影响。记住这个结论就行不可变对象int、str、tuple等在函数内重新赋值不影响外部可变对象list、dict、set等在函数内进行修改操作会直接影响外部。如果你不希望函数内修改外部对象可以在传入时主动复制一份比如add_item(my_list.copy())或者在函数内部用list()、dict()重新创建容器。5.2 默认参数的可变对象陷阱你踩过几次了这是Python初学者必踩的坑之一很多工作了两三年的人也偶尔翻车。先看这段代码def add_element(item, container[]): container.append(item) return container print(add_element(a)) # [a] print(add_element(b)) # [a, b] print(add_element(c)) # [a, b, c] 第二次调用时容器里居然已经有a了第三次成了[a, b, c]。原因前文说过默认参数在函数定义时只计算一次container始终引用定义时创建的那个空列表。每次调用不传container用的都是同一个列表于是越存越多。解决办法是公认的None哨兵模式def add_element(item, containerNone): if container is None: container [] container.append(item) return container这样每次调用不传container时都会创建一个全新的列表互不影响。规则可以总结成一句话默认参数永远不要用可变对象一律用None占位在函数内部再创建。5.3 可变对象传递时的注意事项处理可变对象参数时要有意识地考虑一个问题函数应该修改传入的对象还是应该修改自己的一份副本并返回新对象这两种设计思路结果完全不同。举个例子同样是给列表去重def deduplicate_inplace(mylist): seen set() index 0 while index len(mylist): if mylist[index] in seen: del mylist[index] else: seen.add(mylist[index]) index 1 def deduplicate_new(mylist): return list(set(mylist))第一个函数直接改了外部列表第二个函数返回一个新列表外部列表不动。哪种好看使用场景。团队协作时我建议在函数命名、文档注释里明确写明原地修改还是返回新对象否则调用方默认你返回新对象结果你改了原数据排查时找半天都找不到源头。6. 实战应用装饰器、参数转发与函数签名设计6.1 装饰器为什么必须要用 *args 和 **kwargs装饰器本质上是接收一个函数、返回一个新函数的函数。为了让你写的装饰器能应用到任何函数上内部包装函数必须把原函数所有可能的参数都接住再转发。这就是*args和**kwargs在装饰器里几乎必出现的原因import time import functools def timer(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) print(f函数 {func.__name__} 耗时 {time.time() - start:.4f} 秒) return result return wrapper timer def compute_something(x, y10): return x ** ytimer根本不知道compute_something具体有什么参数但通过*args收集所有位置参数、**kwargs收集所有关键字参数再原样转发给原函数就能做到无感知装饰。这就是前面说的参数的收集与展开是一体两面在装饰器里完美合体。这里还有个细节functools.wraps(func)是干吗的不写它装饰后的函数__name__会变成wrapper而且原始函数的文档字符串也会丢失导致调用方查不到原函数的参数说明。加上functools.wraps会把原函数的元信息复制到包装函数上同时保留__wrapped__属性方便追踪原函数。写装饰器不写 wraps基本可以算是事故现场。6.2 参数转发的真实场景函数封装的基石参数转发argument forwarding是指一个函数接收到的所有参数原封不动地传给另一个函数中间可能夹带一些额外处理。这种模式在封装第三方库、写中间层、写API网关时极其常见。比如你在团队里负责封装一个底层数据库操作库def safe_query(sql, *args, timeout5, **kwargs): print(f[日志] 执行SQL: {sql}) try: # 把args和kwargs转发给真正的查询函数 return db_engine.execute(sql, *args, timeouttimeout, **kwargs) except Exception as e: print(f[错误] 查询失败: {e}) raise调用方传什么safe_query就转发什么中间只多做了日志记录和异常处理。这种封装模式的好处是接口层稳定底层实现换了也不会影响上层调用方。很多开源框架的中间件机制底层就是靠这一套参数转发跑起来的。理解了参数转发你就看懂了为什么很多大型框架的代码密密麻麻都是*args, **kwargs在传递——不是开发者偷懒而是这种写法在框架层是刚需。6.3 设计函数签名时的三条建议签名设计这件事我自己的经验总结下来就三条第一参数数量超过4个时考虑用关键字参数或者强关键字参数来引导调用方。某知名Python库就明确要求所有参数一律传关键字包括bool值——就是为了可读性。第二默认参数只用于大多数调用方不需要关心的细节上。如果某个参数几乎所有调用都必须传就别给默认值否则会在代码里看到一大堆func(x, y, utf-8, False, True, None, 3)意义不明且极易传错。第三给函数写清晰的docstring把每个参数的语义、类型、默认行为写清楚。def foo(a, b1):这种裸声明在项目里等于没有接口文档。建议用Google风格的docstringdef fetch(url, timeout3, retries0): 抓取URL内容。 Args: url (str): 目标URL。 timeout (float): 超时秒数默认3秒。 retries (int): 失败后重试次数默认0次。 Returns: str: 页面内容。 这种习惯在写接口给别人用时功德无量。7. 常见报错与排查技巧实录7.1 这些报错你八成见过实际操作中传参相关的报错翻来覆去就那么几类。我把它们整理成一个表方便对照排查报错信息原因解决办法missing 1 required positional argument: b必传位置参数没传够检查是否需要补充实参takes 2 positional arguments but 3 were given传的位置参数比形参多移除多余实参或用*args接收多余参数got multiple values for argument a同一个参数既用位置传了又用关键字传了确认调用方式二选一unexpected keyword argument x传了函数定义中不存在的关键字参数检查参数名拼写或考虑用**kwargsSyntaxError: positional argument follows keyword argument关键字参数后面又写了位置参数调整顺序位置参数全部提前SyntaxError: non-default argument follows default argument默认参数后面又定义了无默认值的参数调整声明顺序把无默认值参数提前报错永远是最好用的学习材料。建议各位遇到报错先别急着复制粘贴到搜索框自己读一遍把报错里的函数名、参数名、第几行读出来很多问题光靠读就能定位到原因。7.2 快速调试传参的三个工具第一个是往函数开头打印所有参数。一个print(args, kwargs)治百病能看到函数实际收到了什么。第二个是利用Python内置的inspect模块import inspect print(inspect.signature(create_job)) # (name, retries3, *tags, timeout30, verboseFalse, **extra)inspect.signature()能输出函数的完整签名是研究他人代码和排查函数定义问题的利器。第三个是 IDE 的自动补全和悬停提示——多数现代IDE会把你函数定义里的参数名和默认值直接显示在调用处前提是你写了清晰准确的函数定义。用好这三个大多数传参问题不用进断点就能解决。7.3 我自己踩过的一个印象最深的坑有一次封装一个数据处理接口函数大概长这样def process(data, config{}): config.setdefault(step, default) ...起初觉得没什么后来在一次跑批任务里发现上一次调用的配置竟然残留在下一次调用里导致两次任务结果不一致排查了大半天。根因就是用可变对象做了默认参数。幸好当时还没上线正式环境但也让我长了个记性写函数时每看到一个[]、{}出现在形参位置条件反射就要改成None。从那以后我给自己定了一个规矩凡是我写的函数默认参数一律只用不可变对象int、str、float、bool、None、tuple凡是需要容器类型默认值的一律用None加内部初始化的模式。这个习惯帮我避掉了这个类别的所有后续问题。8. 传参方式背后的设计哲学与选择建议8.1 你的调用方是人还是程序传参方式的选择本质上是接口设计问题。如果你的函数是给别人写的业务代码调用的那优先保证可读性和防呆性——多用关键字参数和强制关键字参数让错误在调用处就暴露。如果你的函数是高层的框架接口参数需要被大量转发、包装那*args、**kwargs几乎是标配因为你需要兼容未来无法预知的参数。我用一个生活中的类比来讲点菜时服务员问你要什么菜、几份、是否加辣这是位置参数你把菜单翻到对应页签在每道菜后面勾选微辣、不加葱、打包这是关键字参数你跟服务员说剩下的菜你看着配、别放香菜这是**kwargs。你会发现好的沟通方式在现实和代码里逻辑是通的。8.2 一个复杂函数从定义到调用的完整复盘我把前面所有技术点汇总成一个最终例子从定义到调用完整过一遍大家可以直接拿这段代码做实验def build_report( title, author, templatedefault, *sections, output_formathtml, compressFalse, **metadata, ): print(f标题:{title}) print(f作者:{author}) print(f模板:{template}) print(f内容板块:{sections}) print(f导出格式:{output_format}) print(f是否压缩:{compress}) print(f元信息:{metadata}) # 合法的调用方式 build_report( Q3运营报告, Alice, report_v2, 趋势分析, 竞品动态, 用户留存, output_formatpdf, compressTrue, team增长组, date2025-09-30, )跑一遍这段代码再试着把output_format用位置方式传比如写在用户留存后面会立刻看到报错——这就是强制关键字参数在起保护作用。把错误试出来比看一百遍文档都有用。8.3 面试中关于传参的高频追问面试里这块的知识点经常被延伸我列出几个常见追问提前想明白总比现场卡壳好Python里函数参数是按值传还是按引用传怎么准确回答传对象引用。f(a, b, *args)、f(a, b, **kwargs)同时出现时匹配顺序怎样为什么顺序不可换。def f(a, b[]):这个函数连续调用两次结果是什么为什么。如何实现一个装饰器既支持无参数使用又支持带参数使用inspect.signature如何处理装饰器包装后的函数需要配合functools.wraps说明。其实这些问题在今天的文章里基本都有答案了。面试官考这个表面上是考语法本质上是考你写代码时有没有真正的接口意识——你在设计一个函数的时候是否想过它会被怎么用被多少种方式用。9. 我的实操体会与收尾建议函数传参这部分内容说难不难说简单里面的细节也确实多。我自己从一知半解到彻底理清靠的就是三件事反复写报错再对着报错改、读优秀开源代码时盯着函数签名看、以及不断被坑之后总结经验。传参不只是语法它决定了你的函数是好用还是难用。最后再分享一个小技巧每次写完一个对外使用的函数都站在调用方的角度重新读一遍函数签名问自己三个问题——这个参数必须传吗这个默认值合理吗调用方看到这个名字能猜到要传什么吗这三个问题的答案就是你的函数接口设计水平的真实反映。如果这篇文章对你有帮助最有效的巩固方式是把文中的每个例子都亲手敲一遍把每个报错都亲手试出来。试着给build_report增加一个新的参数组合看看会发生什么。这个过程跑完你的Python函数传参算是真正过关了。