资讯详情

资讯详情

Python 星号参数详解:*args与**kwargs的收集、解包与签名设计

1. 先把概念摆正星号在定义处和调用处是两件事写 Python 写到一定年头会碰到一种很微妙的情况同一个星号出现在函数定义里是一个意思出现在函数调用里又是另一个意思。def f(*args)里的星号是“收”把散装参数收进一个元组f(*some_list)里的星号是“放”把容器拆成散装参数喂进去。很多人第一次被TypeError: f() got multiple values for argument a打脸基本都是这两件事在脑子里混成了一件事。我把这个话题拆成两层来看会更清楚定义端解决的是“我的函数能接住多少种调用方式”调用端解决的是“我手里这个容器怎么摊开交给别人”。前者关乎函数签名的设计后者关乎调用现场的数据形态。标题里说的“*和**做参数”其实覆盖了这两层的全部组合一共四种写法定义端的*args、定义端的**kwargs、调用端的*iterable、调用端的**mapping。这份东西适合谁看刚过完语法阶段、写过几个函数但一遇到装饰器源码就发懵的人需要给团队写公共工具库、被“加一个参数就改十个调用点”折磨过的人以及想把函数签名设计得既宽松又不失控的人。我尽量不写教科书式的语法罗列而是把每一步背后的“为什么这么设计”“不这么写会炸在哪”讲透代码片段都能直接粘到解释器里跑。1.1 定义端星号是“收拢”不是“展开”先看最基础的一层。*args出现在形参列表里语义是“从这个位置开始剩下的位置参数全部打包成一个元组”。**kwargs出现在形参列表里语义是“剩下所有没被显式形参匹配到的关键字参数全部打包成一个字典”。注意两个关键词剩下和打包后是新的对象。def collect(first, *args, **kwargs): print(first , first) print(args , args, type(args).__name__) print(kwargs, kwargs, type(kwargs).__name__) collect(1, 2, 3, namedemo, level2)跑出来是args (2, 3) class tuple、kwargs {name: demo, level: 2} class dict。这里有个容易被忽略的点args一定是元组不是列表所以它不可变你在函数里args.append(...)会直接报错kwargs一定是一个新建的字典不是调用方那个字典的引用往里面写东西不会污染调用方。我在早期写配置合并逻辑时就是想当然地以为传进来的字典是引用结果在函数里改kwargs.pop(...)之后发现外部变量没变还以为是解释器抽风。另一个细节是“剩下”这个词的含义。collect(1, 2, 3, namedemo)里1被first吃掉2、3进入argsname走了关键字通道进入kwargs。但如果写成collect(1, 2, namedemo, 3)那是语法错误位置参数不能跟在关键字参数后面——这跟星号没关系是 Python 调用语法本身的规则*并不会让你绕过它。1.2 调用端星号是“摊开”且只摊一层调用端的星号完全是另一回事。f(*seq)表示把seq迭代出的每个元素依次当作独立的位置参数传进去f(**mapping)表示把mapping的每个键值对当作“键值”形式的关键字参数传进去。用生活化的说法定义端是在收货时把散件装箱调用端是在发货时把整箱拆成散件。def add3(a, b, c): return a b c nums [1, 2, 3] print(add3(*nums)) # 等价于 add3(1, 2, 3) pairs {a: 1, b: 2, c: 3} print(add3(**pairs)) # 等价于 add3(a1, b2, c3)关键限制是只摊一层。如果nums [[1, 2], [3]]add3(*nums)传进去的是两个列表不会继续往下摊报错是必然的。想要深摊得自己写递归但生产代码里我基本不建议这么干——深层的结构差异太容易埋雷宁可显式写清楚add3(*nums[0], nums[1][0])。还有一个高频误用f(*abc)会摊成f(a, b, c)因为字符串也是可迭代对象。我见过有人想传一个字符串参数手滑多写了个星号结果函数收到三个单字符参数报错信息还特别难看懂。同理f(*{a: 1})摊的是键而不是键值对得到的是f(a)。这类错误用静态检查工具也不一定能拦住只能靠对语义的清醒认知。1.3 四种写法对照表先建立全局印象把四种组合放在一张表里后面每一节都是这张表的展开。我在给团队做内部分享时第一页永远是这张表因为它能一次性消掉大部分混淆。出现位置写法语义结果类型典型用途函数定义*args收集多余位置参数tuple不定长参数、透传函数定义**kwargs收集多余关键字参数dict可选配置、透传函数调用*iterable把可迭代对象摊成位置参数无展开动作复用已有序列函数调用**mapping把映射摊成关键字参数无展开动作复用已有配置字典再补一条容易被忽略的定义端的*和**是声明式的写死了函数能接收的参数形态调用端的*和**是动作运行时才发生展开。前者影响的是函数签名后者影响的是单次调用的实参列表。理解了这个区别看装饰器源码里def wrapper(*args, **kwargs)就不会再觉得是在变魔术了——它只是在说“我不管你来什么参数我都原样收着”。2. 参数收集的实操细节顺序、命名与默认值语法层面知道怎么写只是起点真正让人栽跟头的是顺序规则和默认值语义。这一节讲的全是我在实际项目里被绊过的地方尤其是参数顺序Python 的规则比大多数人以为的要严格得多。2.1 参数顺序不是随便排的排错了直接语法错误一个合法的函数签名顺序必须严格遵循位置参数 → 默认参数 →*args→ 仅关键字参数 →**kwargs。任何打乱都会在定义时就抛SyntaxError注意是定义时就报不用等到调用。# 合法 def f1(a, b1, *args, c, d2, **kwargs): return a, b, args, c, d, kwargs # 非法默认参数跑到 *args 后面去了不是 c 没有默认值但后面又跟了带默认值的 d —— 这是合法的 # 真正非法的长这样 # def f2(a, *args, b1, c): # SyntaxError: non-default argument follows default argument上面那段注释里说的坑值得展开。def f(a, *args, b1, c)会报错因为c是仅关键字参数且没有默认值而它前面已经出现了带默认值的b。规则是“仅关键字参数区域里非默认参数也不能跟在默认参数后面”。解决办法只有两个把c改成有默认值或者把c挪到b前面。我自己习惯是把必填的仅关键字参数全部排在前面可选配置全部放后面这样签名一眼就能看出哪些是调用方必须给的。顺带说一个常见误解def f(a, b1, *args)里的b是可以被调用方跳过的写f(1, 2, 3, 4)时b2args(3,4)。但没法写f(1, *args(3,4))然后让b保持默认——位置参数是按顺序填的b总要占一个坑。想要“只给a和args不给b”那b就必须设计成仅关键字参数这是签名设计上的取舍下面第 3 节会细说。2.2args/kwargs只是约定但别乱改名*args和**kwargs里的名字本身没有魔力def f(*items)和def f(*args)在语义上完全等价星号才是关键。但强烈建议守住这个约定原因有两个一是几乎所有文档、教程、第三方库源码都用这套命名团队协作时统一命名能省掉大量认知成本二是像inspect.signature、一些序列化框架、接口文档生成工具会一定程度上依赖这个约定来做展示或推导。真需要改成有意义的名字时我有两个判断标准。第一这个函数是内部私有的且参数含义非常特定比如def build_sql(*where_clauses)读起来确实比*args清楚。第二不会有人靠**kwargs往这个函数里塞意料之外的配置——一旦函数对外暴露、且接收**kwargs当“万能配置通道”名字就必须叫kwargs因为你无法列举它到底包含哪些键用一个泛化的名字反而更诚实。反过来**kwargs最危险的地方恰恰是“万能配置通道”这个用法。它让你的函数签名对外完全开放调用方拼错一个键名不会报错值会静默躺在kwargs里没人消费。我踩过的真实坑是配置键从timeout改成time_out函数签名没变因为都在**kwargs里测试也没覆盖到该分支线上跑了三天才发现超时配置根本没生效。所以我的经验是**kwargs只用在两种场景透传给下层函数、或者确实需要动态键比如根据外部数据生成参数名。业务函数的配置项宁可一个个显式写出来。2.3 参数解包的实操列表、元组、字典、生成器调用端解包对容器的要求很宽松*后面跟任何可迭代对象都行列表、元组、集合、生成器、字符串、字典的键视图都可以。**后面必须是映射mapping也就是有keys()和__getitem__的对象最常见的就是字典。def report(title, count, levelinfo): print(f[{level}] {title}: {count}) report(*(启动完成, 42)) # 元组 report(*(x for x in [启动完成, 42])) # 生成器 report(**{title: 启动完成, count: 42, level: warn})生成器解包有个隐含代价f(*gen)会先把生成器完全消费成一个元组再传参意味着内存占用和生成器的“惰性”优势在这里全部消失。如果生成器产出十万条数据f(*gen)会一次性构造一个十万元素的元组还可能直接撞上参数个数的上限。这种场景下正确做法是让函数接收可迭代对象本身而不是靠星号摊开——def f(items: Iterable)而不是def f(*items)。字典解包还有一个硬性约束键必须是字符串。f(**{1: a})会抛TypeError: keywords must be strings。这个错误在配置合并场景里特别隐蔽因为你往往是从 JSON、YAML 或某个下游接口拿到的字典绝大多数时候键是字符串偶尔混进一个整数键就炸了。我的习惯是用f(**{str(k): v for k, v in raw.items()})这种显式转换兜底虽然丑但比线上崩一次强。2.4 解包时的覆盖与冲突规则多个解包同时出现在一次调用里规则是 Python 3.5 之后才稳定下来的位置参数解包可以写多个f(*a, *b, *c)合法关键字解包也可以写多个f(**d1, **d2)合法。后者带来的一个实际影响是后面的字典会覆盖前面的同名键这跟字典合并的语义一致。base {level: info, count: 1} override {level: debug} report(**base, **override) # level 取 debugcount 取 1这个特性在做“默认配置 用户覆盖”时非常好用比{**base, **override}合并后再传更直观。但它有个边界如果同名参数同时也用显式关键字给了一次就会冲突。report(titlex, **{title: y, count: 1})直接抛TypeError: report() got multiple values for keyword argument title。注意这跟“后者覆盖前者”不是一回事——显式关键字和**解包之间的重复是错误而两个**之间的重复是覆盖。这个不对称性我当初着实困惑了一阵后来想明白了显式写出的关键字参数是程序员明确表达的意图和**里动态冒出来的同名键撞车解释器没法判断你想保留哪个只能报错让你自己决定。还有一种冲突是位置和关键字撞车report(标题, title另一个)同样报multiple values。这类错误信息其实写得挺清楚指明了是哪个参数名冲突排查时顺着参数名往回找调用点就行。3. 用星号把函数签名设计得更抗变化前面讲的是“怎么用”这一节讲“什么时候该用、什么时候不该用”。函数签名的设计是接口设计的核心星号用得好能让接口在演进中保持兼容用不好就是一个能吞掉所有错误的全开放黑洞。3.1 仅位置参数与仅关键字参数给参数上锁Python 3.8 引入了/来标记仅位置参数配合*标记仅关键字参数一个签名可以做到“两头锁死”。这套机制的价值在于把参数该怎么传这件事写进契约里改名字不会破坏兼容性改顺序也不会静默出错。def deposit(account, /, amount, *, currencyCNY, note): ... deposit(A001, 100) # OK deposit(A001, 100, currencyUSD) # OK deposit(accountA001, amount100) # TypeErroraccount 是仅位置参数 deposit(A001, 100, USD) # TypeErrorcurrency 是仅关键字参数为什么值得这么写/之前的参数调用方不能用关键字传也不能被**kwargs里的同名键“意外”覆盖函数内部可以放心地改名而不影响调用方因为没人能按名字传。*之后的参数调用方必须写明参数名def f(a, *, b)这种签名对布尔开关尤其友好——f(1, True)读起来毫无意义f(1, strictTrue)就清楚多了。我现在的习惯是凡是布尔型或枚举型、含义不直观的参数一律用*强制关键字传。有个实操层面的坑得提一下。*单独使用后面不跟名字表示“后面的都是仅关键字参数”这跟*args是两回事两者不能同时出现在同一个声明位置def good(a, *args, b): # 合法args 收集位置参数b 仅关键字 def also_good(a, *, b): # 合法不要 args只要一个分隔 def bad(a, *args, *, b): # SyntaxError* 已经用过了def bad这行报的是SyntaxError: / must be ahead of *这个错误提示有点误导人我第一次见时以为是/的位置问题其实本质就是不允许两个*分隔符。真需要“既收多余位置参数又强制关键字”用def good(a, *args, b)就够了b自动成为仅关键字参数。3.2 透传场景包装层和装饰器为什么必须用*args, **kwargs装饰器是*args, **kwargs最正宗的使用场景。包装函数对被包装函数的签名一无所知唯一正确的做法就是“原样收、原样转”。import functools def timing(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() try: return func(*args, **kwargs) finally: cost (time.perf_counter() - start) * 1000 print(f{func.__name__} 耗时 {cost:.2f} ms) return wrapper这里有几个细节值得逐一说明。functools.wraps必须加否则被装饰函数的__name__、__doc__、__module__、__wrapped__全都会变成wrapper的日志、文档生成、调试器堆栈全都会误导你。*args, **kwargs在wrapper里既是收集又是透传同一个符号在两行代码里扮演了两个角色第一行是定义端收拢第二行是调用端摊开这是很多人读装饰器源码卡住的第一个点。第二个细节如果被装饰的函数签名里有仅关键字参数wrapper(*args, **kwargs)依然能正确透传因为在wrapper内部kwargs是一个字典func(**kwargs)会把这些键原样当作关键字参数传进去。位置参数那边同理。也就是说装饰器这一层是签名无关的什么都不用改。但代价是类型检查器在这一层就断链了——它看到的是(*args, **kwargs)没法知道你实际传了什么这个问题的解法在下一节。第三个细节return func(*args, **kwargs)如果被包在try里return的求值发生在try块内函数返回值会被正确传出finally里不能有return否则会吞掉返回值这是 Python 的try/finally语义跟星号无关但经常一起踩。我习惯是只包计时逻辑返回值单独用一个变量接住逻辑更清晰。3.3 继承与super()里的透传别写死参数子类重写父类方法时如果父类签名将来可能变用*args, **kwargs透传是保住兼容性的稳妥做法但也不是无脑用。核心判断是子类是否真的对参数无感。class Base: def handle(self, payload, *, retry0): ... class Child(Base): def handle(self, *args, **kwargs): kwargs.setdefault(retry, 1) return super().handle(*args, **kwargs)这段代码干了三件事原样收、给retry补一个默认值、原样转给父类。setdefault是个好用的技巧它只在键不存在时才写入调用方显式传了retry3就尊重调用方。但这里有个隐患kwargs里如果混进了拼错的键比如retiressetdefault不会报错父类那边如果也用**kwargs收着这个错键会一路静默传下去。所以这种写法只适合你确认父类签名稳定、且调用方来自受控内部的场景。另一种更安全的写法是把签名显式重写一遍让参数名在子类这里就固定下来class Child(Base): def handle(self, payload, *, retry1, **extra): return super().handle(payload, retryretry, **extra)这样拼错的键会落到extra里虽然还是不会报错但至少你能在子类里加一行if extra: raise TypeError(...)主动拦截。我在公共基类上吃过一次亏某个子类重写时写成了def handle(self, *args)漏掉了**kwargs导致所有关键字参数在子类这一层就被吞掉父类收到的是空参数用默认值跑了一整周才被发现。这种 bug 静态检查工具基本拦不住只能靠代码评审和显式的签名重写来防。3.4 用**kwargs做配置合并的边界在哪“把一堆可选配置塞进**kwargs”是很多人天然的直觉但边界得划清楚。适合放进**kwargs的是键集合由外部数据决定、函数本身不解释键名的场景。典型例子是 HTTP 请求库的request(**options)或者某个中间件把上下文原样传给下游。不适合的是键集合固定、只是当前实现懒得列全。判断标准很简单问自己一句如果调用方传了一个我这层不认识的键应该发生什么答案应该是“报错”——那就别用**kwargs收显式列出来让解释器替你报错。答案应该是“透传给下游”——那就用且一定要把**kwargs原样往下传别在中途pop掉之后忘了处理。还有一个实际约束**kwargs配合typing时表达能力很弱**kwargs: Any基本等于放弃类型检查。用TypedDict加Unpack能缓解但这个方案在跨版本兼容上有额外成本第三部分会具体讲。4. 类型注解与静态检查下的星号写法类型注解加进来之后星号相关的写法从“语法问题”变成了“工具链问题”。这一块我踩过的坑相对密集因为很多报错不是运行时错误而是类型检查器在你没预期的地方亮红。4.1 给*args/**kwargs标类型标的是“元素类型”最容易搞错的一点*args: int表示“每个位置参数都是int”而不是“args是int”。args的运行时类型永远是tuple注解标的是元组里元素的类型。同理**kwargs: str表示每个值都是字符串。def join_all(sep: str, *parts: str) - str: return sep.join(parts) def configure(**options: int) - None: ...configure(a1, b2)通过configure(ax)会被类型检查器标红。但注意这是静态检查层面的约束运行时 Python 完全不检查你强行传字符串一样跑得通只是后面的逻辑可能崩。所以注解这东西在团队协作里的价值主要是把“参数该传什么”从口头约定变成可校验的契约。**kwargs的类型标注有个陷阱键的类型默认永远是str你没法用注解限制“只允许这三个键”。想要限制键集合就只能放弃**kwargs改用显式关键字参数或者上TypedDict。4.2Unpack与ParamSpec对装饰器签名做保真Python 3.10 引入的ParamSpec解决了长期以来的一个痛点装饰器包装函数后类型信息丢失。用ParamSpec可以把原函数的参数签名“搬运”到包装函数上。from typing import Callable, ParamSpec, TypeVar P ParamSpec(P) R TypeVar(R) def timing(func: Callable[P, R]) - Callable[P, R]: functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) - R: ... return func(*args, **kwargs) return wrapper这段代码的好处是被装饰函数的签名对调用方完全透明参数名、参数类型、仅关键字约束全部保留IDE 的自动补全和类型检查都能正常工作。注意事项是P.args和P.kwargs只能成对出现在同一个函数的签名里且必须分别用于*args和**kwargs位置写错位置类型检查器会直接报错。另一个实用工具是Unpack3.11 起在typing里3.12 起可以直接用内置的**语法用来表达“**kwargs的键集合由某个TypedDict决定”from typing import TypedDict, Unpack class Options(TypedDict, totalFalse): retry: int timeout: float def request(url: str, **opts: Unpack[Options]) - None: ...这样request(x, retry3)能通过检查request(x, retries3)会被标红拼写错误的键终于能在静态阶段被拦住。我这边的实际做法是新项目直接用 3.11 的写法老项目为了兼容低版本会用TypedDict加# type: ignore折中或者干脆不标靠代码评审兜底。还有一个跨版本兼容的实践提醒ParamSpec和Unpack都是较新特性如果你的库要支持多个 Python 版本需要靠typing_extensions做兼容导入并在 CI 里跑多版本的类型检查。我见过不少项目在本地检查全绿到 CI 上换了版本就一片红原因是typing_extensions版本没锁底层行为不一致。5. 常见问题与排查技巧实录这一节是踩坑集合。下面这些报错我敢说每个写了半年以上 Python 的人都至少见过一半。5.1 高频报错对照表按错误信息反向定位报错信息触发条件定位思路解决方向got multiple values for argument x同一参数被位置和关键字各传一次或显式关键字与**解包撞名检查调用处是否对x既按位置又按名字传删掉其中一处或改用不同参数名keywords must be strings**解包的字典里有非字符串键打印字典所有键的类型解包前把键统一转成字符串takes 2 positional arguments but 3 were given位置参数多传了数一下实参个数注意*摊开后的实际个数检查解包的容器长度missing 1 required keyword-only argument: x*之后的必填参数没给看签名里*后面哪些没默认值调用时补上或给默认值argument after * must be an iterablef(*None)或f(*5)检查星号后面是不是空值加or []兜底non-default argument follows default argument定义时参数顺序违规看仅关键字参数区是否默认值混排必填的前置可选的置后我把这张表贴在自己的笔记首页快两年了实际命中率比想象的高。尤其是第一条多数时候不是你不懂规则而是调用链太长某个中间层把参数名字传重了报错信息却只指向最外层。5.2 可变默认参数跟星号无关但总在同一个函数里出事这条严格来说不属于星号语义但只要有*args或**kwargs的函数十有八九同时存在默认值参数所以放在一起说。经典陷阱def append_item(item, bucket[]): # 危险 bucket.append(item) return bucket print(append_item(1)) # [1] print(append_item(2)) # [1, 2] ← 不是 [2]原因在于默认值是在函数定义时求值一次的列表对象被所有调用共享。正确写法是用None做哨兵def append_item(item, bucketNone): if bucket is None: bucket [] bucket.append(item) return bucket为什么不用bucket()之类的不可变默认值因为那样函数内部就得转换一次多一次拷贝且如果调用方传了元组进来行为会不一致。用None哨兵是最不容易出错的方式。同样的坑也会出现在**kwargs里塞可变对象def f(**kwargs)内部kwargs本身每次调用都是新的但如果你把某个默认值写成了def f(config{})那就是同一个坑。我个人的纪律是函数签名里出现[]、{}、set()这类字面量当默认值一律视为需要复查的信号。代码评审时看到就直接打回去几乎没有误伤。5.3 排查思路从签名开始而不是从调用开始遇到星号相关的报错我的固定排查顺序是先用inspect.signature把真实签名打印出来再打印实际传入的args和kwargs最后才对调用点。原因很简单星号会把参数的形态彻底改变你脑子里以为的签名和运行时的签名经常不是一回事尤其是装饰器叠加了几层之后。import inspect def debug_call(func, *args, **kwargs): print(签名:, inspect.signature(func)) print(位置参数:, args) print(关键字参数:, kwargs) return func(*args, **kwargs)调试期间在关键函数外面套一层这个能省掉大量猜的时间。inspect.signature还有一个好处是它会顺着functools.wraps找到被包装函数的原始签名即使你看到的是wrapper(*args, **kwargs)打印出来也是原函数的签名。另一个排查技巧是把**kwargs的内容按类型分组打印。我在配置合并的场景里遇到过一个问题某个键的值偶尔是None偶尔是字符串因为上游有不同的数据源其中一路没查到就塞了None。直接 print 整个字典看着都正常分组之后才一眼看出来。6. 性能与可读性的取舍说几句真心话*args/**kwargs不是没有代价的。每一次调用都要构造元组或字典尤其**kwargs每次调用都会新建一个dict在热路径上这是实打实的开销。我做过一个粗略的对比一个只做值透传的函数用固定签名和用**kwargs接收在百万次调用的量级下后者会慢出可感知的差距具体倍数取决于 Python 版本和参数个数我这里不给具体数字因为它在不同环境下波动很大你在自己的场景里跑timeit比看任何人的结论都准。但我想强调的是绝大多数业务代码里这点开销根本不值得优化。为了省那点开销把**kwargs改成固定签名代价是接口失去弹性、新增参数要改所有调用点收益和成本完全不成比例。真正需要在意性能的地方是那种每秒调几十万次的底层循环而在那种地方你本来就该避免函数调用开销而不是纠结参数形态。可读性上的取舍更微妙。def f(*args, **kwargs)这个签名对读代码的人几乎零信息量你完全不知道该怎么调它。我给自己的规矩是能被列举出来的参数一定要列出来只有真正无法枚举的才用星号兜底。一个函数如果同时有五个显式参数和**kwargs说明它的职责可能已经过载了该考虑拆了。最后分享一个我最近改的小习惯凡是函数接收**kwargs并且要在内部消费其中某些键我会在函数开头写一行显式的解构把要用到的键拿出来其余的再原样传下去。def build_request(url, **options): retry options.pop(retry, 0) timeout options.pop(timeout, 5.0) # 到这里 options 里剩下的都是要透传给下层的 return _send(url, retryretry, timeouttimeout, **options)这么做的好处是哪些键是我这层消费的、哪些是往下传的代码里写得明明白白。坏处是pop改动了options如果后面还有别的逻辑要用原始字典就会出问题所以只在函数尾部没有其他消费时用。用pop还是get加显式del取决于你的团队怎么理解“消费”这个词——我个人倾向pop因为它语义上更贴近“我把这个键取走了”。这套东西说到底没什么高深理论难的是在实际项目里保持一致的判断标准什么时候该开放、什么时候该锁死、什么该显式、什么该省略。我用了好几年才把直觉磨得比较稳中间交的学费基本都是线上事故换来的。如果你现在正好在写一个要给很多人用的公共函数我的建议是先把签名写死、把参数列全等真有“必须透传”的需求时再放开反过来做的话收拾起来会麻烦得多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →