R

ReflexKR

Reflex 官方文档

名牌机场:Python 对象模型

·

课程概览

1

标签不是箱子:名字与对象

2

封口的箱子:可变与不可变

3

箱中箱:容器与复制

4

交出箱子的时候:函数与参数

5

区域与通行证:作用域

6

随身行李:闭包

7

箱子规格书:类与实例

8

机场标准流程:双下划线方法

9

规格书的规格书:继承与 MRO

10

安检口:属性访问的秘密

11

无主的箱子:内存与 GC

12

储物柜号码:哈希与相等,综合

名牌机场:Python 对象模型
第4课

时间 2:30–3:20

交出箱子的时候:函数与参数

失物报告单

LOST-004

每次调用,上一单都跟着出来

报告人

外卖 App 创业公司的开发者 陈工

丢了什么

新的订单列表。每次调用创建订单列表的函数,返回的列表里都还堆着之前顾客的订单。

结案条件

找出之前的订单留在了哪里,并让每次调用都从空列表开始。

天空岛国际机场 行李中心

午饭时间,一位创业公司的开发者抱着笔记本电脑冲了进来。第一位顾客只点了一杯咖啡,可第二位顾客的小票上,咖啡和贝果一起打了出来。

都美兰班长用手指点着函数定义的那一行。“把箱子交出去的时候,交出去的不是箱子,而是多挂了一个标签。还有,这个空箱子是在创建函数时只买了一次的。”

4-1. 函数给箱子多挂一个标签(11 分钟)

调用函数时,作为参数传进去的对象不会被复制。函数里的形参名会作为标签,再挂到同一个对象上。这叫传对象引用(call by object reference,也叫 call by sharing)。

mine = ["laptop"]
def show(bag):
    print(bag is mine)
show(mine)
# True

既然只是多挂了一个标签,第 1 课讲的两种动作就原样适用:

  • 改动箱子:在函数里像 bag.append(...) 这样修改内容,外面的名字也能看到。因为是同一个箱子。
  • 移动标签:在函数里用 bag = ... 重新绑定,只有函数里的标签移走了。外面的名字仍挂在原来的箱子上。
def pack(bag):
    bag.append("charger")

def swap(bag):
    bag = ["new"]
    bag.append("charger")

mine = ["laptop"]
pack(mine)
print(mine)
# ['laptop', 'charger']
swap(mine)
print(mine)
# ['laptop', 'charger']

下面的标签板上第一次出现了调用栏(帧)。调用函数时,左边会出现一个 pack 栏,里面的标签 bag 和外面的 mine 指向同一个箱子。函数结束后,这一栏就消失了。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

在 bag.append 中,pack 栏里的 bag 和 module 里的 mine 指向同一个箱子,被改动的正是这个箱子。在 bag = bag + [...] 中,只有 bag 移到了新箱子上,等这一栏消失,那个新箱子也一起被丢弃。

第 2 课的 += 又出现了。同样是 +=,根据类型不同,外面有时能看到,有时看不到。

def bump(n):
    n += 1          # int:新对象,只移动函数里的标签

def extend(lst):
    lst += [1]      # list:__iadd__,改动同一个箱子

count, items = 0, []
bump(count)
extend(items)
print(count, items)
# 0 [1]

Python 里没有“能改动外部变量的参数”。函数影响外部的途径只有两条:修改传进来的可变对象,或者返回一个值。

小钉提示

→ 别纠结“到底是传值还是传引用”。Python 做的永远是同一件事:把形参这个标签再挂到传进来的对象上。外面能不能看到,取决于函数是改动了那个对象,还是只移动了标签。

4-2. 默认参数在定义时只创建一次(11 分钟)

把报告人的函数精简一下,是这样的:

def f(x=[]):
    x.append(1)
    return x

print(f())
# [1]
print(f())
# [1, 1]
print(f.__defaults__)
# ([1, 1],)

