失物报告单
LOST-004
报告人
外卖 App 创业公司的开发者 陈工
丢了什么
新的订单列表。每次调用创建订单列表的函数,返回的列表里都还堆着之前顾客的订单。
结案条件
找出之前的订单留在了哪里,并让每次调用都从空列表开始。
天空岛国际机场 行李中心
午饭时间,一位创业公司的开发者抱着笔记本电脑冲了进来。第一位顾客只点了一杯咖啡,可第二位顾客的小票上,咖啡和贝果一起打了出来。
都美兰班长用手指点着函数定义的那一行。“把箱子交出去的时候,交出去的不是箱子,而是多挂了一个标签。还有,这个空箱子是在创建函数时只买了一次的。”
调用函数时,作为参数传进去的对象不会被复制。函数里的形参名会作为标签,再挂到同一个对象上。这叫传对象引用(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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
第 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 做的永远是同一件事:把形参这个标签再挂到传进来的对象上。外面能不能看到,取决于函数是改动了那个对象,还是只移动了标签。
把报告人的函数精简一下,是这样的:
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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
修正方法是固定的:默认值设为 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__ 上。把函数分成两类来想会很方便:一类就地修改传进来的箱子,另一类不动原件、返回一个新箱子。
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] FalsePython 有个惯例:就地修改的函数和方法返回 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 |
自己写函数时,也在两者中选一个。既修改又返回的话,调用方很容易在不知道原件已变的情况下继续使用。
*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
正在准备实验台…
刚刚变化的
封口的箱子(不可变)
会把传进来的可变对象长期保存的函数,尤其要小心。调用方以后一旦修改那个对象,保存起来的东西也会跟着变。
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 课)。里面装的是字符串,所以这样就够了。反过来,如果函数只是临时读一下收到的东西,就没必要复制。
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,也是这个缘故。
下面是报告人的函数和修正后的函数。
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 哨兵让每次调用新建列表,问题解决。
def add(x, seen=set()) 执行 seen.add(x) 后返回 len(seen)。预测 add(1)、add(2)、add(1) 的结果,并查看 add.__defaults__。def clear(lst): lst = [] 和 def clear(lst): lst.clear() 分别会对外面的列表产生什么影响?sorted、先复制再 sort)各写一遍。班长的工作手册
参数传递是传对象引用。形参是挂到同一个对象上的又一个标签。
在函数里修改对象,外面能看到;重新绑定,外面看不到。
默认参数在 def 时计算一次,留在 __defaults__ 里。可变默认值改用 None。
就地修改的函数返回 None(sort 对 sorted)。
*args、**kwargs 是新元组、新 dict,但里面的对象是同一批。要长期保存的,做防御性复制。
return 也是移动标签。返回多个值其实是返回一个元组。
订阅新课时和新课程通知
本课程新增课时或推出后续课程时,我们会发邮件通知你。
问答
关于本课有任何疑问都可以在这里留言。所有语言版本页面的提问会汇集在一起。