ARTICLE DETAIL

资讯详情

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

Python函数参数*args和**kwargs详解:从打包解包到装饰器实战

Python函数参数*args和**kwargs详解:从打包解包到装饰器实战 1. 为什么每个Python开发者都绕不开args和kwargs刚接触Python函数定义那会儿我看到def func(*args, **kwargs)这种写法就头大。星号像两只眼睛瞪着我完全搞不懂为什么要这么写更不明白什么时候该用。后来写得多了踩的坑也多了才慢慢体会到这两个东西的设计有多精妙。这篇文章就是把我这些年对*args和**kwargs的理解完整梳理一遍。从最基础的概念到装饰器里的实战应用从参数打包解包的底层逻辑到容易翻车的细节都会讲到。不管你是刚学Python的新手还是已经写过一些项目但对其理解还不够透彻的开发者应该都能从中找到有用的东西。核心关键词就三个Python、args、kwargs。围绕它们我会把函数参数这个看似简单实则暗藏玄机的话题彻底讲清楚。先抛一个结论*args和**kwargs的本质是参数打包与解包机制它们让函数具备了处理任意数量参数的能力。这个能力在写通用工具函数、装饰器、框架基类的时候几乎是不可或缺的。不理解它们你写出来的代码要么不够灵活要么在阅读别人的源码时一头雾水。2. 参数打包与解包args和kwargs到底在做什么2.1 从函数参数的基本传递说起Python的函数参数传递有好几种形式先快速过一遍方便后面理解*args和**kwargs的定位。最普通的是位置参数def greet(name, greeting): return f{greeting}, {name}! greet(小明, 你好) # 输出你好, 小明!调用时按顺序传小明对应name你好对应greeting。这种写法最直观但问题是参数个数固定死了想传三个、四个就得改函数定义。然后是关键字参数greet(greeting你好, name小明) # 输出你好, 小明!用参数名指定顺序可以打乱。再进一步是默认参数def greet(name, greeting你好): return f{greeting}, {name}! greet(小明) # 输出你好, 小明! greet(小明, 早上好) # 输出早上好, 小明!这些都很常规。但当你遇到这样的需求——“写一个函数能接收任意数量的数字并求和”——固定参数就无能为力了。你可能会想到传一个列表进去def sum_all(numbers): return sum(numbers) sum_all([1, 2, 3, 4, 5]) # 输出15这能解决问题但调用的时候必须先把数据打包成列表。如果我想直接写sum_all(1, 2, 3, 4, 5)呢这时候*args就派上用场了。2.2 *args把多余的位置参数打包成元组*args的作用是在函数定义时把所有多余的位置参数收集到一个元组里。def sum_all(*args): print(fargs的类型{type(args)}) print(fargs的值{args}) return sum(args) result sum_all(1, 2, 3, 4, 5) # 输出 # args的类型class tuple # args的值(1, 2, 3, 4, 5) # result 15关键点args只是一个约定俗成的名字你完全可以叫它*numbers、*items、*whatever。真正起作用的是前面那个星号*。但为了代码可读性业界默认用argsarguments的缩写。再来看一个混合使用的例子def introduce(name, age, *hobbies): print(f姓名{name}) print(f年龄{age}) print(f爱好{hobbies}) introduce(小明, 25, 篮球, 编程, 旅行) # 输出 # 姓名小明 # 年龄25 # 爱好(篮球, 编程, 旅行)name和age按位置匹配剩下的篮球、编程、旅行全部被*hobbies打包成元组。这就是“多余位置参数的收集器”。注意*args收集的参数在函数内部是一个元组元组是不可变的。如果你需要修改这些参数得先转成列表。2.3 **kwargs把多余的关键字参数打包成字典**kwargs的作用类似但它收集的是关键字参数打包成一个字典。def print_info(**kwargs): print(fkwargs的类型{type(kwargs)}) for key, value in kwargs.items(): print(f{key}: {value}) print_info(name小明, age25, city北京) # 输出 # kwargs的类型class dict # name: 小明 # age: 25 # city: 北京同样kwargs也只是个约定名字keyword arguments的缩写核心是前面的**。混合使用位置参数、*args和**kwargsdef full_demo(a, b, *args, **kwargs): print(fa {a}) print(fb {b}) print(fargs {args}) print(fkwargs {kwargs}) full_demo(1, 2, 3, 4, 5, name小明, age25) # 输出 # a 1 # b 2 # args (3, 4, 5) # kwargs {name: 小明, age: 25}参数匹配的顺序是先匹配固定位置参数剩下的位置参数给*args所有关键字参数给**kwargs。2.4 解包调用函数时的反向操作前面讲的是定义函数时用*和**打包参数。反过来在调用函数时也可以用*和**来解包。def add(a, b, c): return a b c # 正常调用 add(1, 2, 3) # 输出6 # 用*解包列表 numbers [1, 2, 3] add(*numbers) # 输出6 # 用**解包字典 params {a: 1, b: 2, c: 3} add(**params) # 输出6这个特性在实际开发中非常实用。比如你有一个列表存了函数需要的所有参数直接func(*my_list)就能展开传入不用一个个手动写。再看一个组合场景def process(*args, **kwargs): print(f位置参数{args}) print(f关键字参数{kwargs}) list_data [1, 2, 3] dict_data {name: 小明, age: 25} process(*list_data, **dict_data) # 输出 # 位置参数(1, 2, 3) # 关键字参数{name: 小明, age: 25}这种写法在需要把参数从一个函数透传到另一个函数时特别常见。3. 实际开发中args和kwargs的高频使用场景3.1 装饰器args和kwargs最重要的战场装饰器是Python中*args和**kwargs最经典的应用场景没有之一。如果你看过任何装饰器的教程几乎都会看到这样的代码模板import functools def my_decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): print(f调用函数{func.__name__}) result func(*args, **kwargs) print(f函数返回{result}) return result return wrapper my_decorator def add(a, b): return a b add(3, 5) # 输出 # 调用函数add # 函数返回8为什么装饰器必须用*args和**kwargs因为装饰器不知道被装饰的函数需要什么参数。add需要两个参数但另一个被装饰的函数可能需要零个、三个或者关键字参数。wrapper函数用*args, **kwargs就能通吃所有情况把接收到的参数原封不动地传给原函数。如果不用*args和**kwargs你就得为每种参数组合写一个装饰器这显然不现实。再来看一个带参数的装饰器这是进阶用法import functools def repeat(times): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(times): result func(*args, **kwargs) return result return wrapper return decorator repeat(times3) def say_hello(name): print(f你好{name}) say_hello(小明) # 输出 # 你好小明 # 你好小明 # 你好小明这里有三层嵌套repeat接收装饰器参数decorator接收被装饰的函数wrapper接收函数调用时的实际参数。*args和**kwargs在最内层负责透传参数保证不管say_hello需要什么参数都能正确传递。实操心得写装饰器时一定要加functools.wraps(func)否则被装饰函数的元信息如__name__、__doc__会丢失调试时会很痛苦。这个坑我踩过不止一次。3.2 通用工具函数让接口更灵活写工具类或工具函数时*args和**kwargs能让接口设计更灵活。比如一个日志函数def log_message(level, *messages, **context): msg | .join(str(m) for m in messages) ctx , .join(f{k}{v} for k, v in context.items()) print(f[{level}] {msg} (f ({ctx}) if ctx else )) log_message(INFO, 用户登录, 操作成功, user_id1001, ip192.168.1.1) # 输出[INFO] 用户登录 | 操作成功 (user_id1001, ip192.168.1.1)这个函数可以接收任意数量的消息和任意数量的上下文键值对调用方不需要关心具体有几个参数。再比如一个批量数据库插入的封装def batch_insert(table_name, *records, **options): batch_size options.get(batch_size, 100) print(f向 {table_name} 插入 {len(records)} 条记录批次大小{batch_size}) # 实际插入逻辑... batch_insert(users, {name: 小明, age: 25}, {name: 小红, age: 23}, batch_size50 )这种设计在ORM框架、API客户端、配置管理工具中非常常见。3.3 继承与super()调用参数透传的典型场景在面向对象编程中子类调用父类方法时*args和**kwargs能帮你省去很多重复代码class BaseModel: def __init__(self, name, **kwargs): self.name name for key, value in kwargs.items(): setattr(self, key, value) class UserModel(BaseModel): def __init__(self, name, email, **kwargs): super().__init__(name, **kwargs) self.email email user UserModel(小明, xiaomingexample.com, age25, city北京) print(user.name) # 小明 print(user.email) # xiaomingexample.com print(user.age) # 25 print(user.city) # 北京UserModel把**kwargs透传给父类父类负责把额外的属性设置到实例上。这种模式在Django的模型、各种框架的基类中大量使用。好处是子类不需要知道父类接收哪些额外参数新增参数时也不用改子类的代码。3.4 函数参数转发包装API的利器当你需要包装一个已有函数在它前后加一些逻辑但又不想改变它的调用方式时*args和**kwargs是最佳选择import time def timing(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) elapsed time.time() - start print(f{func.__name__} 耗时{elapsed:.4f}秒) return result return wrapper timing def slow_calculation(n, multiplier1): total 0 for i in range(n): total i * multiplier return total slow_calculation(1000000, multiplier2) # 输出slow_calculation 耗时0.0832秒这种模式在性能分析、缓存、重试、权限校验等场景中反复出现。4. 参数顺序、类型注解与常见误区4.1 参数定义的顺序规则Python对函数参数的排列顺序有严格要求搞错了直接报语法错误。正确的顺序是普通位置参数默认参数*args可变位置参数关键字-only参数*后面的参数**kwargs可变关键字参数def correct_order(a, b10, *args, c, d20, **kwargs): print(fa{a}, b{b}, args{args}, c{c}, d{d}, kwargs{kwargs}) correct_order(1, 2, 3, 4, c5, e6) # 输出a1, b2, args(3, 4), c5, d20, kwargs{e: 6}注意c是关键字-only参数因为在它前面有一个*args所以调用时必须用c5的方式传不能按位置传。如果顺序写错了比如把**kwargs放在*args前面def wrong_order(**kwargs, *args): # SyntaxError pass直接语法报错。这个规则记住就好不需要死记硬背写多了自然就熟了。4.2 类型注解怎么写Python 3.5支持类型注解*args和**kwargs也可以标注类型from typing import Any def process(*args: int, **kwargs: str) - None: for arg in args: print(arg) for key, value in kwargs.items(): print(f{key}: {value})这里*args: int表示每个位置参数都是int类型**kwargs: str表示每个关键字参数的值都是str类型。注意注解的是每个参数的类型不是args元组或kwargs字典本身的类型。更精确的写法可以用UnpackPython 3.11from typing import Unpack, TypedDict class Options(TypedDict): name: str age: int def func(**kwargs: Unpack[Options]) - None: ...不过日常开发中简单的注解就够用了不必过度设计。4.3 几个容易踩的坑坑一修改args或kwargsargs是元组不可变不能直接修改。kwargs是字典可以修改但修改后会影响函数内部后续的使用def modify_kwargs(**kwargs): kwargs[new_key] new_value # 可以修改 print(kwargs) modify_kwargs(a1) # 输出{a: 1, new_key: new_value}坑二位置参数和关键字参数冲突def func(a, b): return a b func(1, a2) # TypeError: got multiple values for argument aa既按位置传了又按关键字传了Python不知道该用哪个直接报错。坑三kwargs中的键必须是字符串def func(**kwargs): pass func(**{1: a}) # TypeError: keywords must be strings字典的键必须是字符串否则解包时会报错。坑四忘记解包def add(a, b): return a b params [1, 2] add(params) # TypeError: missing 1 required positional argument: b add(*params) # 正确3传列表时忘了加*Python会把整个列表当成一个位置参数传给a导致b没有值。实操心得遇到TypeError: missing required positional argument或者got multiple values这类错误时第一反应就是检查*和**有没有写对、有没有漏写。5. 进阶技巧与性能考量5.1 用*和**做参数合并Python 3.5支持在函数调用时多次解包def func(a, b, c, d): print(a, b, c, d) list1 [1, 2] list2 [3, 4] func(*list1, *list2) # 输出1 2 3 4 dict1 {a: 1, b: 2} dict2 {c: 3, d: 4} func(**dict1, **dict2) # 输出1 2 3 4如果字典有重复的键后面的会覆盖前面的d1 {a: 1, b: 2} d2 {b: 99, c: 3} func(**d1, **d2) # b99后面的覆盖前面的这个特性在合并配置字典时很有用。5.2 性能影响别过度使用*args和**kwargs虽然灵活但会带来轻微的性能开销。每次调用都需要打包和解包比直接传固定参数慢一些。在绝大多数应用场景中这点开销可以忽略不计。但如果你在写高性能计算库或者被频繁调用的底层函数就需要权衡了。我做过一个简单的测试import timeit def fixed(a, b, c): return a b c def flexible(*args): return sum(args) # 测试固定参数 t1 timeit.timeit(lambda: fixed(1, 2, 3), number1000000) # 测试可变参数 t2 timeit.timeit(lambda: flexible(1, 2, 3), number1000000) print(f固定参数{t1:.4f}秒) print(f可变参数{t2:.4f}秒)实测下来可变参数大约慢10%-20%。对于百万次调用差距在零点几秒级别。日常业务代码完全不用在意但底层库开发者需要心里有数。5.3 用inspect模块检查参数有时候你需要在运行时检查一个函数接收了哪些参数inspect模块可以帮忙import inspect def example(a, b10, *args, **kwargs): pass sig inspect.signature(example) for name, param in sig.parameters.items(): print(f{name}: {param.kind}, 默认值{param.default}) # 输出 # a: POSITIONAL_OR_KEYWORD, 默认值class inspect._empty # b: POSITIONAL_OR_KEYWORD, 默认值10 # args: VAR_POSITIONAL, 默认值class inspect._empty # kwargs: VAR_KEYWORD, 默认值class inspect._empty这个技巧在写框架、做参数校验、生成API文档时很有用。比如FastAPI就是通过分析函数的参数签名来自动生成请求参数校验逻辑的。5.4 常见问题速查表问题现象可能原因解决方法TypeError: missing required positional argument调用时参数个数不够或忘记解包检查参数个数确认是否需要加*TypeError: got multiple values for argument同一参数既按位置传又按关键字传统一用一种方式传参TypeError: keywords must be strings**解包的字典键不是字符串确保字典键都是字符串SyntaxError: invalid syntax参数顺序写错按“位置→默认→*args→关键字-only→**kwargs”排列装饰器丢失函数元信息没加functools.wraps在wrapper上加functools.wraps(func)args修改后不生效args是元组不可变先转成列表再修改6. 从源码角度看args和kwargs的设计哲学6.1 为什么是元组和字典*args打包成元组而不是列表**kwargs打包成字典而不是其他映射类型这不是随意选择的。元组是不可变的意味着函数内部不会意外修改传入的位置参数。这符合“参数是输入不应该被修改”的设计原则。字典则天然适合键值对结构而且Python的字典在3.7版本中保证了插入顺序所以**kwargs中的参数顺序和调用时传入的顺序一致。从CPython的实现角度看函数调用时解释器会把多余的位置参数收集到一个元组对象中把多余的关键字参数收集到一个字典对象中。这个过程在字节码层面有专门的操作码CALL_FUNCTION_EX等来处理。6.2 与其他语言的对比很多语言都有类似的可变参数机制。比如C语言的printf用...表示可变参数但需要va_list手动遍历类型不安全。Java的String... args本质是数组只能处理位置参数没有关键字参数的概念。JavaScript的...args是ES6引入的剩余参数功能类似Python的*args但没有**kwargs的对应物对象解构可以模拟一部分。Kotlin的vararg也是数组同样没有关键字参数打包。Python同时支持*args和**kwargs而且两者可以混合使用这在主流语言中是比较少见的。这种设计让Python的函数接口极其灵活也是很多框架能做到“约定优于配置”的底层支撑。6.3 在框架设计中的应用如果你去看Django、Flask、FastAPI这些框架的源码会发现*args和**kwargs无处不在。以Django的视图函数为例def view(request, *args, **kwargs): # kwargs里可能包含URL捕获的命名组参数 ...Django的URL路由系统会把URL中捕获的参数以位置或关键字的形式传给视图函数视图函数用*args和**kwargs来接收不需要提前知道URL里定义了多少个参数。再比如FastAPI的依赖注入from fastapi import FastAPI, Depends app FastAPI() def get_db(): db database_connection try: yield db finally: db.close() app.get(/items/) def read_items(dbDepends(get_db), **kwargs): return {db: db}框架通过分析函数签名来决定如何注入依赖**kwargs在这里充当了“兜底”的角色接收框架可能传入的额外参数。理解了*args和**kwargs你在阅读这些框架源码时就不会被参数传递绕晕也能更自如地写自己的装饰器和工具函数。7. 几个实战案例的完整拆解7.1 写一个通用的重试装饰器import functools import time def retry(max_attempts3, delay1, exceptions(Exception,)): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts 1): try: return func(*args, **kwargs) except exceptions as e: if attempt max_attempts: raise print(f第{attempt}次失败{e}{delay}秒后重试...) time.sleep(delay) return wrapper return decorator retry(max_attempts3, delay0.5) def unstable_network_call(url, timeout10): import random if random.random() 0.7: raise ConnectionError(网络超时) return f成功获取{url} result unstable_network_call(https://example.com/api, timeout5) print(result)这个装饰器完整展示了*args和**kwargs的透传能力。unstable_network_call需要什么参数wrapper就原样传递什么参数装饰器本身不需要关心。7.2 写一个参数校验工具def validate_types(**expected_types): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): import inspect sig inspect.signature(func) bound sig.bind(*args, **kwargs) bound.apply_defaults() for param_name, expected_type in expected_types.items(): if param_name in bound.arguments: value bound.arguments[param_name] if not isinstance(value, expected_type): raise TypeError( f参数 {param_name} 期望 {expected_type.__name__} f实际得到 {type(value).__name__} ) return func(*args, **kwargs) return wrapper return decorator validate_types(namestr, ageint) def create_user(name, age): return {name: name, age: age} create_user(小明, 25) # 正常 create_user(小明, 25) # TypeError这个例子用到了inspect.signature和bind方法把*args和**kwargs绑定到具体的参数名上然后逐个校验类型。这种模式在需要做参数校验的API层非常实用。7.3 写一个支持任意参数的缓存装饰器import functools import hashlib import json def memoize(func): cache {} functools.wraps(func) def wrapper(*args, **kwargs): # 把参数序列化成可哈希的键 key_data json.dumps( {args: args, kwargs: kwargs}, sort_keysTrue, defaultstr ) key hashlib.md5(key_data.encode()).hexdigest() if key not in cache: cache[key] func(*args, **kwargs) return cache[key] wrapper.cache cache return wrapper memoize def expensive_query(user_id, filtersNone, **options): print(f执行查询user_id{user_id}, filters{filters}, options{options}) return f结果_{user_id} expensive_query(1, filters{status: active}, limit10) expensive_query(1, filters{status: active}, limit10) # 第二次不会打印查询日志这个缓存装饰器用*args和**kwargs接收任意参数然后序列化成哈希键。注意defaultstr是为了处理不可序列化的参数类型sort_keysTrue保证字典键的顺序不影响哈希结果。实操心得写缓存装饰器时参数序列化是个容易出问题的地方。如果参数包含不可哈希的对象比如自定义类的实例需要额外处理。我一般会加一个defaultstr兜底但更严谨的做法是要求被缓存的函数参数都是可序列化的基本类型。8. 我个人的一些经验总结写了这么多年Python关于*args和**kwargs有几个体会特别深。第一不要为了炫技而滥用。有些函数明明只需要固定参数非要写成*args, **kwargs结果调用者完全不知道要传什么IDE的自动补全也失效了。灵活性是有代价的可读性和可维护性同样重要。我的原则是只有当函数确实需要处理不确定数量的参数时才用它们。第二命名要清晰。虽然args和kwargs是约定俗成的名字但在某些场景下用更有意义的名字能提升代码可读性。比如*numbers、*items、**options、**config一看就知道是什么。第三文档字符串要写清楚。用了*args和**kwargs的函数光看签名是不知道具体支持哪些参数的。这时候docstring就特别重要要把支持的参数、类型、默认值都写明白。第四类型注解能加就加。Python 3.5的类型注解虽然不影响运行时但配合mypy等工具能在开发阶段发现很多问题。对于*args和**kwargs至少标注一下基本类型比如*args: int、**kwargs: str。第五调试时善用print和inspect。当参数传递出问题时在函数入口打印args和kwargs是最快的排查方式。inspect.signature则能在运行时查看函数的参数签名对于理解框架的行为很有帮助。最后说一个我踩过的大坑有一次写装饰器忘了加functools.wraps结果被装饰函数的__name__变成了wrapper导致日志里所有函数名都一样排查了半天才发现问题。从那以后我写装饰器的第一件事就是加上functools.wraps。*args和**kwargs看起来简单但真正用好需要理解它们背后的设计逻辑和适用场景。希望这篇内容能帮你把这部分知识彻底理清楚下次在装饰器、框架源码或者自己的工具函数里再看到它们时能一眼看穿参数是怎么传递的。
返回列表