函数把一段逻辑包起来:参数是入口,return 是出口。
没有 return 的函数返回 None,不是「什么都不返回」。
传列表进函数,函数改了它,外面看得见;传数字进去改,外面看不见。
这是可变与不可变的第一次正面遭遇。
前两篇的代码都是一路写到底的,改一处要翻半天。 教材第 3 章最后两节是【创建自定义函数】和【再谈列表】。 这两节合在一起讲,因为「传给函数之后原数据变没变」这个问题,答案全在列表身上。 这一篇是第 3 章的收尾,也是第 4 章【可变与不可变】的引子。 那一条讲透了,后面所有的踩坑都能提前躲开。
一、参数是入口,return 是出口
def area(w, h): return w * h,四个词:def 说「我要定义」,area 是名字,括号里是入口,
return 是出口。
调用 area(3, 4) 得 12,area(2.5, 4) 得 10.0。
同一个函数,参数类型换了照样跑——这是 Python 不给参数标类型的直接后果。
没有 return 的函数返回 None。不是「什么都不返回」,是「返回了一个叫 None 的东西」。
这两句话差别很大:前者你以为不能用,后者你能拿去判断 if x is None。
return 一旦执行,函数立刻结束。
写在 return 后面的代码永远跑不到——这不是错误,但常常是 bug 的来源,尤其是从别的语言转过来、
习惯了「最后一行是返回值」的人。
二、默认值和关键字参数
def greet(name, punct='。')——参数后面写个等号,就是默认值。不传就用默认的,传了就用你给的。
规矩只有一条:带默认值的参数必须写在后面。 def f(a=1, b) 会直接报语法错。
为什么?因为调用时是按位置对应的。如果第一个参数能省、第二个不能省,解释器没法判断你省的是哪一个。
调用时写上参数名,就变成关键字参数:greet('小王', punct='!')。
好处是顺序不重要,而且看一眼就知道这个值给的是谁。
参数一多,这个好处就明显了。draw(x=10, y=20, w=100, h=50) 比 draw(10, 20, 100, 50) 好读十倍。
还有一个坑要提前说:默认值只在定义时算一次。
写 def f(a=[]) 会让这个空列表被所有调用共享,第二次调用时它已经不是空的了。
要可变默认值,写 a=None 再在函数里判断。
三、看着像返回三个值,其实是一个元组
return lo, hi, avg 看着像返回三个值。演示里 type() 一打,是 tuple。
Python 没有「多返回值」这个功能,它是「一个元组 + 自动打包」。
左边用三个变量接,叫解包:lo, hi, avg = stats(...)。
个数对不上会报 ValueError,不是安静地给一半。
所以接收端也可以只用一个变量接:t = stats(...),然后 t[0]、t[1]。
两种写法都合法,前者可读性好,后者在「待会儿要整个传下去」时更方便。
四、局部变量出不去
演示第四段:外面 counter = 0,函数里写 counter = 1,函数跑完外面还是 0。
因为函数里赋值就是造一个新的局部变量,跟外面那个同名变量没有关系。名字一样,位置不一样。
真要改外面的,得在函数里写 global counter。但更好的做法是让它 return 出来,外面接住再赋值。
为什么?因为 global 让函数的副作用变得看不见——你读函数体时不知道它偷偷改了外面什么。
能 return 就别 global。
顺带一个相关的东西:函数里可以「读」外面的变量,不用声明。只有「写」的时候才需要 global。
这条不对称经常让人困惑。
五、传列表进去,会被改掉
演示第五段是关键对照。
add_item(box, '扳手') 里写 box.append(item),函数跑完,外面的 box 也多了扳手。
换成 box = box + [item],外面就看不见了。差别在哪?append 改的是传进来那个列表本身;
box + [...] 造了一个新列表,赋给局部名字,跟外面没关系。
这是「可变对象」的脾气:传的是标签,不是箱子。 函数拿到的是同一个列表的另一个名字,
改它就是改原来的。
数字、字符串、元组没有这个问题,因为它们不给改。x = x + 1 永远是造一个新的,原来那个动不了。
想让函数不改原数据,进去先复制一份:box = list(box) 或 box = box.copy()。
多写一行,省掉一整类难查的 bug。
六、再谈列表:排序的 key 和浅拷贝
演示最后一段用了 [('螺丝', 3), ('扳手', 12), ('电钻', 1)] 这种「名字配数量」的列表。
要按数量排,写 sorted(items, key=lambda t: t[1])。
key 就是「按什么排」,给一个函数,从每个元素里取出一列来比。
不用自己写比较逻辑,也不用把数据先拆成两个列表。这是 Python 排序最省事的地方。
想倒序加 reverse=True;想按名字排就 key=lambda t: t[0]。规则换一下就行,排序的骨架不动。
第二个坑是浅拷贝。a[:]、a.copy()、list(a) 都只复制最外面一层。
里面如果还装着列表,那些内层列表还是同一批。
演示里 nested = [[1, 2, 99], [3, 4]],浅拷贝之后改内层,两个都变。
要连里层一起复制,得用 copy.deepcopy。
什么时候需要深拷贝? 数据结构里套着可变对象,而且你要改里层。 只有一层的话,浅拷贝就够了——本书绝大部分场景都属于这种。
七、动手 Demo:函数:把一段逻辑包起来
先写最小的函数,再看默认值和关键字参数怎么用。 然后揭穿「返回三个值」的真相:它其实是一个元组。 最后两个对照最关键:局部变量出不去,但传进去的列表改得动。
print('① 最小的函数')
def area(w, h):
return w * h
print(' area(3, 4) =', area(3, 4))
print(' area(2.5, 4) =', area(2.5, 4))
def nothing():
pass
print(' 没有 return 的函数返回:', nothing())
print('② 默认值、关键字参数')
def greet(name, word='你好', punct='。'):
return '%s,%s%s' % (word, name, punct)
print(" greet('小王') =", greet('小王'))
print(" greet('小王', '早上好') =", greet('小王', '早上好'))
print(" greet('小王', punct='!') =", greet('小王', punct='!'))
print(" greet(punct='?', name='小李') =", greet(punct='?', name='小李'))
print('③ 一次返回多个值')
def stats(nums):
return min(nums), max(nums), sum(nums) / len(nums)
r = stats([9, 22, 14, 20])
print(' 返回', r)
print(' 类型', type(r).__name__)
lo, hi, avg = stats([9, 22, 14, 20])
print(' 拆开 lo=%s hi=%s avg=%.2f' % (lo, hi, avg))
print('④ 局部变量出不去')
counter = 0
def bump():
counter = 1
return counter
print(' 函数返回', bump())
print(' 函数跑完,外面 counter 还是', counter)
print('⑤ 传进去的列表会被改掉')
def add_item(box, item):
box.append(item)
def add_item2(box, item):
box = box + [item]
return box
box = ['螺丝']
add_item(box, '扳手')
print(' 传列表:add_item(box, \'扳手\') → box =', box)
box2 = ['螺丝']
new = add_item2(box2, '扳手')
print(' 改成 box = box + [item] → box2 =', box2, ' 返回的', new)
print('⑥ 再谈列表:排序与浅拷贝')
items = [('螺丝', 3), ('扳手', 12), ('电钻', 1)]
print(' 原顺序 ', items)
print(' 按数量排 ', sorted(items, key=lambda t: t[1]))
print(' 按名字排(反) ', sorted(items, key=lambda t: t[0], reverse=True))
nested = [[1, 2], [3, 4]]
shallow = nested[:]
shallow[0].append(99)
print(' 浅拷贝后改里层 nested =', nested, ' shallow =', shallow)
import copy
deep = copy.deepcopy(nested)
deep[0].append(100)
print(' 深拷贝后改里层 nested =', nested, ' deep =', deep)
实跑输出:
--------------------------------------------------------------------
函数:把一段逻辑包起来
--------------------------------------------------------------------
① 最小的函数
--------------------------------------------------------------------
def area(w, h):
return w * h
area(3, 4) = 12
area(2.5, 4) = 10.0
参数是入口,return 是出口。没有 return 的函数返回 None,不是「什么都不返回」。
② 默认值、关键字参数
--------------------------------------------------------------------
greet('小王') = 你好,小王。
greet('小王', '早上好') = 早上好,小王。
greet('小王', punct='!') = 你好,小王!
带默认值的参数要写在后面。调用时写参数名(关键字参数)可以跳着给,顺序就不重要了。
③ 一次返回多个值
--------------------------------------------------------------------
返回 (9, 22, 14.5)
拆开 lo=9 hi=22 avg=14.50
类型 tuple
看着像返回三个值,其实返回的是一个元组。左边用三个变量接,叫解包。
④ 局部变量出不去
--------------------------------------------------------------------
外面 counter = 0
函数里 counter = 1
函数跑完,外面 counter 还是 0
函数里赋值就是造一个新局部变量,跟外面同名的那个没关系。
真要改外面的,得写 global counter——但更好的做法是让它 return 出来。
⑤ 传进去的列表会被改掉
--------------------------------------------------------------------
传列表:add_item(box, '扳手') → box = ['螺丝', '扳手']
改成 box = box + [item] → box2 = ['螺丝']
append 改的是传进来那个列表本身,所以外面看得见。
写 box = box + [...] 是造了个新列表,外面就看不见。想让函数不改原数据,进去先 copy 一份。
⑥ 再谈列表:排序与浅拷贝
--------------------------------------------------------------------
原顺序 [('螺丝', 3), ('扳手', 12), ('电钻', 1)]
按数量排 [('电钻', 1), ('螺丝', 3), ('扳手', 12)]
按名字排(反) [('螺丝', 3), ('电钻', 1), ('扳手', 12)]
浅拷贝后再改里层 nested = [[1, 2, 99], [3, 4]]
key 就是「按什么排」,给个函数取出一列即可,不用自己写比较。
浅拷贝只复制最外面一层。里层还是同一批列表,改里层两个都变。
要连里层一起复制,得用 copy.deepcopy。
第一段那个 pass 是「先占个位」的写法,函数体不能空着,至少要有这一行。 它返回 None,不是「什么都没有」。 第二段最后一行调换了参数顺序,照样跑对——因为写的是参数名。参数一多,这个好处就很明显。 第三段是本篇一个反直觉点:看着返回三个值,type 一打是 tuple。 Python 没有多返回值,它是把三个值打包成一个元组再给你,左边解包拆开。 第四段和第五段是一组对照。局部变量改不了外面,但列表改得动。 差别在「可变」两个字:数字和字符串不给改,列表给改。 第六段那个 add_item 和 add_item2 的对比最值得记住:append 改的是传进来那个列表, box + [...] 造的是新列表。想让函数不动原数据,进去先 copy 一份。 浅拷贝那两行要连起来看:nested 和 shallow 的外层是两个列表,但里层还是同一批。改里层两边都变。 deepcopy 才是彻底断干净。
30 秒实验
- 把
def greet(name, word='你好', punct='。')改成def greet(word='你好', name, punct='。'), 看报的是什么错。 - 把演示第五段的
add_item(box, '扳手')改成连续调两次,看box里有几个扳手。 - 把演示第六段的
sorted(items, key=lambda t: t[1])改成sorted(items, key=lambda t: -t[1]), 看是不是也能倒序(提示:数字前面加负号就等于反着比)。
常见误区
- 「函数没有 return 就什么都不返回。」它返回
None。None是一个值,能判断、能赋值、能打印。写x = f()之后x就是None, 不是「没有 x」。 - 「参数传进函数就是复制了一份。」数字和字符串是这样,列表不是。 列表传的是标签,函数改它,外面看得见。
- 「
a.copy()复制得很干净。」它只复制最外面一层。 里面套着列表的话,那些内层还是同一批,改里层两个都变。要彻底断干净用copy.deepcopy。
自测三问(点开对答案)
1. 函数里写 `counter = 1`,为什么外面的 `counter` 没变? —— 因为函数里的赋值造了一个新的局部变量,跟外面那个同名变量没有关系。名字一样,位置不一样。 要改外面的得写 `global counter`,但更好的做法是 `return` 出来让外面接。 2. `add_item(box, '扳手')` 里写 `box.append(item)` 和写 `box = box + [item]`, 对调用方有什么不一样? —— `append` 改的是传进来那个列表本身,调用方的 `box` 也变了; `box + [item]` 造了一个新列表赋给局部名字,调用方的 `box` 一点没动。**前者是改,后者是换。** 3. `a = [[1, 2], [3, 4]]`,`b = a.copy()`,然后 `b[0].append(9)`,`a` 会变吗? —— 会。`copy()` 只复制最外面一层,`b[0]` 和 `a[0]` 还是同一个列表。 要连里层一起复制得用 `copy.deepcopy(a)`。 判断标准很简单:结构里套着可变对象、而你要改里层,就得深拷贝。第 3 章到此结束。下一篇进第 4 章:让程序去动硬盘上的文件,先从路径说起。
- ☐ (本篇完,下一篇 4.1 os 程序库与文件路径)