15

Python 函数的参数传递[编程的日常]

 3 years ago
source link: https://zhuanlan.zhihu.com/p/118671408
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Python 函数的参数传递[编程的日常]

☑编程 ☑读书 ☑翻译 ☑太极

背景:看到 @爱可可-爱生活 老师发了一条微博投票(https://weibo.com/1402400261/IAqC2CwEb):

v2-bcc0d038c3d4c302d55a95addf4bd8f5_720w.jpg

感觉好久没有用 Python 了,一时技痒,再加上这是 Python 典型的“坑”,隔三差五就能看到有人提出困惑,我就试着梳理一番。

首先,我们比较熟悉的函数传参的两种方式是:

  1. 传值:传入被调函数的是一个实参副本。被调函数中对形参的操作不会影响实参变量;
  2. 传引用:传入被调函数的是实参变量的地址,形参的操作就是寻址处理,被调函数中对形参的操作会影响实参变量。

典型的传值、传引用方式如 C 语言,C 语言中的变量 hold 住了一块内存区域。

而 Python 对变量的处理有很大不同。此外,Python 中的函数参数传递是遵循极具 Python 土味的 Call-by-Object(传递对象),也有叫 call-by-object-reference 的(传递对象的引用,或传对象的地址指向),不是 C 传统思维里的传值或传引用。

且听慢慢道来。

首先,Python 中一切皆对象,是的,数字、列表、字典等等啥都是对象。其次,Python 的变量充其量就是对象的一个引用,变量赋值操作 = 其实是把一个名字绑定到对象上,通俗语气讲,变量就是一个标签,一个名字,仅此而已,不像 C 语言那样:

a = 5  # a 是对象 5 的一个引用,a 是 5 的一个名字、标签
b = 5  # b 是对象 5 的又一个引用,b 是 5 的又一个名字、标签
​
# 看看以下的 id 值吧,都是 140732178864064(类似这样的数字):
id(a)  # 140732178864064
id(b)  # 140732178864064
id(5)  # 140732178864064

上述代码说明了 ab 都不是个事儿,它们只是个标签记号,辅助你记住 5 这个对象,5 这个对象才是主角。

来看一个函数:

a = 5
def test(p):
    p = 10
    print(p, "in test.")
​
test(a)
print(a)
​
# 结果:
10 in test.
5
  1. 给对象 5 一个名字 / 标签叫 a——a 绑定到对象,是对象的一个引用;
  2. Python 的参数传递其实也是赋值,也即前述的把名字绑定到对象。调用 test 函数时,传递的是对象 5,再确切点是对象 5 的地址指向。这时,对象 5 又多了一个名字 p,现在对象 5 有了两个名字:ap
  3. 执行 p = 10 时,p 这个名字从对象 5 上给“撕了下来”,并贴给了 10 这个对象,现在对象 5 只剩下一个名字 a 了。

就这么简单。

再来看看本道题目中的例子:

def f(x=[]):
    x.append(1)
    return x
print(f(), f())
  1. def 的时候会构造一个函数对象(名为 f)。构造函数对象的时候,发现有一个默认参数,因此也会按要求初始化默认参数——空 List 对象 [],并“贴上”标签 x。函数对象构建后,其附带的一系列方法和属性也都初始完毕,且存于内存,在后续调用中被共享;
  2. x.append(1) 会在 List 对象中追加一个项,值为 1
  3. print(f(), f()) 中第一个 f() 先调用执行,找到共享的名为 x 的 List 对象,并 append 一下 1,此时 List 为 [1]。接下来调用执行第二个 f(),仍是在已有的名为 f 的函数对象上执行 append(1),此时 List 变为 [1, 1]
  4. 好了,要打印了。这时候虽然有两处 f(),但都是同一个函数对象,其返回值也就都一样并变成了最后的结果:[1, 1],因此打印出来就有两个 [1, 1][1, 1] [1, 1]

————— 补充 —————

有人问:传统函数内部的局部变量是存在栈上,函数退出后就会销毁,为啥x没有被销毁?

函数是对象,def 时就构建了,默认参数作为属性也相应初始化(提示:f.__defaults__),而默认参数值 List 对象一直伴随着函数对象存在。这里每次都是返回同个函数对象的这个 List 对象的地址指向(跟传参一样),在打印时才取出 List 值,但这时 List 值已经是第二次 f() 后的值了。甚至你还可以比如这样看看:

f()
print(f.__defaults__[0])
f()
print(f.__defaults__[0])

Python 一切皆对象可不是闹着玩的,谁说 Python 简单了?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK