我给函数参数设了个默认空列表,本以为每次调用都会拿到全新的一个,结果它竟在一次次独立的调用之间,诡异地记住了上一次追加进去的数据的深度复盘

我写了个"往列表追加一项再返回"的函数,图方便给列表参数设了默认值

我给函数参数设了个默认空列表,本以为每次调用都是全新的一个,结果它竟在多次调用之间诡异地"记住"了上一次的数据的深度复盘

这是一个让我对 Python "默认参数"长记性的故事。我写了一个工具函数,大致是"往一个列表里追加一项,然后返回这个列表"。为了图方便,我给那个列表参数,设了一个默认值——一个空列表 [],这样调用方不传的时候,就用一个"新的空列表"开始。在我朴素的认知里,这天经地义:既然默认值是 [],那每次调用这个函数、不传那个参数时,它不就各自拿到一个"全新的空列表"嘛?

可运行结果,彻底颠覆了我的认知:我第一次调用,传入 "a",返回了 ["a"],没问题;可我第二次调用,传入 "b",本以为会返回一个全新的 ["b"],结果它返回的,竟是 ["a", "b"]!那个本该"全新"的空列表里,居然还留着上一次调用追加进去的 "a"!我再调第三次,它就变成了 ["a", "b", "c"]——这个函数,像是有了"记忆"一样,在一次次独立的调用之间,诡异地累积、记住了之前所有的数据。我当时整个人都是懵的:我明明每次都期望一个"空列表"开始啊,这些旧数据,是从哪冒出来的?难道是什么"幽灵"?我一度怀疑是不是变量作用域出了问题,排查了半天。直到我去翻 Python 的官方文档,看到那条著名的"可变默认参数(mutable default argument)"警告,才恍然大悟,狠狠给自己上了一课:原来,Python 函数的默认参数值,是在函数"定义"的那一刻(def 语句执行时),就被求值一次、并创建好的;之后,这个默认值对象,会被所有调用这个函数、且没传该参数的调用,共享同一个!也就是说,我那个写在 def 行里的 [],在整个程序里,只被创建了一次——它是一个单一的、被反复复用的列表对象;而我每次调用函数时往里 append 的数据,都追加到了这同一个列表上!而列表又是可变(mutable)的,我的修改会"留"在它身上。所以,它当然会"记住"上次的数据——因为,从头到尾,压根就只有一个列表,而不是我以为的"每次调用都新建一个"。我以为的"每次一个全新的空列表",从一开始,就是我对默认参数求值时机的、一个美丽的误解。

故障现场:一个被所有调用共享的默认列表

我把这个"会记忆的默认参数"的现场,用代码摊开给你看:

# ✗ 灾难: 用"可变对象(列表)"作为默认参数
def add_item(item, items=[]):     # ← 这个 [] 只在"def 执行时"创建一次!
    items.append(item)
    return items

# 调用方以为每次都是新列表, 结果:
print(add_item("a"))    # ["a"]            ← 看起来没问题
print(add_item("b"))    # ["a", "b"]       ← ??? "a" 怎么还在?!
print(add_item("c"))    # ["a", "b", "c"]  ← 它"记住"了所有历史!

# 为什么? 因为默认值 [] 在"函数定义时"求值一次, 之后被所有调用共享:
print(add_item.__defaults__)   # (['a', 'b', 'c'],)  ← 看! 默认值就是那个被改过的列表

# 真相:
#   - def add_item(item, items=[]) 执行时, 创建了"一个"列表对象做默认值。
#   - 此后每次"不传 items"地调用, 用的都是"同一个"那个列表。
#   - items.append(item) 修改的, 是这个被共享的列表 → 修改累积下来。
#   - 列表是可变的, 所以改动"留"在了它身上, 下次调用还看得见。

# 对比: 用"不可变对象"做默认值, 看起来"没事"(但原理一样):
def f(x, n=0):       # 0 是不可变的
    n = n + 1        # 这是"重新绑定 n", 不是"修改 0"
    return n
# 每次都返回 1, 因为没有"修改"那个共享的默认值, 只是基于它算了新值。

看着这段代码和那个 __defaults__ 的输出,我才算真正理解了这个"幽灵记忆"的根源。问题的核心,是我对 Python 默认参数的求值时机,有一个根本性的误解:我以为,默认值 [],是在每次调用函数时,才"现场"创建一个新的;可事实是,它是在函数被"定义"的那一刻(def 语句执行时),就只创建了一次这就埋下了祸根:def add_item(item, items=[]) 这行代码执行时,Python 会立即对那个 [] 求值,创建出一个列表对象,把它存起来(就存在 add_item.__defaults__ 里)作为默认值;此后,每一次"不传 items"地调用这个函数,用的,都是这同一个、被存起来的列表对象——而不是每次新建一个。而列表,又恰恰是可变(mutable)的:我函数体里的 items.append(item),是在原地修改这个被所有调用共享的列表;每一次调用的修改,都累积在了它身上、并留存了下来。所以,它当然会"记住"历史——因为从头到尾,根本就只有一个列表,我所有的 append,都堆在了这一个列表上。这也解释了为什么"用不可变对象(如 0)做默认值时,看起来没事":因为像 n = n + 1 这样的操作,是重新绑定 n 到一个新值,而不是去"修改"那个共享的 0(数字本就不可变、改不了);你没有污染那个共享的默认值,所以下次调用,它还是干净的 0。问题,只在默认值是可变对象(列表、字典、集合)、且你原地修改了它时,才会暴露出来。归根结底,我犯的错,是想当然地以为"默认参数每次调用都重新创建",而不知道它其实是"定义时创建一次、之后被所有调用共享"——这个对"求值时机"的误解,加上"列表可变"这个事实,就共同酿成了那个看似见鬼、实则必然的"幽灵记忆"。

第一件事:搞懂默认参数"定义时求值一次、被共享"

定位到根源,我必须把"Python 默认参数的求值时机和共享机制",彻底搞清楚:

Python 默认参数: "定义时"求值一次, 之后被所有调用"共享"

# 关键机制:
#   def f(x, items=[]):  ← 这个 [] 在"def 这行执行时"(即函数定义时)
#                           就被求值、创建好, 存进 f.__defaults__。
#   之后每次"不传 items"地调用 f, 用的都是"同一个"那个对象。

# 所以:
#   - 默认值, 全程"只创建一次", 不是"每次调用新建"!
#   - 如果默认值是"可变对象"(list/dict/set), 且你在函数里"原地修改"了它,
#     那这个修改会"留存", 影响所有后续调用 → 就是本文的坑。

# 可变 vs 不可变, 表现不同:
#   - 不可变默认值(int/str/tuple/None): 你没法"原地改"它,
#     通常只是基于它算新值/重新绑定 → 看起来"没事"。
#   - 可变默认值(list/dict/set): 你一旦原地 append/update/add,
#     就污染了那个共享对象 → 幽灵记忆。

# 一个更深的 Python 认知: "变量是名字, 对象是实体"
#   - items=[] 让"默认值"这个槽位, 指向"一个列表对象"。
#   - 多次调用, 多个 items 名字, 指向的是"同一个列表对象"。
#   - append 改的是那个"对象", 所有指向它的名字都看得见变化。

# 黄金法则: 永远不要用"可变对象"作为默认参数值!
#   要"每次调用都新建", 就用 None 做哨兵, 在函数体里新建(下一节)。

原理终于刻进脑子里了。Python 默认参数的机制,就一句话:默认值在"函数定义时"(def 那行执行时)就被求值、创建一次,存进 f.__defaults__;之后每次"不传该参数"的调用,用的都是"同一个"那个对象所以,默认值是全程只创建一次的,不是每次调用新建!而如果这个默认值,恰好是个可变对象(list/dict/set),并且你在函数里原地修改了它,那这个修改就会留存下来、影响所有后续调用——这,就是本文那个坑。这也解释了"可变"和"不可变"默认值的不同表现:不可变默认值(int/str/tuple/None),你没法"原地改"它,通常只是基于它算个新值,所以看起来"没事";而可变默认值,你一旦原地 append/update,就污染了那个共享对象,幽灵记忆就出现了。而这背后,是 Python 一个更深的对象模型认知:"变量是名字,对象是实体"——items=[] 让"默认值"这个槽位,指向了"一个列表对象";多次调用,多个 items 名字,指向的是"同一个列表对象";append 改的是那个对象本身,于是所有指向它的名字,都看得见这个变化。由此,我给自己立下了一条 Python 编程的黄金法则:永远不要用"可变对象"(列表、字典、集合)作为默认参数值!如果你想要的是"每次调用都新建一个",那就用 None 做"哨兵",在函数体里去新建——这,是我用一个"会记忆的幽灵列表",换来的、最该铭记的一课。

第二件事:正解——用 None 做哨兵,在函数体里新建

搞懂了根因——"可变默认值被定义时创建一次、所有调用共享"——正解就清晰了:把默认值,设成不可变的 None 做"哨兵";然后在函数体内部,判断:如果参数是 None,就现场新建一个全新的列表/字典。这样,每次调用、不传参时,都能拿到一个真正全新的对象。

# 正解: 用 None 做哨兵, 在函数体里新建可变对象
def add_item(item, items=None):     # ✓ 默认值用不可变的 None
    if items is None:               # ✓ 判断: 没传 → 现场新建一个全新列表
        items = []
    items.append(item)
    return items

# 现在, 每次调用都是全新的列表:
print(add_item("a"))    # ["a"]   ✓
print(add_item("b"))    # ["b"]   ✓ 干净! 不再记住 "a"
print(add_item("c"))    # ["c"]   ✓

# 为什么 None 行? 因为:
#   - None 是不可变的, 没法被"污染"。
#   - "新建列表"这一步, 是在"函数体里"执行的——每次调用都会跑一遍,
#     所以每次都得到一个"全新的" []。
#   ↑ 关键区别: 把"创建可变对象"的时机, 从"定义时一次"
#     移到了"每次调用时", 这才是我们真正想要的行为。

# 字典、集合同理:
def f(data=None):
    if data is None:
        data = {}    # 每次新建
    ...

# 用 None 哨兵, 而不是直接判断真假, 注意:
#   if not items:  ✗ 不够严谨! 如果调用方真的传了个空列表 [], 也会被当成"没传"
#   if items is None:  ✓ 精确判断"是不是没传"(None 是哨兵, [] 是合法的传入)

# 核心: 默认值用不可变的 None; 真正的可变对象, 在函数体内"每次新建"。

这个正解的核心,是把"创建可变对象"的时机,从"函数定义时(只一次)",挪到"每次调用时"。具体做法:把默认值,设成不可变的 None 来当一个"哨兵(sentinel)"——表示"调用方没有传这个参数";然后在函数体内部,加一句 if items is None: items = [],意思是"如果没传,就现在、这里,新建一个全新的列表"。这样为什么就对了?因为:第一,None不可变的,它没法被污染,做哨兵很安全;第二,也是最关键的——"items = [] 新建列表"这一步,是写在函数体里的,而函数体是每次调用都会执行一遍的,所以,每次调用、不传参时,都会实实在在地、重新创建一个全新的 []。这,才是我们真正想要的"每次一个新列表"的行为。字典、集合,也是完全一样的处理方式。这里还有一个容易踩的细节:判断时,要用 if items is None,而不要if not items——因为 if not items 不够严谨,如果调用方真的传了一个空列表 [](这是合法的输入),not [] 也是 True,会被错误地当成"没传",从而被你新建的列表覆盖掉;而 is None,能精确地判断"到底是不是没传"(None 是哨兵,空列表 [] 是一个合法的、应被尊重的传入值)。归根结底:默认值,用不可变的 None;而真正的可变对象,在函数体内"每次新建"。我那次的错误,正是把可变对象直接写在了默认值的位置,让它在"定义时"就被创建并共享了;而正解,就是把它的创建,推迟到"每次调用"。

下面这张图,对比了"可变对象做默认值"和"None 哨兵"两条路径:

这张图的对比很清楚:左边红色那条,直接把空列表写成默认值,定义时创建一次、所有调用共享同一个,原地修改不断累积、形成幽灵记忆;右边绿色那条,用 None 做哨兵,默认值是不可变的 None,在函数体里判断后新建,每次调用都得到一个全新的列表。两条路的根本分野,在于那个可变对象,是在"定义时创建一次",还是在"每次调用时新建"。

第三件事:默认参数"定义时求值"还会在哪些地方坑你

填平了可变列表这个坑,我系统排查了"默认参数定义时求值"这个机制,还会在哪些地方,带来意料之外的行为:

# "默认参数定义时求值一次"会坑你的其它地方:

# 1. 可变默认值(本文): list/dict/set 被共享、被污染。
def f(items=[]): ...      # ✗      def f(items=None): ...  ✓

# 2. 用"定义时的值"做默认, 而你以为是"调用时的值"
import time
def log(msg, ts=time.time()):    # ✗ ts 在"定义时"就固定了那一刻的时间!
    print(ts, msg)               # 之后每次调用, ts 都是同一个(定义时的)时间
# 正解:
def log(msg, ts=None):
    if ts is None: ts = time.time()   # ✓ 每次调用取"当时"的时间

# 3. 默认值引用了一个"定义时"的变量
DEFAULT = get_config()           # 假设这是定义时的配置
def f(cfg=DEFAULT): ...          # cfg 默认值绑定的是"定义那一刻"的 DEFAULT
                                 # 之后 DEFAULT 变了, 默认值也不会跟着变

# 4. 默认值是一个"定义时就调用的函数"
def f(x=expensive_call()):       # ✗ expensive_call 在"定义时"就执行了一次!
    ...                          # (不是每次调用执行) 既浪费、行为也可能不对

# 共同点: 默认参数表达式, 都是在"def 执行(定义)时"求值一次的,
#   而不是"每次调用时"。凡是你期望"每次调用都重新算"的, 都不能放默认值里!
#   → 一律用 None 哨兵, 把"每次都要算/建的东西", 挪到函数体里。

# 记住: 默认参数的表达式 = 定义时算一次; 函数体的代码 = 每次调用都算。

这一排查,让我对"定义时求值"这个机制的影响,有了全面的认识。"默认参数在定义时求值一次"这个机制,坑人的地方远不止可变列表:用"定义时的值"当默认、却以为是"调用时的值"——比如 def log(msg, ts=time.time()),这个 ts,在函数定义的那一刻就被固定成了那一刻的时间,之后每次调用,ts 都是同一个(定义时的)时间,而不是你以为的"当时的时间";默认值引用了定义时的变量/配置——绑定的是"定义那一刻"的值,之后那个变量变了,默认值也不会跟着变;默认值是一个"定义时就调用的函数"——比如 def f(x=expensive_call()),这个 expensive_call()定义时就执行了一次(而不是每次调用执行),既浪费、行为也可能不对。这些坑的共同点是:默认参数的表达式,都是在"def 执行(定义)时"求值一次的,而不是"每次调用时"。所以,凡是你期望"每次调用都重新计算/创建"的东西,都绝不能直接放在默认值里!正确的做法,统一是:None 做哨兵,把那些"每次都要算、要建"的东西,挪到函数体里去执行我把这条规律,浓缩成一句话刻在心里:默认参数的表达式,是"定义时算一次";函数体的代码,才是"每次调用都算"。想清楚你要的是哪一种,就不会再被默认参数的求值时机,给绊倒了。

第四件事:可变 vs 不可变,Python 里处处要分清

这次踩坑,逼我把 Python 里"可变(mutable)"和"不可变(immutable)"的区别,以及它在各处引发的连锁反应,系统地梳理了一遍——这是理解 Python 对象模型的关键:

# 可变 vs 不可变: Python 对象模型的一条主线

# 不可变(immutable): int, float, str, tuple, frozenset, bool, None
#   - 创建后"内容不能改", 任何"修改"其实是"生成新对象"。
#   - x = "abc"; x += "d"  → 不是改了 "abc", 是生成新串 "abcd" 让 x 指向。

# 可变(mutable): list, dict, set, 以及大多数自定义对象
#   - 可以"原地修改"内容(append/update/add/改属性), 对象身份(id)不变。
#   - 多个变量指向同一个可变对象时, 一处改, 处处可见!

# 这条主线, 会在很多地方引发"意外":

# 1. 默认参数(本文): 可变默认值被共享、污染。
def f(items=[]): ...   # ✗

# 2. 赋值只是"绑定名字", 不是"拷贝"
a = [1, 2]; b = a      # b 和 a 指向同一个列表!
b.append(3)            # a 也变成 [1,2,3]! (不是拷贝)
# 要拷贝: b = a.copy() / b = list(a) / b = a[:]

# 3. 函数传参: 传的是"引用"(对象), 改可变实参会影响外面
def g(lst): lst.append(99)
data = [1]; g(data)    # data 变成 [1, 99]! 函数改了外面的列表

# 4. 把可变对象当字典的 key —— 报错(list 不可哈希)
# d = {[1,2]: "x"}     # ✗ TypeError: unhashable type: 'list'
d = {(1,2): "x"}       # ✓ tuple 不可变, 可哈希, 能当 key

# 5. 类属性用可变对象 —— 所有实例共享!(和默认参数同源的坑)
class C:
    items = []         # ✗ 类属性, 所有实例共享同一个列表!
# 正解: 在 __init__ 里 self.items = []

# 核心: 时刻分清"你手里的是可变还是不可变对象"、"你是在改它还是在重新绑定"。
#   Python 很多"诡异行为", 根子都在这条"可变/不可变 + 引用语义"上。

这一梳理,让我看清了一条贯穿 Python 的主线:对象的"可变 / 不可变"——不可变对象(int/str/tuple/None 等),创建后内容不能改,任何"修改"其实都是生成新对象;可变对象(list/dict/set 及大多数自定义对象),可以原地修改、对象身份不变,而当多个变量指向同一个可变对象时,一处改、处处可见这条主线,会在 Python 的许多地方,引发"意外":默认参数(本文);赋值只是绑定名字、不是拷贝(b = aba 指向同一个列表,改 b 会影响 a,要拷贝得用 a.copy());函数传参传的是引用(在函数里改可变实参,会影响到外面的对象);可变对象不能当字典的 key(list 不可哈希,要用 tuple);类属性用可变对象会被所有实例共享(这和默认参数是同源的坑,正解是在 __init__self.items = [])。归根结底:要时刻分清"你手里的,是可变还是不可变对象"、以及"你是在修改它,还是在重新绑定一个名字"。Python 里许许多多看似"诡异"的行为,根子,都在这条"可变/不可变 + 引用语义"的主线上——理解了它,那些诡异,就都成了理所当然。把可变与不可变的关键区别,整理成一张表:

维度 不可变 (int/str/tuple) 可变 (list/dict/set)
内容能改吗 不能,改=生成新对象 能,原地修改
做默认参数 安全 危险,会被共享污染
多名字指向 无影响(改即新对象) 一处改处处可见
能当 dict key 能(可哈希) 不能(不可哈希)
做类属性 相对安全 所有实例共享,危险

第五件事:理解一门语言的"求值时机"和"对象模型"

这次踩坑,在认知层面给了我最大的纠偏——它让我明白,真正掌握一门语言,要深入它的"求值时机"和"对象模型"。我把这层反思,沉淀了下来:

认知纠偏: 掌握一门语言, 要懂它的"求值时机"和"对象模型"

# 我的误解(错误的):
#   我凭"直觉"以为"默认参数每次调用都新建"——这个直觉,
#   建立在我对 Python"求值时机"的错误想象上, 而非它真实的规则。

# 两个我没吃透、却处处影响行为的底层概念:

# 1. 求值时机(什么时候算): "定义时" vs "调用时" vs "使用时"
#   - 默认参数: 定义时算一次(本文的坑)。
#   - 函数体: 每次调用时算。
#   - 装饰器: 定义时应用。生成器: 惰性、用到才算。
#   → 搞错"什么时候算", 就会对行为产生错误预期。

# 2. 对象模型(数据怎么存): "变量是名字, 不是盒子"
#   - 变量是"指向对象的名字"(引用), 不是"装值的盒子"。
#   - 赋值是"贴标签", 不是"拷贝内容"。
#   - 可变对象被多个名字指, 改一个全看见。
#   → 用"盒子"的心智模型去理解 Python, 处处会撞墙。

# 普遍道理: 每门语言都有它的"心智模型", 凭别的语言的直觉会栽
#   - C 程序员的"变量是内存盒子"直觉, 在 Python 里行不通。
#   - JS 的相等性直觉, 到了别的语言也不一样。
#   → 学一门语言, 要主动学它"自己的"求值规则和对象模型, 别凭直觉迁移。

核心: 真正掌握一门语言, 不止是会语法, 更要懂它的"求值时机"和"对象模型"。
  很多"诡异 bug", 都是用错了心智模型、对求值时机想当然的结果。

这层反思,是这次踩坑给我最高维度的收获。复盘我的误解,根源是:我凭着一个直觉——"默认参数每次调用都新建"——去用它;而这个直觉,建立在我对 Python "求值时机"的错误想象上,而非它真实的规则。这件事,让我意识到两个我一直没吃透、却处处影响着程序行为的底层概念:第一,求值时机(什么时候算)——同样是代码,"定义时"算、"调用时"算、"使用时"算,是完全不同的:默认参数是定义时算一次(本文的坑)、函数体是每次调用时算、生成器是惰性的用到才算;一旦你搞错了"什么时候算",就会对程序的行为,产生错误的预期。第二,对象模型(数据怎么存)——Python 里,"变量是名字,不是盒子":变量是"指向对象的名字(引用)",而不是"装值的盒子";赋值是"贴标签",而不是"拷贝内容";可变对象被多个名字指着时,改一个、全都看得见。如果你用"盒子"那套心智模型去理解 Python,就会处处撞墙而这,其实是一个更普遍的道理:每一门语言,都有它自己的"心智模型";凭着别的语言的直觉,去用一门新语言,迟早会栽跟头——C 程序员"变量是内存盒子"的直觉,在 Python 里行不通;一门语言的相等性、求值规则,换一门语言也常常不一样。所以,学一门语言,要主动地、专门地,去学它"自己的"求值规则和对象模型,而不是想当然地,从别处迁移直觉。归根结底:真正掌握一门语言,不止是会它的语法,要懂它的"求值时机"和"对象模型"。我这次的坑,以及许许多多看似诡异的 bug,追到根上,几乎都是"用错了心智模型、对求值时机想当然"的结果。把语言的底层模型搞懂,才能从"会写"走向"真正掌握"。把"凭直觉"和"懂模型"两种状态对比成一张表:

维度 凭直觉(踩坑) 懂底层模型(掌握)
对默认参数 以为每次新建 知道定义时算一次
对变量 当成装值的盒子 知道是指向对象的名字
对赋值 以为是拷贝 知道是绑定/贴标签
学新语言 照搬旧语言直觉 专门学它自己的模型
遇诡异 bug 百思不得其解 从模型上理所当然

一套"写默认参数该怎么做"的决策流程

把这次踩坑的全部教训,我浓缩成了一张"给函数写默认参数时,该怎么做"的决策图,贴在了团队的 Python 规范里:

这张图,把我"血泪换来"的整套方法论,串成了一条可执行的路径:设默认值前,先问它是不是可变对象——不可变(int/str/tuple/None)可以直接写、安全;可变(list/dict/set)绝不能直接写,要把默认值设成 None、在函数体里 if x is None 新建。再问它是不是一个表达式/函数调用——如果你期望它每次调用都重新算,也要用 None 哨兵、挪进函数体;如果你就是要定义时算一次,可以放默认值,但要清楚这是"定义时算"。这条以"可变性"和"求值时机"为两把尺子的决策链,现在是我们团队写每一个默认参数时的准则。

我立下的几条 Python 默认参数与对象规矩

这次"幽灵记忆列表"的踩坑,让我把 Python 默认参数和对象模型的注意事项,认真地立成了几条规矩:

  1. 绝不用可变对象做默认参数。列表/字典/集合一律别直接写默认值,会被定义时创建、所有调用共享污染。
  2. 可变默认值用 None 哨兵。默认值设 None,函数体里 if x is None: x = [] 每次新建。
  3. 判断用 is None,别用 not x精确区分"没传"和"传了空对象"。
  4. 记牢默认参数定义时求值一次。表达式/函数调用做默认值都是定义时算,要每次算就挪进函数体。
  5. 类属性也别用可变对象。会被所有实例共享,可变状态放 __init__ 里的 self.x = []
  6. 分清可变与不可变、赋值是绑定不是拷贝。要拷贝就显式 copy(),传参会影响外部可变对象。
  7. 掌握语言的求值时机和对象模型。别凭别的语言的直觉迁移,诡异 bug 多源于心智模型用错。

写在最后

这次"我给函数设了个默认空列表、它却在多次调用间诡异地记住了旧数据"的经历,是我在 Python 路上,一次很经典、也很受用的成长。它教给我的,远不止"别用可变对象做默认参数"这一条具体的技术经验,更是一种对待编程语言的根本态度——真正掌握一门语言,不止是会写它的语法,更要深入理解它的"求值时机"和"对象模型"这些底层的心智模型。我那次的坑,根源不在某个语法细节,而在于我用一个错误的心智模型(以为默认参数每次调用都新建、以为变量是装值的盒子),去想象 Python 的行为——而语言,从不会按我的想象来,它只按它真实的规则运行。

所以,当你学习、使用一门语言时,请别满足于"能写出能跑的代码",也别想当然地,把别的语言的直觉,迁移过来——而要带着好奇,去搞懂它自己的底层规则:它的代码什么时候求值?它的变量和对象,到底是怎么存、怎么传的?就像 Python 的默认参数,你只要真正理解了"它在定义时求值一次、之后被共享",就再也不会写出那个"会记忆的幽灵列表",反而会觉得"这不是理所当然吗"。把一门语言的求值时机和对象模型,真正吃透,是从一个"会用语法"的使用者,走向一个"懂原理、能预判行为"的掌握者,必经的修炼。愿你写的每一个默认参数,都行为如你所愿;也愿你我,在学习每一门语言时,都肯下功夫,去理解它那套独一无二的心智模型。共勉。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的 AI Agent 陷入了死循环,把同一个工具翻来覆去调了几十次就是不肯停,token 哗哗烧光、任务却永远完不成的深度复盘

2026-6-1 22:33:01

技术教程

我在 forEach 里写了 async/await 处理数组,以为会一项项乖乖等着执行完,结果它根本不等、后面的代码抢先跑了还把报错给悄悄吞了的深度复盘

2026-6-1 22:44:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索