默认值 [] 不是在调用函数时计算,而是在执行 def 语句时计算一次。这样创建的列表挂在函数对象的 __defaults__ 元组上,只要函数还活着,它就一直在。每次省略参数调用时,形参 x 都会挂到这唯一的列表上。

print(f() is f.__defaults__[0])
# True

在下面的标签板上跟踪两次调用。调用栏出现又消失,但挂在 defaults 上的列表一直留着,订单越积越多。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

在 orders=[] 中,function 箱子的 defaults 元组抓着一个列表,两次调用都往同一个列表里放。在 orders=None 中,defaults 里只有 None,每次调用都会新建一个列表。

修正方法是固定的:默认值设为 None,在函数里每次现做一个新列表。

def add_order(item, orders=None):
    if orders is None:
        orders = []
    orders.append(item)
    return orders

print(add_order("coffee"), add_order("bagel"))
# ['coffee'] ['bagel']
print(add_order.__defaults__)
# (None,)

如果 None 本身是有意义的值,需要和“没传参数”区分开,就专门创建一个谁都不会用的对象,当作哨兵(sentinel)。它是世上独一无二的对象,所以用 is 比较。

_MISSING = object()

def lookup(key, default=_MISSING):
    if default is _MISSING:
        return f"no default for {key}"
    return default

print(lookup("gate"), lookup("gate", None))
# no default for gate None
  • 危险的默认值不只有 []、{}、set()。“在 def 时计算一次”这条规则对所有默认值都一样适用。
  • 仅限关键字参数的默认值,另外挂在 f.__kwdefaults__ 上。

4-3. 修改型函数与返回新对象型函数(7 分钟)

把函数分成两类来想会很方便:一类就地修改传进来的箱子,另一类不动原件、返回一个新箱子。

nums = [3, 1, 2]
print(nums.sort(), nums)
# None [1, 2, 3]

nums = [3, 1, 2]
out = sorted(nums)
print(out, nums, out is nums)
# [1, 2, 3] [3, 1, 2] False

Python 有个惯例:就地修改的函数和方法返回 None。list.sort()、list.append()、list.reverse()、dict.update()、random.shuffle() 都是如此。这是一种约定,用来明确告诉你:被改的是原件。

于是就有了这样的失误:

nums = [3, 1, 2]
nums = nums.sort()
print(nums)
# None
就地修改(返回 None)返回新对象
lst.sort()sorted(lst)
lst.reverse()lst[::-1]、list(reversed(lst))
lst.append(x)、lst.extend(xs)lst + [x]
d.update(other)d | other

自己写函数时,也在两者中选一个。既修改又返回的话,调用方很容易在不知道原件已变的情况下继续使用。

4-4. *args 与 **kwargs,以及防御性复制(7 分钟)

*args 把多出来的位置参数收进一个新元组,**kwargs 把多出来的关键字参数收进一个新 dict。但里面插着的,是传进来的那些对象的标签。

def g(*args, **kwargs):
    return args, kwargs

items = ["a"]
opts = {"tag": items}
a, k = g(items, **opts)
print(type(a), type(k))
# <class 'tuple'> <class 'dict'>
print(a[0] is items, k is opts, k["tag"] is items)
# True False True

即使用 **opts 展开传入,函数收到的 kwargs 也是一个新 dict。所以函数往 kwargs 里加键,opts 不受影响。但作为值存放的列表,仍是同一个对象。

标签板

步骤 1 / 0

下一行将执行: 0

正在准备实验台…

刚刚变化的

封口的箱子(不可变)

pack 栏里的 items 是新元组,labels 是新 dict。元组的第一格和外面的 bag 指向同一个箱子,所以第 2 行的 append 在外面也能看到。

会把传进来的可变对象长期保存的函数,尤其要小心。调用方以后一旦修改那个对象,保存起来的东西也会跟着变。

def make_flight(code, passengers):
    return {"code": code, "passengers": passengers}

names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo', 'Cy']

这种时候,在接收的那一刻就给自己新做一份。这叫防御性复制(defensive copy)。

def make_flight(code, passengers):
    return {"code": code, "passengers": list(passengers)}

names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo']

list(passengers) 是浅复制(第 3 课)。里面装的是字符串,所以这样就够了。反过来,如果函数只是临时读一下收到的东西,就没必要复制。

4-5. 返回值也是移动标签(4 分钟)

return 也不复制对象。它把函数里做好的箱子交给调用方,由调用方的名字给这个箱子挂上标签。即使函数的栏消失了,外面还有标签,箱子就留了下来。

def build():
    bag = ["boarding pass"]
    return bag

b = build()
print(b)
# ['boarding pass']

看似返回多个值的 return a, b,其实返回的是一个元组。接收方解包时,再一个个挂上标签。

def stats(xs):
    return min(xs), max(xs)

res = stats([4, 9, 2])
print(res, type(res))
# (2, 9) <class 'tuple'>
lo, hi = stats([4, 9, 2])
print(lo, hi)
# 2 9

没有 return 的函数返回 None。4-3 中“修改型函数”返回 None,也是这个缘故。

结案(10 分钟)

下面是报告人的函数和修正后的函数。

def new_order(item, cart=[]):
    cart.append(item)
    return cart

print(new_order("coffee"))
# ['coffee']
print(new_order("bagel"))
# ['coffee', 'bagel']
print(new_order.__defaults__)
# (['coffee', 'bagel'],)

def new_order(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart

print(new_order("coffee"), new_order("bagel"))
# ['coffee'] ['bagel']

之前的订单,就在挂在函数对象 __defaults__ 上的那个列表里。服务器把函数定义一次后反复调用,于是直到服务器关机之前,这个列表一直在收集每位顾客的订单。

开发者走后,班长补了一句:“函数结束时标签会掉,可函数对象抓住的箱子不会掉。函数本身也是箱子嘛。”

结案报告 — LOST-004。默认参数 cart=[] 在 def 时只创建一次,留在了 __defaults__ 里,所有调用共享同一个列表。用 None 哨兵让每次调用新建列表,问题解决。

课后练习

  1. def add(x, seen=set()) 执行 seen.add(x) 后返回 len(seen)。预测 add(1)、add(2)、add(1) 的结果,并查看 add.__defaults__。
  2. def clear(lst): lst = [] 和 def clear(lst): lst.clear() 分别会对外面的列表产生什么影响?
  3. 写一个函数:接收列表,返回排好序的新列表,但不碰原件。用两种方法(sorted、先复制再 sort)各写一遍。

班长的工作手册

  • 参数传递是传对象引用。形参是挂到同一个对象上的又一个标签。

  • 在函数里修改对象,外面能看到;重新绑定,外面看不到。

  • 默认参数在 def 时计算一次,留在 __defaults__ 里。可变默认值改用 None。

  • 就地修改的函数返回 None(sort 对 sorted)。

  • *args、**kwargs 是新元组、新 dict,但里面的对象是同一批。要长期保存的,做防御性复制。

  • return 也是移动标签。返回多个值其实是返回一个元组。

← 上一课

第3课 · 箱中箱:容器与复制

下一课 →

第5课 · 区域与通行证:作用域

订阅新课时和新课程通知

本课程新增课时或推出后续课程时,我们会发邮件通知你。

收集项目:电子邮箱 · 用途:本课程的新课时和上线通知 · 保存期限:直到你要求删除。不会用于其他用途,也不会提供给他人。回复任意通知邮件,我们会立即删除。

问答

关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。

以纯文本显示,不支持链接和格式。

R

ReflexKR

动手学 Python 和 Reflex。所有示例均已在 Reflex 0.10.0 上实际运行验证。

ReflexKR 免费,没有横幅广告。如果对你有帮助,欢迎请我们喝杯咖啡。 ☕ 请我们喝杯咖啡

联盟链接与赞助说明
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex